Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-vdirsyncer for 
openSUSE:Factory checked in at 2025-12-16 15:56:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-vdirsyncer (Old)
 and      /work/SRC/openSUSE:Factory/.python-vdirsyncer.new.1939 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-vdirsyncer"

Tue Dec 16 15:56:33 2025 rev:22 rq:1323041 version:0.20.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-vdirsyncer/python-vdirsyncer.changes      
2025-07-22 12:21:25.430934796 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-vdirsyncer.new.1939/python-vdirsyncer.changes
    2025-12-16 16:02:44.303240390 +0100
@@ -1,0 +2,13 @@
+Tue Dec 16 04:25:20 UTC 2025 - Steve Kowalik <[email protected]>
+
+- Update to 0.20.0:
+  * Remove dependency on abandoned atomicwrites library.
+  * Implement filter_hook for the HTTP storage.
+  * Drop support for Python 3.7.
+  * Add support for Python 3.12 and Python 3.13.
+  * Properly close the status database after using. This especially affects
+    tests, where we were leaking a large amount of file descriptors.
+  * Extend supported versions of aiostream to include 0.7.x.
+- Drop patch support-new-pytest-asyncio.patch, no longer required.
+
+-------------------------------------------------------------------

Old:
----
  support-new-pytest-asyncio.patch
  vdirsyncer-0.19.3.tar.gz

New:
----
  vdirsyncer-0.20.0.tar.gz

----------(Old B)----------
  Old:  * Extend supported versions of aiostream to include 0.7.x.
- Drop patch support-new-pytest-asyncio.patch, no longer required.
----------(Old E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-vdirsyncer.spec ++++++
--- /var/tmp/diff_new_pack.L9QhLH/_old  2025-12-16 16:02:46.011312581 +0100
+++ /var/tmp/diff_new_pack.L9QhLH/_new  2025-12-16 16:02:46.015312750 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-vdirsyncer
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2025 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           python-vdirsyncer
-Version:        0.19.3
+Version:        0.20.0
 Release:        0
 Summary:        CalDAV and CardDAV synchronization module
 License:        BSD-3-Clause
@@ -25,9 +25,7 @@
 Source0:        
https://files.pythonhosted.org/packages/source/v/vdirsyncer/vdirsyncer-%{version}.tar.gz
 Source1:        vdirsyncer.service
 Source2:        vdirsyncer.timer
-# PATCH-FIX-OPENSUSE Support pytest-asyncio 1.0 changes.
-Patch0:         support-new-pytest-asyncio.patch
-BuildRequires:  %{python_module atomicwrites}
+BuildRequires:  %{python_module base >= 3.8}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module setuptools_scm}
 BuildRequires:  %{python_module wheel}
@@ -38,7 +36,6 @@
 BuildRequires:  pkgconfig(systemd)
 Requires:       python-aiohttp
 Requires:       python-aiostream
-Requires:       python-atomicwrites >= 0.1.7
 Requires:       python-click >= 5.0
 Requires:       python-click-log >= 0.3
 Requires:       python-requests >= 2.20.0

++++++ vdirsyncer-0.19.3.tar.gz -> vdirsyncer-0.20.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.builds/archlinux-py313.yml 
new/vdirsyncer-0.20.0/.builds/archlinux-py313.yml
--- old/vdirsyncer-0.19.3/.builds/archlinux-py313.yml   1970-01-01 
01:00:00.000000000 +0100
+++ new/vdirsyncer-0.20.0/.builds/archlinux-py313.yml   2025-08-28 
22:57:38.000000000 +0200
@@ -0,0 +1,49 @@
+# Run tests using the packaged dependencies on ArchLinux.
+
+image: archlinux
+packages:
+  - docker
+  - docker-compose
+  # Build dependencies:
+  - python-wheel
+  - python-build
+  - python-installer
+  - python-setuptools-scm
+  # Runtime dependencies:
+  - python-click
+  - python-click-log
+  - python-click-threading
+  - python-requests
+  - python-requests-toolbelt
+  - python-aiohttp-oauthlib
+  # Test dependencies:
+  - python-hypothesis
+  - python-pytest-cov
+  - python-pytest-httpserver
+  - python-trustme
+  - python-pytest-asyncio
+  - python-aiohttp
+  - python-aiostream
+  - python-aioresponses
+sources:
+  - https://github.com/pimutils/vdirsyncer
+environment:
+  BUILD: test
+  CI: true
+  CODECOV_TOKEN: b834a3c5-28fa-4808-9bdb-182210069c79
+  DAV_SERVER: radicale xandikos
+  REQUIREMENTS: release
+  # TODO: ETESYNC_TESTS
+tasks:
+  - check-python:
+      python --version | grep 'Python 3.13'
+  - docker: |
+      sudo systemctl start docker
+  - setup: |
+      cd vdirsyncer
+      python -m build --wheel --skip-dependency-check --no-isolation
+      sudo python -m installer dist/*.whl
+  - test: |
+      cd vdirsyncer
+      make -e ci-test
+      make -e ci-test-storage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.builds/tests-archlinux.yml 
new/vdirsyncer-0.20.0/.builds/tests-archlinux.yml
--- old/vdirsyncer-0.19.3/.builds/tests-archlinux.yml   2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/.builds/tests-archlinux.yml   1970-01-01 
01:00:00.000000000 +0100
@@ -1,48 +0,0 @@
-# Run tests using the packaged dependencies on ArchLinux.
-
-image: archlinux
-packages:
-  - docker
-  - docker-compose
-  # Build dependencies:
-  - python-wheel
-  - python-build
-  - python-installer
-  - python-setuptools-scm
-  # Runtime dependencies:
-  - python-atomicwrites
-  - python-click
-  - python-click-log
-  - python-click-threading
-  - python-requests
-  - python-requests-toolbelt
-  - python-aiohttp-oauthlib
-  # Test dependencies:
-  - python-hypothesis
-  - python-pytest-cov
-  - python-pytest-httpserver
-  - python-trustme
-  - python-pytest-asyncio
-  - python-aiohttp
-  - python-aiostream
-  - python-aioresponses
-sources:
-  - https://github.com/pimutils/vdirsyncer
-environment:
-  BUILD: test
-  CI: true
-  CODECOV_TOKEN: b834a3c5-28fa-4808-9bdb-182210069c79
-  DAV_SERVER: radicale xandikos
-  REQUIREMENTS: release
-  # TODO: ETESYNC_TESTS
-tasks:
-  - docker: |
-      sudo systemctl start docker
-  - setup: |
-      cd vdirsyncer
-      python -m build --wheel --skip-dependency-check --no-isolation
-      sudo python -m installer dist/*.whl
-  - test: |
-      cd vdirsyncer
-      make -e ci-test
-      make -e ci-test-storage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.builds/tests-minimal.yml 
new/vdirsyncer-0.20.0/.builds/tests-minimal.yml
--- old/vdirsyncer-0.19.3/.builds/tests-minimal.yml     2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/.builds/tests-minimal.yml     2025-08-28 
22:57:38.000000000 +0200
@@ -3,7 +3,7 @@
 # TODO: It might make more sense to test with an older Ubuntu or Fedora version
 # here, and consider that our "oldest suppported environment".
 
-image: alpine/3.17 # python 3.10
+image: alpine/3.19 # python 3.11
 packages:
   - docker
   - docker-cli
@@ -18,7 +18,6 @@
   CODECOV_TOKEN: b834a3c5-28fa-4808-9bdb-182210069c79
   DAV_SERVER: radicale xandikos
   REQUIREMENTS: minimal
-  # TODO: ETESYNC_TESTS
 tasks:
   - venv: |
       python3 -m venv $HOME/venv
@@ -28,6 +27,8 @@
       sudo service docker start
   - setup: |
       cd vdirsyncer
+      # Hack, no idea why it's needed
+      sudo ln -s /usr/include/python3.11/cpython/longintrepr.h 
/usr/include/python3.11/longintrepr.h
       make -e install-dev
   - test: |
       cd vdirsyncer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.envrc new/vdirsyncer-0.20.0/.envrc
--- old/vdirsyncer-0.19.3/.envrc        1970-01-01 01:00:00.000000000 +0100
+++ new/vdirsyncer-0.20.0/.envrc        2025-08-28 22:57:38.000000000 +0200
@@ -0,0 +1 @@
+layout python3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.gitlab-ci.yml 
new/vdirsyncer-0.20.0/.gitlab-ci.yml
--- old/vdirsyncer-0.19.3/.gitlab-ci.yml        2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/.gitlab-ci.yml        1970-01-01 01:00:00.000000000 
+0100
@@ -1,6 +0,0 @@
-python37:
-  image: python:3.7
-  before_script:
-    - make -e install-dev
-  script:
-    - make -e ci-test
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.pre-commit-config.yaml 
new/vdirsyncer-0.20.0/.pre-commit-config.yaml
--- old/vdirsyncer-0.19.3/.pre-commit-config.yaml       2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/.pre-commit-config.yaml       2025-08-28 
22:57:38.000000000 +0200
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v5.0.0
     hooks:
       - id: trailing-whitespace
         args: [--markdown-linebreak-ext=md]
@@ -8,12 +8,8 @@
       - id: check-toml
       - id: check-added-large-files
       - id: debug-statements
-  - repo: https://github.com/psf/black
-    rev: "24.2.0"
-    hooks:
-      - id: black
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: "v1.8.0"
+    rev: "v1.15.0"
     hooks:
       - id: mypy
         files: vdirsyncer/.*
@@ -21,12 +17,12 @@
           - types-setuptools
           - types-docutils
           - types-requests
-          - types-atomicwrites
   - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: 'v0.2.2'
+    rev: 'v0.11.4'
     hooks:
       - id: ruff
         args: [--fix, --exit-non-zero-on-fix]
+      - id: ruff-format
   - repo: local
     hooks:
       - id: typos-syncroniz
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/.readthedocs.yaml 
new/vdirsyncer-0.20.0/.readthedocs.yaml
--- old/vdirsyncer-0.19.3/.readthedocs.yaml     1970-01-01 01:00:00.000000000 
+0100
+++ new/vdirsyncer-0.20.0/.readthedocs.yaml     2025-08-28 22:57:38.000000000 
+0200
@@ -0,0 +1,15 @@
+version: 2
+
+sphinx:
+  configuration: docs/conf.py
+
+build:
+  os: "ubuntu-22.04"
+  tools:
+    python: "3.9"
+
+python:
+  install:
+    - method: pip
+      path: .
+    - requirements: docs-requirements.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/CHANGELOG.rst 
new/vdirsyncer-0.20.0/CHANGELOG.rst
--- old/vdirsyncer-0.19.3/CHANGELOG.rst 2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/CHANGELOG.rst 2025-08-28 22:57:38.000000000 +0200
@@ -9,6 +9,17 @@
 may want to subscribe to `GitHub's tag feed
 <https://github.com/pimutils/vdirsyncer/tags.atom>`_.
 
+Version 0.20.0
+==============
+
+- Remove dependency on abandoned ``atomicwrites`` library.
+- Implement ``filter_hook`` for the HTTP storage.
+- Drop support for Python 3.7.
+- Add support for Python 3.12 and Python 3.13.
+- Properly close the status database after using. This especially affects 
tests,
+  where we were leaking a large amount of file descriptors.
+- Extend supported versions of ``aiostream`` to include 0.7.x.
+
 Version 0.19.3
 ==============
 
@@ -18,6 +29,7 @@
 - Require matching ``BEGIN`` and ``END`` lines in vobjects. :gh:`1103`
 - A Docker environment for Vdirsyncer has been added `Vdirsyncer DOCKERIZED 
<https://github.com/Bleala/Vdirsyncer-DOCKERIZED>`_.
 - Implement digest auth. :gh:`1137`
+- Add ``filter_hook`` parameter to :storage:`http`. :gh:`1136`
 
 Version 0.19.2
 ==============
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/Makefile 
new/vdirsyncer-0.20.0/Makefile
--- old/vdirsyncer-0.19.3/Makefile      2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/Makefile      2025-08-28 22:57:38.000000000 +0200
@@ -49,10 +49,11 @@
 
 install-dev:
        pip install -U pip setuptools wheel
-       pip install -e .
-       pip install -Ur test-requirements.txt -r docs-requirements.txt 
pre-commit
+       pip install -e '.[test]'
+       pip install -U -r docs-requirements.txt pre-commit
        set -xe && if [ "$(REQUIREMENTS)" = "minimal" ]; then \
-               pip install -U --force-reinstall $$(python setup.py --quiet 
minimal_requirements); \
+               pip install pyproject-dependencies && \
+               pip install -U --force-reinstall $$(pyproject-dependencies . | 
sed 's/>/=/'); \
        fi
 
 .PHONY: docs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/PKG-INFO 
new/vdirsyncer-0.20.0/PKG-INFO
--- old/vdirsyncer-0.19.3/PKG-INFO      2024-09-11 17:27:00.646295000 +0200
+++ new/vdirsyncer-0.20.0/PKG-INFO      2025-08-28 22:57:40.347712000 +0200
@@ -1,33 +1,42 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: vdirsyncer
-Version: 0.19.3
+Version: 0.20.0
 Summary: Synchronize calendars and contacts
-Home-page: https://github.com/pimutils/vdirsyncer
-Author: Markus Unterwaditzer
-Author-email: [email protected]
-License: BSD
+Author-email: Markus Unterwaditzer <[email protected]>
+License-Expression: BSD-3-Clause
+Keywords: todo,task,icalendar,cli
 Classifier: Development Status :: 4 - Beta
 Classifier: Environment :: Console
-Classifier: License :: OSI Approved :: BSD License
 Classifier: Operating System :: POSIX
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Internet
+Classifier: Topic :: Office/Business :: Scheduling
 Classifier: Topic :: Utilities
+Requires-Python: >=3.8
+Description-Content-Type: text/x-rst
 License-File: LICENSE
-License-File: AUTHORS.rst
 Requires-Dist: click<9.0,>=5.0
 Requires-Dist: click-log<0.5.0,>=0.3.0
 Requires-Dist: requests>=2.20.0
-Requires-Dist: atomicwrites>=0.1.7
-Requires-Dist: aiohttp<4.0.0,>=3.8.0
-Requires-Dist: aiostream<0.5.0,>=0.4.3
+Requires-Dist: aiohttp<4.0.0,>=3.8.2
+Requires-Dist: aiostream<0.8.0,>=0.4.3
 Provides-Extra: google
 Requires-Dist: aiohttp-oauthlib; extra == "google"
+Provides-Extra: test
+Requires-Dist: hypothesis<7.0.0,>=6.72.0; extra == "test"
+Requires-Dist: pytest; extra == "test"
+Requires-Dist: pytest-cov; extra == "test"
+Requires-Dist: pytest-httpserver; extra == "test"
+Requires-Dist: trustme; extra == "test"
+Requires-Dist: pytest-asyncio; extra == "test"
+Requires-Dist: aioresponses; extra == "test"
+Dynamic: license-file
 
 ==========
 vdirsyncer
@@ -71,7 +80,7 @@
 between two servers directly.
 
 It aims to be for calendars and contacts what `OfflineIMAP
-<http://offlineimap.org/>`_ is for emails.
+<https://www.offlineimap.org/>`_ is for emails.
 
 .. _programs: https://vdirsyncer.pimutils.org/en/latest/tutorials/
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/README.rst 
new/vdirsyncer-0.20.0/README.rst
--- old/vdirsyncer-0.19.3/README.rst    2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/README.rst    2025-08-28 22:57:38.000000000 +0200
@@ -40,7 +40,7 @@
 between two servers directly.
 
 It aims to be for calendars and contacts what `OfflineIMAP
-<http://offlineimap.org/>`_ is for emails.
+<https://www.offlineimap.org/>`_ is for emails.
 
 .. _programs: https://vdirsyncer.pimutils.org/en/latest/tutorials/
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vdirsyncer-0.19.3/contrib/conflict_resolution/resolve_interactively.py 
new/vdirsyncer-0.20.0/contrib/conflict_resolution/resolve_interactively.py
--- old/vdirsyncer-0.19.3/contrib/conflict_resolution/resolve_interactively.py  
2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/contrib/conflict_resolution/resolve_interactively.py  
2025-08-28 22:57:38.000000000 +0200
@@ -16,6 +16,7 @@
 SPDX-FileCopyrightText: 2021 Intevation GmbH <https://intevation.de>
 Author: <[email protected]>
 """
+
 from __future__ import annotations
 
 import re
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/docs/conf.py 
new/vdirsyncer-0.20.0/docs/conf.py
--- old/vdirsyncer-0.19.3/docs/conf.py  2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/docs/conf.py  2025-08-28 22:57:38.000000000 +0200
@@ -37,9 +37,7 @@
     html_theme = "default"
     if not on_rtd:
         print("-" * 74)
-        print(
-            "Warning: sphinx-rtd-theme not installed, building with default " 
"theme."
-        )
+        print("Warning: sphinx-rtd-theme not installed, building with default 
theme.")
         print("-" * 74)
 
 html_static_path = ["_static"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/docs/config.rst 
new/vdirsyncer-0.20.0/docs/config.rst
--- old/vdirsyncer-0.19.3/docs/config.rst       2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/docs/config.rst       2025-08-28 22:57:38.000000000 
+0200
@@ -484,6 +484,7 @@
         [storage holidays_remote]
         type = "http"
         url = https://example.com/holidays_from_hicksville.ics
+        #filter_hook = null
 
     Too many WebCAL providers generate UIDs of all ``VEVENT``-components
     on-the-fly, i.e. all UIDs change every time the calendar is downloaded.
@@ -508,3 +509,8 @@
     :param auth_cert: Optional. Either a path to a certificate with a client
         certificate and the key or a list of paths to the files with them.
     :param useragent: Default ``vdirsyncer``.
+    :param filter_hook: Optional. A filter command to call for each fetched
+        item, passed in raw form to stdin and returned via stdout.
+        If nothing is returned by the filter command, the item is skipped.
+        This can be used to alter fields as needed when dealing with providers
+        generating malformed events.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/docs/contributing.rst 
new/vdirsyncer-0.20.0/docs/contributing.rst
--- old/vdirsyncer-0.19.3/docs/contributing.rst 2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/docs/contributing.rst 2025-08-28 22:57:38.000000000 
+0200
@@ -81,7 +81,7 @@
 
     # Install development dependencies, including:
     #  - vdirsyncer from the repo into the virtualenv
-    #  - stylecheckers (ruff) and code formatters (black)
+    #  - style checks and formatting (ruff)
     make install-dev
 
     # Install git commit hook for some extra linting and checking
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/docs/installation.rst 
new/vdirsyncer-0.20.0/docs/installation.rst
--- old/vdirsyncer-0.19.3/docs/installation.rst 2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/docs/installation.rst 2025-08-28 22:57:38.000000000 
+0200
@@ -42,7 +42,7 @@
 use Python's package manager "pip". First, you'll have to check that the
 following things are installed:
 
-- Python 3.7 to 3.11 and pip.
+- Python 3.8 to 3.13 and pip.
 - ``libxml`` and ``libxslt``
 - ``zlib``
 - Linux or macOS. **Windows is not supported**, see :gh:`535`.
@@ -84,7 +84,7 @@
 The dirty, easy way
 ~~~~~~~~~~~~~~~~~~~
 
-If pipx is not available on your distirbution, the easiest way to install
+If pipx is not available on your distribution, the easiest way to install
 vdirsyncer at this point would be to run::
 
     pip install --ignore-installed vdirsyncer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/docs/packaging.rst 
new/vdirsyncer-0.20.0/docs/packaging.rst
--- old/vdirsyncer-0.19.3/docs/packaging.rst    2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/docs/packaging.rst    2025-08-28 22:57:38.000000000 
+0200
@@ -46,8 +46,9 @@
     make install-dev
 
 You probably don't want this since it will use pip to download the
-dependencies. Alternatively you can find the testing dependencies in
-``test-requirements.txt``, again with lower-bound version requirements.
+dependencies. Alternatively test dependencies are listed as ``test`` optional
+dependencies in ``pyproject.toml``, again with lower-bound version
+requirements.
 
 You also have to have vdirsyncer fully installed at this point. Merely
 ``cd``-ing into the tarball will not be sufficient.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/docs/when.rst 
new/vdirsyncer-0.20.0/docs/when.rst
--- old/vdirsyncer-0.19.3/docs/when.rst 2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/docs/when.rst 2025-08-28 22:57:38.000000000 +0200
@@ -50,7 +50,6 @@
 
 * Such a setup doesn't work at all with smartphones. Vdirsyncer, on the other
   hand, synchronizes with CardDAV/CalDAV servers, which can be accessed with
-  e.g. DAVx⁵_ or the apps by dmfs_.
+  e.g. DAVx⁵_ or other apps bundled with smartphones.
 
 .. _DAVx⁵: https://www.davx5.com/
-.. _dmfs: https://dmfs.org/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/pyproject.toml 
new/vdirsyncer-0.20.0/pyproject.toml
--- old/vdirsyncer-0.19.3/pyproject.toml        2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/pyproject.toml        2025-08-28 22:57:38.000000000 
+0200
@@ -1,18 +1,75 @@
-[tool.ruff]
-select = [
+# Vdirsyncer synchronizes calendars and contacts.
+#
+# Please refer to https://vdirsyncer.pimutils.org/en/stable/packaging.html for
+# how to package vdirsyncer.
+
+[build-system]
+requires = ["setuptools>=64", "setuptools_scm>=8"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "vdirsyncer"
+authors = [
+    {name = "Markus Unterwaditzer", email = "[email protected]"},
+]
+description = "Synchronize calendars and contacts"
+readme = "README.rst"
+requires-python = ">=3.8"
+keywords = ["todo", "task", "icalendar", "cli"]
+license = "BSD-3-Clause"
+license-files = ["LICENSE"]
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Environment :: Console",
+    "Operating System :: POSIX",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Topic :: Internet",
+    "Topic :: Office/Business :: Scheduling",
+    "Topic :: Utilities",
+]
+dependencies = [
+    "click>=5.0,<9.0",
+    "click-log>=0.3.0,<0.5.0",
+    "requests>=2.20.0",
+    "aiohttp>=3.8.2,<4.0.0",
+    "aiostream>=0.4.3,<0.8.0",
+]
+dynamic = ["version"]
+
+[project.optional-dependencies]
+google = ["aiohttp-oauthlib"]
+test = [
+  "hypothesis>=6.72.0,<7.0.0",
+  "pytest",
+  "pytest-cov",
+  "pytest-httpserver",
+  "trustme",
+  "pytest-asyncio",
+  "aioresponses",
+]
+
+[project.scripts]
+vdirsyncer = "vdirsyncer.cli:app"
+
+[tool.lint.ruff]
+extend-select = [
     "E",
-    "F",
     "W",
     "B0",
     "I",
     "UP",
     "C4",
-    # "TID",
+    "TID",
     "RSE"
 ]
-target-version = "py37"
 
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
 force-single-line = true
 required-imports = ["from __future__ import annotations"]
 
@@ -26,6 +83,7 @@
 --color=yes
 """
 # filterwarnings=error
+asyncio_default_fixture_loop_scope = "function"
 
 [tool.mypy]
 ignore_missing_imports = true
@@ -34,3 +92,10 @@
 exclude_lines = [
     "if TYPE_CHECKING:",
 ]
+
+[tool.setuptools.packages.find]
+include = ["vdirsyncer*"]
+
+[tool.setuptools_scm]
+write_to = "vdirsyncer/version.py"
+version_scheme = "no-guess-dev"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/setup.py 
new/vdirsyncer-0.20.0/setup.py
--- old/vdirsyncer-0.19.3/setup.py      2024-09-11 17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/setup.py      1970-01-01 01:00:00.000000000 +0100
@@ -1,82 +0,0 @@
-"""
-Vdirsyncer synchronizes calendars and contacts.
-
-Please refer to https://vdirsyncer.pimutils.org/en/stable/packaging.html for
-how to package vdirsyncer.
-"""
-
-from __future__ import annotations
-
-from setuptools import Command
-from setuptools import find_packages
-from setuptools import setup
-
-requirements = [
-    # https://github.com/mitsuhiko/click/issues/200
-    "click>=5.0,<9.0",
-    "click-log>=0.3.0, <0.5.0",
-    "requests >=2.20.0",
-    # 
https://github.com/untitaker/python-atomicwrites/commit/4d12f23227b6a944ab1d99c507a69fdbc7c9ed6d
  # noqa
-    "atomicwrites>=0.1.7",
-    "aiohttp>=3.8.0,<4.0.0",
-    "aiostream>=0.4.3,<0.5.0",
-]
-
-
-class PrintRequirements(Command):
-    description = "Prints minimal requirements"
-    user_options: list = []
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def run(self):
-        for requirement in requirements:
-            print(requirement.replace(">", "=").replace(" ", ""))
-
-
-with open("README.rst") as f:
-    long_description = f.read()
-
-
-setup(
-    # General metadata
-    name="vdirsyncer",
-    author="Markus Unterwaditzer",
-    author_email="[email protected]",
-    url="https://github.com/pimutils/vdirsyncer";,
-    description="Synchronize calendars and contacts",
-    license="BSD",
-    long_description=long_description,
-    # Runtime dependencies
-    install_requires=requirements,
-    # Optional dependencies
-    extras_require={
-        "google": ["aiohttp-oauthlib"],
-    },
-    # Build dependencies
-    setup_requires=["setuptools_scm != 1.12.0"],
-    # Other
-    packages=find_packages(exclude=["tests.*", "tests"]),
-    include_package_data=True,
-    cmdclass={"minimal_requirements": PrintRequirements},
-    use_scm_version={"write_to": "vdirsyncer/version.py"},
-    entry_points={"console_scripts": ["vdirsyncer = vdirsyncer.cli:app"]},
-    classifiers=[
-        "Development Status :: 4 - Beta",
-        "Environment :: Console",
-        "License :: OSI Approved :: BSD License",
-        "Operating System :: POSIX",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Topic :: Internet",
-        "Topic :: Utilities",
-    ],
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/test-requirements.txt 
new/vdirsyncer-0.20.0/test-requirements.txt
--- old/vdirsyncer-0.19.3/test-requirements.txt 2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/test-requirements.txt 1970-01-01 01:00:00.000000000 
+0100
@@ -1,7 +0,0 @@
-hypothesis>=5.0.0,<7.0.0
-pytest
-pytest-cov
-pytest-httpserver
-trustme
-pytest-asyncio
-aioresponses
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/__init__.py 
new/vdirsyncer-0.20.0/tests/__init__.py
--- old/vdirsyncer-0.19.3/tests/__init__.py     2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/tests/__init__.py     2025-08-28 22:57:38.000000000 
+0200
@@ -103,10 +103,8 @@
 HAHA:YES
 END:FOO"""
 
-printable_characters_strategy = st.text(
-    st.characters(blacklist_categories=("Cc", "Cs"))
-)
+printable_characters_strategy = 
st.text(st.characters(exclude_categories=("Cc", "Cs")))
 
 uid_strategy = st.text(
-    st.characters(blacklist_categories=("Zs", "Zl", "Zp", "Cc", "Cs")), 
min_size=1
+    st.characters(exclude_categories=("Zs", "Zl", "Zp", "Cc", "Cs")), 
min_size=1
 ).filter(lambda x: x.strip() == x)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/conftest.py 
new/vdirsyncer-0.20.0/tests/conftest.py
--- old/vdirsyncer-0.19.3/tests/conftest.py     2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/tests/conftest.py     2025-08-28 22:57:38.000000000 
+0200
@@ -45,7 +45,7 @@
     "deterministic",
     settings(
         derandomize=True,
-        suppress_health_check=HealthCheck.all(),
+        suppress_health_check=list(HealthCheck),
     ),
 )
 settings.register_profile("dev", 
settings(suppress_health_check=[HealthCheck.too_slow]))
@@ -59,12 +59,12 @@
 
 
 @pytest_asyncio.fixture
-async def aio_session(event_loop):
+async def aio_session():
     async with aiohttp.ClientSession() as session:
         yield session
 
 
 @pytest_asyncio.fixture
-async def aio_connector(event_loop):
+async def aio_connector():
     async with aiohttp.TCPConnector(limit_per_host=16) as conn:
         yield conn
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/system/cli/test_sync.py 
new/vdirsyncer-0.20.0/tests/system/cli/test_sync.py
--- old/vdirsyncer-0.19.3/tests/system/cli/test_sync.py 2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/tests/system/cli/test_sync.py 2025-08-28 
22:57:38.000000000 +0200
@@ -90,9 +90,7 @@
     result = runner.invoke(["sync"])
     lines = result.output.splitlines()
     assert lines[0] == "Syncing my_pair"
-    assert lines[1].startswith(
-        "error: my_pair: " 'Storage "my_b" was completely emptied.'
-    )
+    assert lines[1].startswith('error: my_pair: Storage "my_b" was completely 
emptied.')
     assert result.exception
 
 
@@ -553,9 +551,7 @@
     type = "filesystem"
     path = "{path}"
     fileext.fetch = ["command", "sh", "{script}"]
-    """.format(
-                path=str(tmpdir.mkdir("bogus")), script=str(fetch_script)
-            )
+    """.format(path=str(tmpdir.mkdir("bogus")), script=str(fetch_script))
         )
     )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/unit/sync/test_status.py 
new/vdirsyncer-0.20.0/tests/unit/sync/test_status.py
--- old/vdirsyncer-0.19.3/tests/unit/sync/test_status.py        2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/tests/unit/sync/test_status.py        2025-08-28 
22:57:38.000000000 +0200
@@ -34,3 +34,5 @@
         assert meta2_a.to_status() == meta_a
         assert meta2_b.to_status() == meta_b
         assert ident_a == ident_b == ident
+
+    status.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/unit/sync/test_sync.py 
new/vdirsyncer-0.20.0/tests/unit/sync/test_sync.py
--- old/vdirsyncer-0.19.3/tests/unit/sync/test_sync.py  2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/tests/unit/sync/test_sync.py  2025-08-28 
22:57:38.000000000 +0200
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import asyncio
+import contextlib
 from copy import deepcopy
 
 import aiostream
@@ -25,13 +26,12 @@
 from vdirsyncer.vobject import Item
 
 
-async def sync(a, b, status, *args, **kwargs):
-    new_status = SqliteStatus(":memory:")
-    new_status.load_legacy_status(status)
-    rv = await _sync(a, b, new_status, *args, **kwargs)
-    status.clear()
-    status.update(new_status.to_legacy_status())
-    return rv
+async def sync(a, b, status, *args, **kwargs) -> None:
+    with contextlib.closing(SqliteStatus(":memory:")) as new_status:
+        new_status.load_legacy_status(status)
+        await _sync(a, b, new_status, *args, **kwargs)
+        status.clear()
+        status.update(new_status.to_legacy_status())
 
 
 def empty_storage(x):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/unit/test_metasync.py 
new/vdirsyncer-0.20.0/tests/unit/test_metasync.py
--- old/vdirsyncer-0.19.3/tests/unit/test_metasync.py   2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/tests/unit/test_metasync.py   2025-08-28 
22:57:38.000000000 +0200
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import asyncio
+
 import hypothesis.strategies as st
 import pytest
 import pytest_asyncio
@@ -56,23 +58,19 @@
 
 
 @pytest_asyncio.fixture
[email protected]
-async def conflict_state(request, event_loop):
+async def conflict_state(request):
     a = MemoryStorage()
     b = MemoryStorage()
     status = {}
     await a.set_meta("foo", "bar")
     await b.set_meta("foo", "baz")
 
-    def cleanup():
-        async def do_cleanup():
-            assert await a.get_meta("foo") == "bar"
-            assert await b.get_meta("foo") == "baz"
-            assert not status
-
-        event_loop.run_until_complete(do_cleanup())
+    async def do_cleanup():
+        assert await a.get_meta("foo") == "bar"
+        assert await b.get_meta("foo") == "baz"
+        assert not status
 
-    request.addfinalizer(cleanup)
+    request.addfinalizer(lambda: asyncio.run(do_cleanup()))
 
     return a, b, status
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/unit/test_repair.py 
new/vdirsyncer-0.20.0/tests/unit/test_repair.py
--- old/vdirsyncer-0.19.3/tests/unit/test_repair.py     2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/tests/unit/test_repair.py     2025-08-28 
22:57:38.000000000 +0200
@@ -17,7 +17,7 @@
 
 @given(uid=uid_strategy)
 # Using the random module for UIDs:
-@settings(suppress_health_check=HealthCheck.all())
+@settings(suppress_health_check=list(HealthCheck))
 @pytest.mark.asyncio
 async def test_repair_uids(uid):
     s = MemoryStorage()
@@ -40,7 +40,7 @@
 
 @given(uid=uid_strategy.filter(lambda x: not href_safe(x)))
 # Using the random module for UIDs:
-@settings(suppress_health_check=HealthCheck.all())
+@settings(suppress_health_check=list(HealthCheck))
 @pytest.mark.asyncio
 async def test_repair_unsafe_uids(uid):
     s = MemoryStorage()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/tests/unit/utils/test_vobject.py 
new/vdirsyncer-0.20.0/tests/unit/utils/test_vobject.py
--- old/vdirsyncer-0.19.3/tests/unit/utils/test_vobject.py      2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/tests/unit/utils/test_vobject.py      2025-08-28 
22:57:38.000000000 +0200
@@ -154,7 +154,7 @@
 
 
 def test_multiline_uid(benchmark):
-    a = "BEGIN:FOO\r\n" "UID:123456789abcd\r\n" " efgh\r\n" "END:FOO\r\n"
+    a = "BEGIN:FOO\r\nUID:123456789abcd\r\n efgh\r\nEND:FOO\r\n"
     assert benchmark(lambda: vobject.Item(a).uid) == "123456789abcdefgh"
 
 
@@ -299,7 +299,7 @@
 
 value_strategy = st.text(
     st.characters(
-        blacklist_categories=("Zs", "Zl", "Zp", "Cc", "Cs"), 
blacklist_characters=":="
+        exclude_categories=("Zs", "Zl", "Zp", "Cc", "Cs"), 
exclude_characters=":="
     ),
     min_size=1,
 ).filter(lambda x: x.strip() == x)
@@ -365,6 +365,16 @@
 TestVobjectMachine = VobjectMachine.TestCase
 
 
+def test_dupe_consecutive_keys():
+    state = VobjectMachine()
+    unparsed_0 = state.get_unparsed_lines(encoded=False, joined=False)
+    parsed_0 = state.parse(unparsed=unparsed_0)
+    state.add_prop_raw(c=parsed_0, key="0", params=[], value="0")
+    state.add_prop_raw(c=parsed_0, key="0", params=[], value="0")
+    state.add_prop(c=parsed_0, key="0", value="1")
+    state.teardown()
+
+
 def test_component_contains():
     item = vobject._Component.parse(["BEGIN:FOO", "FOO:YES", "END:FOO"])
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/__init__.py 
new/vdirsyncer-0.20.0/vdirsyncer/__init__.py
--- old/vdirsyncer-0.19.3/vdirsyncer/__init__.py        2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/__init__.py        2025-08-28 
22:57:38.000000000 +0200
@@ -21,8 +21,8 @@
 def _check_python_version():
     import sys
 
-    if sys.version_info < (3, 7, 0):  # noqa: UP036
-        print("vdirsyncer requires at least Python 3.7.")
+    if sys.version_info < (3, 8, 0):  # noqa: UP036
+        print("vdirsyncer requires at least Python 3.8.")
         sys.exit(1)
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/cli/config.py 
new/vdirsyncer-0.20.0/vdirsyncer/cli/config.py
--- old/vdirsyncer-0.19.3/vdirsyncer/cli/config.py      2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/cli/config.py      2025-08-28 
22:57:38.000000000 +0200
@@ -8,6 +8,7 @@
 from typing import IO
 from typing import Any
 from typing import Generator
+from typing import Tuple
 
 from .. import PROJECT_HOME
 from .. import exceptions
@@ -231,7 +232,7 @@
         self.name_b: str = options.pop("b")
 
         self._partial_sync: str | None = options.pop("partial_sync", None)
-        self.metadata = options.pop("metadata", None) or ()
+        self.metadata: str | Tuple[()] = options.pop("metadata", ())
 
         self.conflict_resolution = self._process_conflict_resolution_param(
             options.pop("conflict_resolution", None)
@@ -359,7 +360,7 @@
             new_b = f.read()
 
         if new_a != new_b:
-            raise exceptions.UserError("The two files are not completely " 
"equal.")
+            raise exceptions.UserError("The two files are not completely 
equal.")
         return Item(new_a)
     finally:
         shutil.rmtree(dir)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/cli/discover.py 
new/vdirsyncer-0.20.0/vdirsyncer/cli/discover.py
--- old/vdirsyncer-0.19.3/vdirsyncer/cli/discover.py    2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/cli/discover.py    2025-08-28 
22:57:38.000000000 +0200
@@ -72,8 +72,7 @@
             )
         else:
             raise exceptions.UserError(
-                f"Please run `vdirsyncer discover {pair.name}` "
-                " before synchronization."
+                f"Please run `vdirsyncer discover {pair.name}`  before 
synchronization."
             )
 
     logger.info(f"Discovering collections for pair {pair.name}")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/cli/utils.py 
new/vdirsyncer-0.20.0/vdirsyncer/cli/utils.py
--- old/vdirsyncer-0.19.3/vdirsyncer/cli/utils.py       2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/cli/utils.py       2025-08-28 
22:57:38.000000000 +0200
@@ -10,7 +10,6 @@
 
 import aiohttp
 import click
-from atomicwrites import atomic_write
 
 from .. import BUGTRACKER_HOME
 from .. import DOCS_HOME
@@ -21,6 +20,7 @@
 from ..sync.exceptions import StorageEmpty
 from ..sync.exceptions import SyncConflict
 from ..sync.status import SqliteStatus
+from ..utils import atomic_write
 from ..utils import expand_path
 from ..utils import get_storage_init_args
 from . import cli_logger
@@ -232,7 +232,8 @@
         prepare_status_path(path)
         status = SqliteStatus(path)
 
-    yield status
+    with contextlib.closing(status):
+        yield status
 
 
 def save_status(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/http.py 
new/vdirsyncer-0.20.0/vdirsyncer/http.py
--- old/vdirsyncer-0.19.3/vdirsyncer/http.py    2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/http.py    2025-08-28 22:57:38.000000000 
+0200
@@ -1,8 +1,11 @@
 from __future__ import annotations
 
 import logging
+import os
+import platform
 import re
-from abc import ABC, abstractmethod
+from abc import ABC
+from abc import abstractmethod
 from base64 import b64encode
 from ssl import create_default_context
 
@@ -17,6 +20,13 @@
 logger = logging.getLogger(__name__)
 USERAGENT = f"vdirsyncer/{__version__}"
 
+# 'hack' to prevent aiohttp from loading the netrc config,
+# but still allow it to read PROXY_* env vars.
+# Otherwise, if our host is defined in the netrc config,
+# aiohttp will overwrite our Authorization header.
+# https://github.com/pimutils/vdirsyncer/issues/1138
+os.environ["NETRC"] = "NUL" if platform.system() == "Windows" else "/dev/null"
+
 
 class AuthMethod(ABC):
     def __init__(self, username, password):
@@ -34,7 +44,11 @@
     def __eq__(self, other):
         if not isinstance(other, AuthMethod):
             return False
-        return self.__class__ == other.__class__ and self.username == 
other.username and self.password == other.password
+        return (
+            self.__class__ == other.__class__
+            and self.username == other.username
+            and self.password == other.password
+        )
 
 
 class BasicAuthMethod(AuthMethod):
@@ -43,20 +57,19 @@
 
     def get_auth_header(self, _method, _url):
         auth_str = f"{self.username}:{self.password}"
-        return "Basic " + b64encode(auth_str.encode('utf-8')).decode("utf-8")
+        return "Basic " + b64encode(auth_str.encode("utf-8")).decode("utf-8")
 
 
 class DigestAuthMethod(AuthMethod):
     # make class var to 'cache' the state, which is more efficient because 
otherwise
     # each request would first require another 'initialization' request.
-    _auth_helpers = {}
+    _auth_helpers: dict[tuple[str, str], requests.auth.HTTPDigestAuth] = {}
 
-    def __init__(self, username, password):
+    def __init__(self, username: str, password: str):
         super().__init__(username, password)
 
         self._auth_helper = self._auth_helpers.get(
-            (username, password),
-            requests.auth.HTTPDigestAuth(username, password)
+            (username, password), requests.auth.HTTPDigestAuth(username, 
password)
         )
         self._auth_helpers[(username, password)] = self._auth_helper
 
@@ -78,7 +91,7 @@
 
         if not self.auth_helper_vars.chal:
             # Need to do init request first
-            return ''
+            return ""
 
         return self._auth_helper.build_digest_header(method, url)
 
@@ -90,10 +103,12 @@
         elif auth == "digest":
             return DigestAuthMethod(username, password)
         elif auth == "guess":
-            raise exceptions.UserError(f"'Guess' authentication is not 
supported in this version of vdirsyncer. \n"
-                                       f"Please explicitly specify either 
'basic' or 'digest' auth instead. \n"
-                                       f"See the following issue for more 
information: "
-                                       
f"https://github.com/pimutils/vdirsyncer/issues/1015";)
+            raise exceptions.UserError(
+                "'Guess' authentication is not supported in this version of 
vdirsyncer. \n"
+                "Please explicitly specify either 'basic' or 'digest' auth 
instead. \n"
+                "See the following issue for more information: "
+                "https://github.com/pimutils/vdirsyncer/issues/1015";
+            )
         else:
             raise exceptions.UserError(f"Unknown authentication method: 
{auth}")
     elif auth:
@@ -208,6 +223,10 @@
     logger.debug(response.headers)
     logger.debug(response.content)
 
+    if logger.getEffectiveLevel() <= logging.DEBUG and response.status >= 400:
+        # https://github.com/pimutils/vdirsyncer/issues/1186
+        logger.debug(await response.text())
+
     if response.status == 412:
         raise exceptions.PreconditionFailed(response.reason)
     if response.status in (404, 410):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/repair.py 
new/vdirsyncer-0.20.0/vdirsyncer/repair.py
--- old/vdirsyncer-0.19.3/vdirsyncer/repair.py  2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/repair.py  2025-08-28 22:57:38.000000000 
+0200
@@ -56,9 +56,7 @@
         new_item = item.with_uid(generate_href())
     elif not href_safe(item.uid) or not href_safe(basename(href)):
         if not repair_unsafe_uid:
-            logger.warning(
-                "UID may cause problems, add " "--repair-unsafe-uid to repair."
-            )
+            logger.warning("UID may cause problems, add --repair-unsafe-uid to 
repair.")
         else:
             logger.warning("UID or href is unsafe, assigning random UID.")
             new_item = item.with_uid(generate_href())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/storage/dav.py 
new/vdirsyncer-0.20.0/vdirsyncer/storage/dav.py
--- old/vdirsyncer-0.19.3/vdirsyncer/storage/dav.py     2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/storage/dav.py     2025-08-28 
22:57:38.000000000 +0200
@@ -261,7 +261,7 @@
 
             href = response.find("{DAV:}href")
             if href is None:
-                raise InvalidXMLResponse("Missing href tag for collection " 
"props.")
+                raise InvalidXMLResponse("Missing href tag for collection 
props.")
             href = urlparse.urljoin(str(r.url), href.text)
             if href not in done:
                 done.add(href)
@@ -310,9 +310,7 @@
             </mkcol>
         """.format(
             etree.tostring(etree.Element(self._resourcetype), 
encoding="unicode")
-        ).encode(
-            "utf-8"
-        )
+        ).encode("utf-8")
 
         response = await self.session.request(
             "MKCOL",
@@ -740,9 +738,7 @@
         """.format(
             etree.tostring(element, encoding="unicode"),
             action=action,
-        ).encode(
-            "utf-8"
-        )
+        ).encode("utf-8")
 
         await self.session.request(
             "PROPPATCH",
@@ -796,7 +792,7 @@
         self.item_types = tuple(item_types)
         if (start_date is None) != (end_date is None):
             raise exceptions.UserError(
-                "If start_date is given, " "end_date has to be given too."
+                "If start_date is given, end_date has to be given too."
             )
         elif start_date is not None and end_date is not None:
             namespace = dict(datetime.__dict__)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/storage/filesystem.py 
new/vdirsyncer-0.20.0/vdirsyncer/storage/filesystem.py
--- old/vdirsyncer-0.19.3/vdirsyncer/storage/filesystem.py      2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/storage/filesystem.py      2025-08-28 
22:57:38.000000000 +0200
@@ -5,9 +5,8 @@
 import os
 import subprocess
 
-from atomicwrites import atomic_write
-
 from .. import exceptions
+from ..utils import atomic_write
 from ..utils import checkdir
 from ..utils import expand_path
 from ..utils import generate_href
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/storage/google.py 
new/vdirsyncer-0.20.0/vdirsyncer/storage/google.py
--- old/vdirsyncer-0.19.3/vdirsyncer/storage/google.py  2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/storage/google.py  2025-08-28 
22:57:38.000000000 +0200
@@ -11,9 +11,9 @@
 
 import aiohttp
 import click
-from atomicwrites import atomic_write
 
 from .. import exceptions
+from ..utils import atomic_write
 from ..utils import checkdir
 from ..utils import expand_path
 from ..utils import open_graphical_browser
@@ -98,6 +98,7 @@
             token_updater=self._save_token,
             connector=self.connector,
             connector_owner=False,
+            trust_env=True,
         )
 
     async def _init_token(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/storage/http.py 
new/vdirsyncer-0.20.0/vdirsyncer/storage/http.py
--- old/vdirsyncer-0.19.3/vdirsyncer/storage/http.py    2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/storage/http.py    2025-08-28 
22:57:38.000000000 +0200
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import logging
+import subprocess
 import urllib.parse as urlparse
 
 import aiohttp
@@ -14,6 +16,8 @@
 from ..vobject import split_collection
 from .base import Storage
 
+logger = logging.getLogger(__name__)
+
 
 class HttpStorage(Storage):
     storage_name = "http"
@@ -34,6 +38,7 @@
         useragent=USERAGENT,
         verify_fingerprint=None,
         auth_cert=None,
+        filter_hook=None,
         *,
         connector,
         **kwargs,
@@ -56,6 +61,7 @@
         self.useragent = useragent
         assert connector is not None
         self.connector = connector
+        self._filter_hook = filter_hook
 
         collection = kwargs.get("collection")
         if collection is not None:
@@ -66,6 +72,19 @@
     def _default_headers(self):
         return {"User-Agent": self.useragent}
 
+    def _run_filter_hook(self, raw_item):
+        try:
+            result = subprocess.run(
+                [self._filter_hook],
+                input=raw_item,
+                capture_output=True,
+                encoding="utf-8",
+            )
+            return result.stdout
+        except OSError as e:
+            logger.warning(f"Error executing external command: {str(e)}")
+            return raw_item
+
     async def list(self):
         async with aiohttp.ClientSession(
             connector=self.connector,
@@ -82,8 +101,13 @@
             )
         self._items = {}
 
-        for item in split_collection((await r.read()).decode("utf-8")):
-            item = Item(item)
+        for raw_item in split_collection((await r.read()).decode("utf-8")):
+            if self._filter_hook:
+                raw_item = self._run_filter_hook(raw_item)
+            if not raw_item:
+                continue
+
+            item = Item(raw_item)
             if self._ignore_uids:
                 item = item.with_uid(item.hash)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/storage/singlefile.py 
new/vdirsyncer-0.20.0/vdirsyncer/storage/singlefile.py
--- old/vdirsyncer-0.19.3/vdirsyncer/storage/singlefile.py      2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/storage/singlefile.py      2025-08-28 
22:57:38.000000000 +0200
@@ -8,9 +8,8 @@
 import os
 from typing import Iterable
 
-from atomicwrites import atomic_write
-
 from .. import exceptions
+from ..utils import atomic_write
 from ..utils import checkfile
 from ..utils import expand_path
 from ..utils import get_etag_from_file
@@ -95,7 +94,7 @@
                 path = path % (collection,)
             except TypeError:
                 raise ValueError(
-                    "Exactly one %s required in path " "if collection is not 
null."
+                    "Exactly one %s required in path if collection is not 
null."
                 )
 
         checkfile(path, create=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/sync/status.py 
new/vdirsyncer-0.20.0/vdirsyncer/sync/status.py
--- old/vdirsyncer-0.19.3/vdirsyncer/sync/status.py     2024-09-11 
17:26:58.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/sync/status.py     2025-08-28 
22:57:38.000000000 +0200
@@ -169,6 +169,11 @@
             ); """
             )
 
+    def close(self):
+        if self._c:
+            self._c.close()
+            self._c = None
+
     def _is_latest_version(self):
         try:
             return bool(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/utils.py 
new/vdirsyncer-0.20.0/vdirsyncer/utils.py
--- old/vdirsyncer-0.19.3/vdirsyncer/utils.py   2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/utils.py   2025-08-28 22:57:38.000000000 
+0200
@@ -1,8 +1,10 @@
 from __future__ import annotations
 
+import contextlib
 import functools
 import os
 import sys
+import tempfile
 import uuid
 from inspect import getfullargspec
 from typing import Callable
@@ -125,12 +127,13 @@
             raise exceptions.CollectionNotFound(f"Directory {path} does not 
exist.")
 
 
-def checkfile(path, create=False):
-    """
-    Check whether ``path`` is a file.
+def checkfile(path, create=False) -> None:
+    """Check whether ``path`` is a file.
 
     :param create: Whether to create the file's parent directories if they do
         not exist.
+    :raises CollectionNotFound: if path does not exist.
+    :raises OSError: if path exists but is not a file.
     """
     checkdir(os.path.dirname(path), create=create)
     if not os.path.isfile(path):
@@ -208,7 +211,7 @@
 
     cli_names = {"www-browser", "links", "links2", "elinks", "lynx", "w3m"}
 
-    if webbrowser._tryorder is None:  # Python 3.7
+    if webbrowser._tryorder is None:  # Python 3.8
         webbrowser.register_standard_browsers()
 
     for name in webbrowser._tryorder:
@@ -219,4 +222,28 @@
         if browser.open(url, new, autoraise):
             return
 
-    raise RuntimeError("No graphical browser found. Please open the URL " 
"manually.")
+    raise RuntimeError("No graphical browser found. Please open the URL 
manually.")
+
+
[email protected]
+def atomic_write(dest, mode="wb", overwrite=False):
+    if "w" not in mode:
+        raise RuntimeError("`atomic_write` requires write access")
+
+    fd, src = tempfile.mkstemp(prefix=os.path.basename(dest), 
dir=os.path.dirname(dest))
+    file = os.fdopen(fd, mode=mode)
+
+    try:
+        yield file
+    except Exception:
+        os.unlink(src)
+        raise
+    else:
+        file.flush()
+        file.close()
+
+        if overwrite:
+            os.rename(src, dest)
+        else:
+            os.link(src, dest)
+            os.unlink(src)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/version.py 
new/vdirsyncer-0.20.0/vdirsyncer/version.py
--- old/vdirsyncer-0.19.3/vdirsyncer/version.py 2024-09-11 17:27:00.000000000 
+0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/version.py 2025-08-28 22:57:40.000000000 
+0200
@@ -1,16 +1,34 @@
-# file generated by setuptools_scm
+# file generated by setuptools-scm
 # don't change, don't track in version control
+
+__all__ = [
+    "__version__",
+    "__version_tuple__",
+    "version",
+    "version_tuple",
+    "__commit_id__",
+    "commit_id",
+]
+
 TYPE_CHECKING = False
 if TYPE_CHECKING:
-    from typing import Tuple, Union
+    from typing import Tuple
+    from typing import Union
+
     VERSION_TUPLE = Tuple[Union[int, str], ...]
+    COMMIT_ID = Union[str, None]
 else:
     VERSION_TUPLE = object
+    COMMIT_ID = object
 
 version: str
 __version__: str
 __version_tuple__: VERSION_TUPLE
 version_tuple: VERSION_TUPLE
+commit_id: COMMIT_ID
+__commit_id__: COMMIT_ID
+
+__version__ = version = '0.20.0'
+__version_tuple__ = version_tuple = (0, 20, 0)
 
-__version__ = version = '0.19.3'
-__version_tuple__ = version_tuple = (0, 19, 3)
+__commit_id__ = commit_id = 'g8803d5a08'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer/vobject.py 
new/vdirsyncer-0.20.0/vdirsyncer/vobject.py
--- old/vdirsyncer-0.19.3/vdirsyncer/vobject.py 2024-09-11 17:26:58.000000000 
+0200
+++ new/vdirsyncer-0.20.0/vdirsyncer/vobject.py 2025-08-28 22:57:38.000000000 
+0200
@@ -329,7 +329,7 @@
                 break
 
             for line in lineiter:
-                if not line.startswith((" ", "\t")):
+                if not line.startswith((" ", "\t", *prefix)):
                     new_lines.append(line)
                     break
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer.egg-info/PKG-INFO 
new/vdirsyncer-0.20.0/vdirsyncer.egg-info/PKG-INFO
--- old/vdirsyncer-0.19.3/vdirsyncer.egg-info/PKG-INFO  2024-09-11 
17:27:00.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer.egg-info/PKG-INFO  2025-08-28 
22:57:40.000000000 +0200
@@ -1,33 +1,42 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: vdirsyncer
-Version: 0.19.3
+Version: 0.20.0
 Summary: Synchronize calendars and contacts
-Home-page: https://github.com/pimutils/vdirsyncer
-Author: Markus Unterwaditzer
-Author-email: [email protected]
-License: BSD
+Author-email: Markus Unterwaditzer <[email protected]>
+License-Expression: BSD-3-Clause
+Keywords: todo,task,icalendar,cli
 Classifier: Development Status :: 4 - Beta
 Classifier: Environment :: Console
-Classifier: License :: OSI Approved :: BSD License
 Classifier: Operating System :: POSIX
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Internet
+Classifier: Topic :: Office/Business :: Scheduling
 Classifier: Topic :: Utilities
+Requires-Python: >=3.8
+Description-Content-Type: text/x-rst
 License-File: LICENSE
-License-File: AUTHORS.rst
 Requires-Dist: click<9.0,>=5.0
 Requires-Dist: click-log<0.5.0,>=0.3.0
 Requires-Dist: requests>=2.20.0
-Requires-Dist: atomicwrites>=0.1.7
-Requires-Dist: aiohttp<4.0.0,>=3.8.0
-Requires-Dist: aiostream<0.5.0,>=0.4.3
+Requires-Dist: aiohttp<4.0.0,>=3.8.2
+Requires-Dist: aiostream<0.8.0,>=0.4.3
 Provides-Extra: google
 Requires-Dist: aiohttp-oauthlib; extra == "google"
+Provides-Extra: test
+Requires-Dist: hypothesis<7.0.0,>=6.72.0; extra == "test"
+Requires-Dist: pytest; extra == "test"
+Requires-Dist: pytest-cov; extra == "test"
+Requires-Dist: pytest-httpserver; extra == "test"
+Requires-Dist: trustme; extra == "test"
+Requires-Dist: pytest-asyncio; extra == "test"
+Requires-Dist: aioresponses; extra == "test"
+Dynamic: license-file
 
 ==========
 vdirsyncer
@@ -71,7 +80,7 @@
 between two servers directly.
 
 It aims to be for calendars and contacts what `OfflineIMAP
-<http://offlineimap.org/>`_ is for emails.
+<https://www.offlineimap.org/>`_ is for emails.
 
 .. _programs: https://vdirsyncer.pimutils.org/en/latest/tutorials/
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer.egg-info/SOURCES.txt 
new/vdirsyncer-0.20.0/vdirsyncer.egg-info/SOURCES.txt
--- old/vdirsyncer-0.19.3/vdirsyncer.egg-info/SOURCES.txt       2024-09-11 
17:27:00.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer.egg-info/SOURCES.txt       2025-08-28 
22:57:40.000000000 +0200
@@ -1,8 +1,9 @@
 .codecov.yml
 .coveragerc
+.envrc
 .gitignore
-.gitlab-ci.yml
 .pre-commit-config.yaml
+.readthedocs.yaml
 AUTHORS.rst
 CHANGELOG.rst
 CODE_OF_CONDUCT.rst
@@ -16,9 +17,7 @@
 docs-requirements.txt
 publish-release.yaml
 pyproject.toml
-setup.py
-test-requirements.txt
-.builds/tests-archlinux.yml
+.builds/archlinux-py313.yml
 .builds/tests-minimal.yml
 .builds/tests-pypi.yml
 contrib/vdirsyncer.service
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vdirsyncer-0.19.3/vdirsyncer.egg-info/requires.txt 
new/vdirsyncer-0.20.0/vdirsyncer.egg-info/requires.txt
--- old/vdirsyncer-0.19.3/vdirsyncer.egg-info/requires.txt      2024-09-11 
17:27:00.000000000 +0200
+++ new/vdirsyncer-0.20.0/vdirsyncer.egg-info/requires.txt      2025-08-28 
22:57:40.000000000 +0200
@@ -1,9 +1,17 @@
 click<9.0,>=5.0
 click-log<0.5.0,>=0.3.0
 requests>=2.20.0
-atomicwrites>=0.1.7
-aiohttp<4.0.0,>=3.8.0
-aiostream<0.5.0,>=0.4.3
+aiohttp<4.0.0,>=3.8.2
+aiostream<0.8.0,>=0.4.3
 
 [google]
 aiohttp-oauthlib
+
+[test]
+hypothesis<7.0.0,>=6.72.0
+pytest
+pytest-cov
+pytest-httpserver
+trustme
+pytest-asyncio
+aioresponses

Reply via email to