Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b30c767
Preparing for releasing v2.2.5
tobixen Jan 31, 2026
59f783e
Oups, I forgot to add the new version number in the CHANGELOG
tobixen Feb 1, 2026
1a8e16e
Update CI, tooling, and project configuration
tobixen Feb 13, 2026
4b949b0
Add Sans-I/O protocol and operations layers
tobixen Feb 13, 2026
0078b99
Add full async support with client refactoring
tobixen Feb 13, 2026
1acbc8b
Refactor domain objects for dual-mode async/sync and API consistency
tobixen Feb 13, 2026
6fb2c27
Overhaul configuration system and compatibility hints
tobixen Feb 13, 2026
89ce797
Overhaul test infrastructure with Docker server framework
tobixen Feb 13, 2026
699ee0a
Add comprehensive tests for new architecture
tobixen Feb 13, 2026
692cc37
Update documentation, examples, and changelog
tobixen Feb 13, 2026
1fc628a
Add search.text.substring unsupported hint for Zimbra
tobixen Feb 13, 2026
03fd701
Update Zimbra and Cyrus compatibility hints
tobixen Feb 13, 2026
4f8b895
Set Cyrus password to sentinel value (any-password-seems-to-work)
tobixen Feb 13, 2026
27b2e7e
Merge master into v3.0-dev to resolve PR conflicts
tobixen Feb 13, 2026
63c240e
Update documentation to reference caldav_test_servers.yaml
tobixen Feb 13, 2026
a00fa99
search.time-range.todo - earlier observation that it was unsupported …
tobixen Feb 13, 2026
f9425e5
Fix compat hints for posteo/purelymail, handle invalid recurrence data
tobixen Feb 13, 2026
da8d4cf
bugfix
tobixen Feb 13, 2026
3d8c107
Improved AI-policy
tobixen Feb 14, 2026
4b6db95
Consolidate _fixCalendar_ into get_or_create_test_calendar
tobixen Feb 14, 2026
4433264
aardvark.co.nz should not be ignored in the lychee link tests, even i…
tobixen Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/linkcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
with:
fail: true
args: >-
--timeout 10
--max-retries 2
--timeout 20
--max-retries 3
'**/*.md'
'**/*.rst'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ tests/docker-test-servers/baikal/baikal-backup/
tests/docker-test-servers/*/baikal-backup/
# But keep the pre-configured Specific directory for Baikal
!tests/docker-test-servers/baikal/Specific/
# Local test server configuration (may contain credentials)
tests/caldav_test_servers.yaml
61 changes: 46 additions & 15 deletions AI-POLICY.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
# Policy on usage of Artifical Intelligence and other tools

## Read this first

The most important rule: Inform about it!

If you've spent hours, perhaps a full day of your time writing up a
pull request, then I sort of owe you something. I should spend some
of my time looking through the submission carefully, and if nothing
else, I owe to be polite, respectful and guide you in the right
direction or give a good explanation for why I think your pull request
is pulling the project in the wrong direction. A human being have
feelings, I should be careful not to hurt your feelings.

At the other hand, perhaps you've spent 30 seconds either doing `ruff
check --fix ; gh pr create` or telling Claude to check what went wrong
in the logs and submit a bugfix upstream. Do I still owe
you to spend time looking through the submission carefully and
spending time being polite and caring about your feelings?

Perhaps your pull request is just one out of many such "drive-by pull
requests". It doesn't scale for a maintainer to spent lots of time on
each such pull request. I should just accept or decline such requests
rapidly with minimum effort.

So it all boils down to this: Be honest about tool usage!

## Background

From time to time I do get pull requests where the author has done
little else than running some tool on the code and submitting it as a
pull request. Those pull requests may have value to the project, but
it's dishonest to not be transparent about it; teaching me how to run
the tool and integrating it into the CI workflow may have a bigger
value than the changes provided by the tool. Recently I've also
started receiving pull requests with code changes generated by AI (and
I've seen people posting screenshots of simple questions and answers
from ChatGPT in forum discussions, without contributing anything else).
The "30 second effort pull request" mentioned above may have value to
the project, but it's dishonest to not be transparent about it.
Sometimes, teaching me how to run the tool and integrating it into the
CI workflow may have a bigger value than the changes provided by the
tool.

Starting in 2025-11, I've spent quite some time testing Claude. I'm
positively surprised, it's doing a much better job than what I had
expected. The AI may do things a lot faster, smarter and better than
a good coder. Sometimes. Other times it may spend a lot of "tokens"
and a long time coming up with sub-optimal solutions, or even
solutions that doesn't work at all. Perhaps at some time in the near
future the AI will do the developer profession completely obsoleted -
but as of 2026-02, my experiences is that the AI performs best when
being "supervised" and "guided" by a good coder knowing the project.
and a long time coming up with sub-optimal or really bad solutions.

Perhaps at some time in the near future the AI will do the developer
profession completely obsoleted - but as of 2026-02, my experiences is
that the AI performs best when being "supervised" and "guided" by a
good coder knowing the project.

## Bugfixes are (most often) welcome

Over the past month, playing with a "max" subscription with Claude,
I've made it into a rule that when I stumble upon some weird bug in
some software or libraries I'm using or dependent on, I always ask
Claude to analyze the bug, check the outstanding issues in the
project, either create a new issue or consider if there is anything of
value to add to an existing issue, and come up with a pull-request. Being a bit aware of the

## The rules
## General rules

* Do **respect the maintainers time**. If/when the maintainer gets
overwhelmed by pull requests of questionable quality or pull
Expand Down
114 changes: 95 additions & 19 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0

Version 3.0 should be fully backward-compatible with version 2.x - but there are massive code changes in version 3.0:

* "Black style" has been replaced with ruff. This causes quite some changes in the code.
* Version 3.0 introduces **full async support** using a Sans-I/O architecture. The same domain objects (Calendar, Event, Todo, etc.) now work with both synchronous and asynchronous clients. The async client uses niquests by default; httpx is also supported for projects that already have it as a dependency.
* Quite some refactoring work has been done
* Some work has been put down ensuring better consistency in the method names. Version 3.0 should be backward-compatible with version 2.0, so the old methods still work, but are deprecated.
* **Full async support** using a Sans-I/O architecture. The same domain objects (Calendar, Event, Todo, etc.) now work with both synchronous and asynchronous clients. The async client uses niquests by default; httpx is also supported for projects that already have it as a dependency.
* **Sans-I/O architecture** -- internal refactoring separates protocol logic (XML building/parsing) from I/O into a layered architecture: protocol layer (`caldav/protocol/`), operations layer (`caldav/operations/`), and response handling (`caldav/response.py`). This enables code reuse between sync and async implementations and improves testability.
* **Lazy imports** -- `import caldav` is now significantly faster due to PEP 562 lazy loading. Heavy dependencies (lxml, niquests, icalendar) are deferred until first use. (PR #621)
* **API naming consistency** -- methods have been renamed for consistency. Server-fetching methods use `get_` prefix, capability checks use `supports_*()`. Old method names still work but are deprecated.
* **Ruff replaces Black** -- code formatting now uses ruff instead of Black, causing cosmetic changes throughout the codebase.
* **Expanded compatibility hints** -- server-specific workarounds added for Zimbra, Bedework, CCS (Apple CalendarServer), Davis, DAViCal, GMX, ecloud, Synology, Posteo, PurelyMail, and more.
* Quite some other refactoring work has been done.

### Breaking Changes

(Be aware that some of the 2.x minor-versions also tagged some "Potentially Breaking Changes")

* **Minimum Python version**: Python 3.10+ is now required (was 3.8+).
* **Test Server Configuration**: `tests/conf.py` has been removed and `conf_private.py` will be ignored. See the Test Framework section below.
* **Config file parse errors now raise exceptions**: `caldav.config.read_config()` now raises `ValueError` on YAML/JSON parse errors instead of logging and returning an empty dict. This ensures config errors are detected early.

### Deprecated

Expand Down Expand Up @@ -69,7 +73,7 @@ Additionally, direct `DAVClient()` instantiation should migrate to `get_davclien

### Added

* **Full async API** - New `AsyncDAVClient` and async-compatible domain objects:
* **Full async API** -- New `AsyncDAVClient` and async-compatible domain objects:
```python
from caldav.async_davclient import get_davclient

Expand All @@ -79,45 +83,99 @@ Additionally, direct `DAVClient()` instantiation should migrate to `get_davclien
for cal in calendars:
events = await cal.get_events()
```
* **Sans-I/O architecture** - Internal refactoring separates protocol logic from I/O:
- Protocol layer (`caldav/protocol/`): Pure functions for XML building/parsing
- Operations layer (`caldav/operations/`): High-level CalDAV operations
- This enables code reuse between sync and async implementations
* **Sans-I/O architecture** -- Internal refactoring separates protocol logic from I/O:
- Protocol layer (`caldav/protocol/`): Pure functions for XML building/parsing with typed dataclasses (DAVRequest, DAVResponse, PropfindResult, CalendarQueryResult)
- Operations layer (`caldav/operations/`): Sans-I/O business logic for CalDAV operations (properties, search, calendar management, principal discovery)
- Response layer (`caldav/response.py`): Shared `BaseDAVResponse` for sync/async
- Data state (`caldav/datastate.py`): Strategy pattern for managing data representations (raw string, icalendar, vobject) -- avoids unnecessary parse/serialize cycles
* **Lazy imports (PEP 562)** -- `import caldav` is now fast. Heavy dependencies (lxml, niquests, icalendar) are deferred until first use. https://github.com/python-caldav/caldav/pull/621
* **`DAVObject.name` deprecated** -- use `get_display_name()` instead. The old `.name` property now emits `DeprecationWarning`.
* Added python-dateutil and PyYAML as explicit dependencies (were transitive)
* Quite some methods have been renamed for consistency and to follow best current practices. See the deprecation section.
* `Calendar` class now accepts a `name` parameter in its constructor, addressing a long-standing API inconsistency (https://github.com/python-caldav/caldav/issues/128)
* **Data representation API** - New efficient data access via `CalendarObjectResource` properties (https://github.com/python-caldav/caldav/issues/613):
- `.icalendar_instance` - parsed icalendar object (lazy loaded)
- `.vobject_instance` - parsed vobject object (lazy loaded)
- `.data` - raw iCalendar string
* **Data representation API** -- New efficient data access via `CalendarObjectResource` properties (https://github.com/python-caldav/caldav/issues/613):
- `.icalendar_instance` -- parsed icalendar object (lazy loaded)
- `.vobject_instance` -- parsed vobject object (lazy loaded)
- `.data` -- raw iCalendar string
- Context managers `edit_icalendar_instance()` and `edit_vobject_instance()` for safe mutable access
- `get_data()`, `get_icalendar_instance()`, `get_vobject_instance()` return copies for read-only access
- Internal `DataState` class manages caching between formats
* **CalendarObjectResource.id property** - Returns the UID of calendar objects (https://github.com/python-caldav/caldav/issues/515)
* **calendar.searcher() API** - Factory method for advanced search queries (https://github.com/python-caldav/caldav/issues/590):
* **CalendarObjectResource.id property** -- Returns the UID of calendar objects (https://github.com/python-caldav/caldav/issues/515)
* **calendar.searcher() API** -- Factory method for advanced search queries (https://github.com/python-caldav/caldav/issues/590):
```python
searcher = calendar.searcher()
searcher.add_filter(...)
results = searcher.search()
```
* **`get_calendars()` and `get_calendar()` context managers** -- Module-level factory functions that create a client, fetch calendars, and clean up on exit:
```python
with get_calendars(url="...", username="...", password="...") as calendars:
for cal in calendars:
...
```
* **Base+override feature profiles** -- YAML config now supports inheriting from base feature profiles:
```yaml
my-server:
features:
base: nextcloud
search.comp-type: unsupported
```
* **Feature validation** -- `caldav.config` now validates feature configuration and raises errors on unknown feature names
* **URL space validation** -- `caldav.lib.url` now validates that URLs don't contain unquoted spaces
* **Fallback for missing calendar-home-set** -- Client falls back to principal URL when `calendar-home-set` property is not available
* **Load fallback for changed URLs** -- `CalendarObjectResource.load()` falls back to UID-based lookup when servers change URLs after save

### Fixed

* RFC 4791 compliance: Don't send Depth header for calendar-multiget REPORT (clients SHOULD NOT send it, but servers MUST ignore it per §7.9)
* Fixed `ssl_verify_cert` not passed through in `get_sync_client` and `get_async_client`
* Fixed `_derive_from_subfeatures` partial-config derivation bug
* Fixed feature name parsing when names include `compatibility_hints.` prefix
* Fixed recursive `_search_with_comptypes` when `search.comp-type` is broken
* Fixed pending todo search on servers with broken comp-type filtering
* Fixed URL path quoting when extracting calendars from PROPFIND results
* Removed spurious warning on URL path mismatch, deduplicated `get_properties`
* Fixed `create-calendar` feature incorrectly derived as unsupported
* Fixed various async test issues (awaiting sync calls, missing feature checks, authorization error handling)
* Fixed `search.category` features to use correct `search.text.category` names

### Changed

* Sync client (`DAVClient`) now shares common code with async client via `BaseDAVClient`
* Response handling unified in `BaseDAVResponse` class
* Search refactored to use generator-based Sans-I/O pattern -- `_search_impl` yields `(SearchAction, data)` tuples consumed by sync or async wrappers
* Test configuration migrated from legacy `tests/conf.py` to new `tests/test_servers/` framework
* Configuration system expanded: `get_connection_params()` provides unified config discovery with clear priority (explicit params > test server config > env vars > config file)
* `${VAR}` and `${VAR:-default}` environment variable expansion in config values
* Ruff replaces Black for code formatting
* `caldav/objects.py` backward-compatibility shim removed (imports go directly to submodules)

### Test Framework

* Fixed Nextcloud Docker test server tmpfs permissions race condition
* Added deptry for dependency verification in CI
* The test server framework has been refactored with a new `tests/test_servers/` module. It provides **YAML-based server configuration**: see `tests/test_servers/__init__.py` for usage
* **New `tests/test_servers/` module** -- Complete rewrite of test infrastructure:
- `TestServer` base class hierarchy (EmbeddedTestServer, DockerTestServer, ExternalTestServer)
- YAML-based server configuration (`tests/caldav_test_servers.yaml.example`)
- `ServerRegistry` for server discovery and lifecycle management
- `client_context()` and `has_test_servers()` helpers
* **New Docker test servers**: CCS (Apple CalendarServer), DAViCal, Davis, Zimbra
* **Updated Docker configs**: Baikal, Cyrus, Nextcloud, SOGo
* Added pytest-asyncio for async test support
* Added deptry for dependency verification in CI
* Added lychee link-check workflow
* Added `convert_conf_private.py` migration tool for old config format
* Removed `tests/conf.py`, `tests/conf_baikal.py`, `tests/conf_private.py.EXAMPLE`
* **New test suites**:
- `test_async_davclient.py` (821 lines) -- Async client unit tests
- `test_async_integration.py` (466 lines) -- Async integration tests
- `test_operations_*.py` (6 files) -- Operations layer unit tests
- `test_protocol.py` (319 lines) -- Protocol layer unit tests
- `test_lazy_import.py` (141 lines) -- PEP 562 lazy import verification
* Fixed Nextcloud Docker test server tmpfs permissions race condition

### GitHub Pull Requests Merged

* #621 - Lazy-load heavy dependencies to speed up import caldav
* #622 - Fix overlong inline literal, replace hyphens with en-dashes
* #607 - Add deptry for dependency verification

### GitHub Issues Closed
Expand All @@ -130,7 +188,25 @@ Additionally, direct `DAVClient()` instantiation should migrate to `get_davclien

### Security

Nothing to report.
* UUID1 usage in UID generation (`calendarobject_ops.py`) may embed the host MAC address in calendar UIDs. Since calendar events are shared with third parties, this is a privacy concern. Planned fix: switch to UUID4.

### Compatibility Hints Expanded

Server-specific workarounds have been significantly expanded. Profiles added or updated for:

* **Zimbra** -- search.is-not-defined, delete-calendar, recurrences.count, case-sensitive search
* **Bedework** -- save-load.journal, save-load.todo.recurrences.thisandfuture, search.recurrences.expanded.todo, search.time-range.alarm
* **CCS (Apple CalendarServer)** -- save-load.journal unsupported, various search hints
* **Davis** -- principal-search at parent level, mixed-calendar features
* **GMX** -- rate-limit, basepath correction
* **ecloud** -- create-calendar unsupported, search.is-not-defined, case-sensitive
* **Synology** -- is-not-defined, wipe-calendar cleanup
* **SOGo** -- save-load.journal ungraceful, case-insensitive, delete-calendar
* **Posteo** -- search.combined-is-logical-and unsupported
* **PurelyMail** -- search.time-range.todo ungraceful
* **DAViCal** -- various search and sync hints
* **Xandikos** -- freebusy-query now supported in v0.3.3
* **Baikal/Radicale** -- case-sensitive search, principal-search features

## [2.2.6] - [2026-02-01]

Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ include COPYING.*
include *.md
recursive-include tests caldav
exclude tests/conf_private.py
exclude tests/caldav_test_servers.yaml
exclude tests/tmp_caldav_test_servers.yaml
2 changes: 1 addition & 1 deletion RELEASE-HOWTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ This is most likely not complete, but should explain some of the "silly" steps a
* Forgetting to add new files to the git repo
* Having checked out a branch or tag or something, and tagging that as the new release rather than the latest HEAD.
* Forgetting to push to pypi, or pushing something else than the tagged revision to pypi
* Pushing out junk files in the pypi-release (i.e. .pyc-files, log files, temp files, `tests/conf_private.py`, etc
* Pushing out junk files in the pypi-release (i.e. .pyc-files, log files, temp files, `tests/conf_private.py`, `tests/caldav_test_servers.yaml`, etc
* Not adding the release to the "github releases" (I don't care much about this feature, but apparently some people check there to find the latest release version)
Loading
Loading