Skip to content

Conversation

@tobixen
Copy link
Member

@tobixen tobixen commented Jan 18, 2026

I've been "vibing" with Claude Code in a "playground" branch for a long time now, but the end result starts looking like something it's possible to continue with.

I still have a TODO-list before I can make a release candidate for 3.0, so this is still a draft PR.

tobixen and others added 26 commits January 22, 2026 20:04
Add get_davclient to caldav/__init__.py exports so users can do:
  from caldav import get_davclient

Ref: #612

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive design documentation for the Sans-I/O architecture:
- SANS_IO_IMPLEMENTATION_PLAN.md: Overall implementation strategy
- SYNC_ASYNC_OVERVIEW.md: How sync/async code sharing works
- PROTOCOL_LAYER_USAGE.md: Guide to using the protocol layer
- CODE_REVIEW.md: Architecture review and decisions

Also add AI-POLICY.md documenting AI assistant usage guidelines.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement the foundation of the Sans-I/O architecture with a protocol
layer that separates HTTP I/O from CalDAV/WebDAV logic:

Protocol layer (caldav/protocol/):
- types.py: Data classes for requests/responses (PropfindRequest, etc.)
- xml_builders.py: Pure functions to build XML request bodies
- xml_parsers.py: Pure functions to parse XML responses

Response handling (caldav/response.py):
- BaseDAVResponse: Common response interface for sync/async
- Parsed results accessible via response.results property

Tests (tests/test_protocol.py):
- Comprehensive unit tests for XML building and parsing
- Tests for various CalDAV operations (PROPFIND, REPORT, etc.)

This layer has no I/O dependencies and can be used with any HTTP client.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement high-level CalDAV operations that build on the protocol layer:

Operations (caldav/operations/):
- base.py: Base operation classes and utilities
- davobject.py: Generic DAV object operations (get_properties, etc.)
- calendarobject.py: Calendar object operations (save, load, delete)
- calendarset.py: Calendar set operations (calendars, make_calendar)
- principal.py: Principal operations (calendar_home_set, etc.)
- calendar.py: Calendar operations (search, events, todos)

Each operation:
- Uses protocol layer for XML building/parsing
- Returns typed request/response data classes
- Has no I/O - caller provides HTTP transport

Tests (tests/test_operations_*.py):
- Unit tests with mocked responses for each operation type
- Tests for error handling and edge cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactor test infrastructure with a unified server abstraction:

Test server framework (tests/test_servers/):
- base.py: Abstract TestServer base class with common interface
- embedded.py: In-process servers (Radicale, Xandikos)
- docker.py: Docker-based servers (Baikal, Nextcloud, etc.)
- config_loader.py: Load server configs from YAML/environment
- registry.py: Server discovery and registration

Shared test utilities (tests/fixture_helpers.py):
- Common fixtures for calendar creation/cleanup
- Helpers that work with both sync and async tests

Docker test server improvements:
- Fixed Nextcloud tmpfs permissions race condition
- Fixed Baikal ephemeral storage configuration
- Fixed SOGo and Cyrus credential configuration
- Added DAViCal server configuration

Updated tests/conf.py:
- Integrate with new test server framework
- Support both legacy and new configuration methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement full async support using httpx (with niquests fallback):

Async client (caldav/async_davclient.py):
- AsyncDAVClient: Full async HTTP client with connection pooling
- Support for HTTP/2 when h2 package is available
- Async context manager for proper resource cleanup
- Auth negotiation (Basic, Digest, Bearer)

Public API (caldav/aio.py):
- AsyncPrincipal, AsyncCalendar, AsyncEvent, AsyncTodo, etc.
- Factory methods: AsyncPrincipal.create(), etc.
- Async-compatible get_davclient() function

Auth utilities (caldav/lib/auth.py):
- Shared authentication logic for sync/async clients

Tests:
- test_async_davclient.py: Unit tests for async client
- test_async_integration.py: Integration tests against real servers

Documentation:
- docs/source/async.rst: Async usage guide
- examples/async_usage_examples.py: Example code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactor domain objects to work with both sync and async clients:

Client consolidation (caldav/base_client.py):
- BaseDAVClient: Shared logic for sync/async clients
- Unified get_davclient() implementation
- Common configuration handling

Domain object updates:
- caldav/davobject.py: Detect client type, delegate to async when needed
- caldav/collection.py: Calendar/CalendarSet with async support
- caldav/calendarobjectresource.py: Event/Todo/Journal async support

The same domain object classes work with both sync and async clients:
- With DAVClient: Methods return results directly
- With AsyncDAVClient: Methods return coroutines to await

Other updates:
- caldav/davclient.py: Use BaseDAVClient, simplified
- caldav/config.py: Support test server configuration
- caldav/search.py: Python 3.9 compatibility fixes
- caldav/__init__.py: Export async classes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
CI/Build improvements:
- .github/workflows/tests.yaml: Add async tests, fix Nextcloud password
- .github/workflows/linkcheck.yml: Add documentation link checker
- pyproject.toml: Add pytest-asyncio, httpx deps, warning filters
- tox.ini: Configure async test environments
- .pre-commit-config.yaml: Update hook versions

Test improvements:
- tests/test_caldav.py: Fix async/sync test isolation
- tests/test_examples.py: Use get_davclient() context manager
- Filter Radicale shutdown warnings in pytest config

Bug fixes:
- Don't send Depth header for calendar-multiget (RFC 4791 §7.9)
- Fix HTTP/2 when h2 package not installed
- Fix Python 3.9 compatibility in search.py

Documentation:
- README.md: Add async usage examples
- docs/source/index.rst: Link to async documentation
- CONTRIBUTING.md: Update development guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document all changes for the v3.0 release:
- Full async API with AsyncDAVClient
- Sans-I/O architecture (protocol and operations layers)
- Unified test server framework
- HTTP/2 support
- Various bug fixes and improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Review changes:
- Set minimum Python version to 3.10 (remove 3.9 from CI and classifiers)
- Make httpx optional (install with `pip install caldav[async]`)
- Add CI job to test sync client with requests fallback
- Add HTTP library documentation (docs/source/http-libraries.rst)
- Update changelog to reflect final niquests decision
- Add _USE_NIQUESTS/_USE_REQUESTS flags to davclient.py for testing

The sync API remains fully backward-compatible. Only niquests is a
required dependency; httpx is optional for async support.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Prefix all internal functions in the protocol and operations layers
with underscore to indicate they are private implementation details:

- caldav/protocol/xml_builders.py: _build_* functions
- caldav/protocol/xml_parsers.py: _parse_* functions
- caldav/operations/*.py: All utility functions now prefixed with _

The __init__.py files now only export data types (QuerySpec, CalendarInfo,
SearchStrategy, etc.) rather than implementation functions.

All call sites updated to import private functions directly from submodules
with local aliases for backward compatibility within the codebase.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Link to local file instead of ReadTheDocs URL since the page
doesn't exist on RTD yet (only in v3.0-dev branch).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
With Python 3.10 as the minimum version, we can simplify imports:
- Use collections.abc for Callable, Container, Iterable, Iterator, Sequence
- Use typing.DefaultDict and typing.Literal directly
- Remove redundant sys.version_info < (3, 9) checks
- Remove unused import sys from collection.py

Also update tests to use new expand parameter format:
- expand="client" → expand=True
- expand="server" → server_expand=True

The backward-compatible support for string values was removed as part
of the caldav 3.0 changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change all imports from `from caldav.davclient import get_davclient`
  to `from caldav import get_davclient`
- Update documentation references to use `caldav.get_davclient`
- Remove TestExpandRRule tests (deprecated methods will be removed in 4.0)
- Update deprecation message in tests/conf.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove 9 design documents specific to the abandoned async-first-with-sync-wrapper
approach:

- ASYNC_REFACTORING_PLAN.md (original async-first plan)
- PHASE_1_IMPLEMENTATION.md, PHASE_1_TESTING.md (old phases)
- PLAYGROUND_BRANCH_ANALYSIS.md, CODE_REVIEW.md (old branch analysis)
- SYNC_WRAPPER_DEMONSTRATION.md, SYNC_ASYNC_OVERVIEW.md (old approach)
- PERFORMANCE_ANALYSIS.md (event loop overhead analysis)
- SYNC_ASYNC_PATTERNS.md (general patterns survey)

Keep API analysis documents that contain design rationale still relevant
to current implementation:

- API_ANALYSIS.md (parameter naming, URL handling)
- URL_AND_METHOD_RESEARCH.md (URL semantics for methods)
- ELIMINATE_METHOD_WRAPPERS_ANALYSIS.md (keep wrappers decision)
- METHOD_GENERATION_ANALYSIS.md (manual implementation decision)

Update README.md to reflect current structure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pytest.mark.filterwarnings to tests that intentionally use the
deprecated date_search method for backward compatibility testing:
- testTodoDatesearch
- testDateSearchAndFreeBusy
- testRecurringDateSearch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add alias methods to DAVClient for API consistency with AsyncDAVClient:
- supports_dav() → check_dav_support()
- supports_caldav() → check_cdav_support()
- supports_scheduling() → check_scheduling_support()

This allows sync users to use the same cleaner API as async users.
Note: get_principal(), get_calendars(), get_events(), get_todos(), and
search_calendar() are already available in the sync client.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add API_NAMING_CONVENTIONS.md documenting:
- Recommended vs legacy method names
- Migration guide from date_search to search
- Deprecation timeline for 4.0

Update docstrings in DAVClient:
- Mark principal(), check_dav_support(), check_cdav_support(),
  check_scheduling_support() as legacy
- Add detailed docs for recommended methods: get_principal(),
  supports_dav(), supports_caldav(), supports_scheduling()

Update date_search docstring in collection.py:
- Add Sphinx deprecated directive
- Include migration example

Update tutorial.rst:
- Use get_principal() instead of principal() in all examples

Update docs/design/README.md:
- Add API_NAMING_CONVENTIONS.md to index
- Reorganize API Design section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The `name` parameter (for displayname) is only meaningful for Calendar
objects, not for all DAVObject subclasses like Principal, CalendarSet,
or CalendarObjectResource.

Changes:
- Remove `name` parameter from DAVObject.__init__()
- Add Calendar.__init__() that accepts `name` parameter
- Keep `name` as a class attribute on DAVObject (defaults to None)

This is a minor breaking change for anyone who was passing `name` to
non-Calendar DAVObject subclasses, but such usage was never meaningful.

Fixes: #128

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Run black/ruff formatting on affected files
- Reorder imports per pre-commit hooks
- Add h2 to DEP001 ignore (optional HTTP/2 dependency)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document the internal flow through the layered architecture for:
- Fetching calendars (sync and async)
- Creating events
- Searching for events
- Sync token synchronization
- Creating calendars

Explains the Protocol layer, Operations layer, and dual-mode
domain objects that enable both sync and async usage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents findings from the pre-release code review:
- Duplicated code between sync/async clients (~240 lines)
- Dead code (auto_calendars, auto_calendar, unused imports)
- Test coverage assessment by module
- Architecture strengths and weaknesses
- GitHub issues #71 and #613 analysis
- Recommendations for v3.0, v3.1, and v4.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add entries for:
- API consistency aliases (supports_dav, supports_caldav, supports_scheduling)
- Calendar class name parameter (issue #128)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract shared logic from get_calendars() and _get_calendar_home_set()
into pure helper functions in the operations layer:

- _extract_calendar_home_set_from_results() in principal_ops.py
- _extract_calendars_from_propfind_results() in calendarset_ops.py

Add shared constants to BaseDAVClient:
- CALENDAR_HOME_SET_PROPS
- CALENDAR_LIST_PROPS

This reduces code duplication by ~50 lines while keeping the I/O
separation between sync and async clients. The helper functions
are pure (no I/O) and can be unit tested independently.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tobixen and others added 14 commits January 23, 2026 12:13
Investigation findings:
- Radicale: 409 Conflict (RFC-compliant)
- Xandikos: 412 Precondition Failed (RFC-compliant)
- Baikal: Creates duplicates (violates RFC 5545)

Using pytest.xfail to document non-compliant server behavior without
breaking the test run.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This change makes add_event, add_todo, add_journal, and add_object
the canonical method names for adding new content to calendars.
The save_* methods remain as deprecated aliases for backwards
compatibility.

Rationale: These methods are for *adding* new content, not updating.
To update existing objects, use object.save() after fetching and
modifying the object.

Changes:
- Renamed save_* to add_* in Calendar class (collection.py)
- Updated all documentation to use add_*
- Updated all examples to use add_*
- Updated all tests to use add_*
- Removed testSaveSameUidDifferentUrl (investigative test no longer needed)

The save_* aliases will be kept for backwards compatibility but
should not be used in new code.

Ref: #71

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert simple aliases to proper wrapper methods to:
- Add docstrings documenting deprecation
- Enable future addition of deprecation warnings

Ref: #71

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rename object_by_uid, event_by_uid, todo_by_uid, journal_by_uid to
get_object_by_uid, get_event_by_uid, get_todo_by_uid, get_journal_by_uid
following the API naming conventions (get_* prefix for retrieval methods).

The old method names are kept as deprecated wrappers for backwards
compatibility.

Changes:
- Renamed methods in Calendar class (collection.py)
- Added deprecated wrappers with docstrings
- Updated all internal usages in calendarobjectresource.py and search.py
- Updated all documentation, examples, and tests
- Updated CHANGELOG with deprecation notices
- Updated API_NAMING_CONVENTIONS.md with new method tables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Renamed the following methods to follow API naming conventions:
- calendars() → get_calendars() (CalendarSet, Principal)
- events() → get_events() (Calendar)
- todos() → get_todos() (Calendar)
- journals() → get_journals() (Calendar)
- objects_by_sync_token() → get_objects_by_sync_token() (Calendar)

The old method names are kept as deprecated wrappers for backwards
compatibility.

Also added get_objects alias for get_objects_by_sync_token.

Changes:
- Added new get_* methods as canonical implementations
- Added deprecated wrappers with docstrings
- Updated all internal usages throughout the library
- Updated all documentation, examples, and tests
- Updated CHANGELOG and API_NAMING_CONVENTIONS.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a Docker-based test server (or embedded server) is already running
before tests start, we should reuse it without stopping it afterward.

This commit adds a `_started_by_us` flag that tracks whether the test
framework actually started the server vs finding it already running.
The `stop()` method now checks this flag and only stops servers that
were started by the test framework.

This allows developers to pre-start test servers for faster iteration,
and ensures running servers are preserved across test runs.

Fixes the issue where servers would be restarted even when already
running (related to commit be0cb5d).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The _started_by_us tracking makes sense for Docker servers (which can be
started externally), but not for embedded servers (Radicale, Xandikos)
which always run in-process.

Embedded servers cannot be "externally started" in a meaningful way -
if they're accessible, it's because we started them in this process.
Keeping the original behavior for embedded servers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:

1. Xandikos shutdown: Changed to properly cleanup the aiohttp runner
   BEFORE stopping the event loop. The old code stopped the loop first,
   which caused "cannot schedule new futures after shutdown" errors
   because the executor was shut down while requests were still in flight.

2. Server restart after stop: Added _was_stopped flag to prevent using
   is_accessible() to detect running servers after a stop. After stop()
   is called, the port might still respond briefly before fully closing,
   so subsequent start() calls would incorrectly think the server was
   running and skip starting a new one.

These fixes prevent test failures when running multiple tests that
start/stop the same embedded server (Radicale or Xandikos).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents the Strategy pattern approach for handling multiple data
representations (string, icalendar, vobject) in CalendarObjectResource.

Key concepts:
- Explicit ownership transfer via edit_*() methods
- Safe read-only access via get_*() methods (returns copies)
- Explicit write access via set_*() methods
- Backward compatible legacy properties

See #613

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Incorporated feedback from @niccokunzmann in issue #613:
- Added Null Object Pattern (NoDataStrategy) to eliminate None checks
- Added borrowing pattern with context managers (Rust-inspired)
- Added state machine diagram for edit states
- Clarified this is more of a State pattern than Strategy pattern
- Added comparison table of edit methods vs borrowing approach

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test class is TestForServerRadicale not TestForServerLocalRadicale.
The -k filter needs to match the actual class name.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents all usages of obj.data, obj.icalendar_instance,
obj.icalendar_component, obj.vobject_instance and their aliases
throughout the codebase. Related to issue #613.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tobixen and others added 15 commits January 23, 2026 12:44
…ated_call

These methods are deprecated and tests should verify they emit
deprecation warnings while still testing their functionality.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This adds a safer API for accessing and modifying calendar data:

New read-only methods (return copies, no side effects):
- get_data() - returns iCalendar string
- get_icalendar_instance() - returns copy of icalendar object
- get_vobject_instance() - returns copy of vobject object

New edit context managers (explicit ownership):
- edit_icalendar_instance() - borrow icalendar for editing
- edit_vobject_instance() - borrow vobject for editing

The context managers prevent concurrent modification of different
representations by raising RuntimeError if already borrowed.

Also adds DataState classes (Strategy/State pattern) for internal
data management, which will enable future optimizations.

Backward compatibility is maintained - existing properties still work.

Fixes #613

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This helps catch issues early. Exceptions are made for:
- niquests asyncio.iscoroutinefunction deprecation (upstream fix pending)
- radicale resource warnings (upstream issue)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds optimized methods for internal use that avoid unnecessary parsing:
- get_component_type() - determine VEVENT/VTODO/VJOURNAL without full parse
- Optimized implementations for RawDataState using string search/regex

Also adds internal helper methods to CalendarObjectResource:
- _get_uid_cheap() - get UID without state changes
- _get_component_type_cheap() - get type without parsing
- _has_data() - check for data without conversions

These will be used to optimize internal code paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
has_component() previously converted to string to count components,
which caused a side effect of decoupling icalendar instances.

Now uses the cheap _get_component_type_cheap() accessor which
uses simple string search or direct object inspection without
triggering format conversions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove invalid WARNING category filters (logging != warnings)
- Re-enable ResourceWarning and thread exception filters for Radicale

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document the upstream Radicale bug (#1972) that causes ResourceWarning
and PytestUnraisableExceptionWarning during test server shutdown.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Mark issue #613 design as implemented with summary of new API methods.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Issue #613 - Safe Data Access API:

Tests:
- testDataAPICheapAccessors: Test cheap internal accessors
- testDataAPIStateTransitions: Test state pattern transitions
- testDataAPINoDataState: Test NoDataState null object pattern
- testDataAPIEdgeCases: Test folded UIDs, sequential edits

Optimizations:
- is_loaded(): Use _has_data() and _get_component_type_cheap()
- _verify_reverse_relation(): Use _get_uid_cheap()
- set_relation(): Use _get_uid_cheap() for other object
- _generate_url(): Use _get_uid_cheap()

Documentation:
- tutorial.rst: Add "Safe Data Access (3.0+)" section with context manager examples
- tutorial.rst: Add warning about legacy property pitfalls

Examples:
- basic_usage_examples.py: Rewrite read_modify_event_demo() to use new API
- sync_examples.py: Fix typo and use get_icalendar_instance()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Simplify tutorial.rst: just show the recommended way to edit data
- Move detailed explanation to howtos.rst
- Use edit_icalendar_instance() in tutorial example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Ref issue #515 - event.id was not always returning the correct UID.
Now .id is a property that extracts the UID from the calendar data
using cheap accessors (_get_uid_cheap), with fallback to full parsing.

- id property getter reads from data, not a separate attribute
- id setter is a no-op (for parent class compatibility)
- Removed self.id assignments that are no longer needed
- __init__ only modifies UID when component actually exists

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a new pattern for building search queries:
  searcher = calendar.searcher(event=True, start=..., end=...)
  searcher.add_property_filter("SUMMARY", "meeting")
  results = searcher.search()

This avoids requiring users to import CalDAVSearcher directly.

Changes:
- Add _calendar field to CalDAVSearcher to store bound calendar
- Make calendar parameter optional in search()/async_search()
- Add Calendar.searcher() method that creates a bound CalDAVSearcher
- Add tests for the new API pattern

Also fixes a bug where copy(keep_uid=False) didn't properly update the
UID. The issue was that _state was cached with the original data before
the icalendar component was modified.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The id property was falling back to _get_icalendar_component which calls
load(only_if_unloaded=True). This caused issues when the icalendar_instance
had its subcomponents cleared (as happens during search result filtering),
because is_loaded() would return False and trigger a reload from the server.

Changes:
- id property now looks directly in _icalendar_instance without triggering load
- _set_icalendar_instance and _set_vobject_instance now keep _state in sync

This fixes the testRecurringDateSearch failure that was introduced in the
issue #515 commit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants