diff --git a/AI-POLICY.md b/AI-POLICY.md index e2aedc90..60737b32 100644 --- a/AI-POLICY.md +++ b/AI-POLICY.md @@ -2,7 +2,7 @@ ## Read this first -The most important rule: Inform about it! +The most important rule: Be honest and 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 @@ -13,17 +13,15 @@ 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? +check --fix ; gh pr create` or telling Claude "Please fix support for +Microsoft Exchange and create a pull request". 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! +each such pull request. At least, I should not waste time trying to +explain in details why I'm rejecting the pull request. ## Bugfixes are (most often) welcome @@ -52,21 +50,20 @@ changes. handle), so it would be nice if you understand the change you're proposing. -* **Transparency** is important. I don't care about your full - tool-chain, but if a significant part of the value in the pull - request was generated by tools, then it's relevant. Do not write "I - found this issue and here is a fix", but rather "I ran the ruff tool - on the code, found this issue, and here is the fix". If the AI was - fixing a bug for you, then write in the pull request that "this code - was AI-generated and haven't been thoroughly reviewed by me". - Commit message should end with "Assisted-by: (name of tool)", - alternatively i.e. `Co-Authored-By: Claude ` +* **Transparency** is important. If a significant part of the value + in the pull request was generated by tools, then it's relevant. Do + not write "I found this issue and here is a fix", but rather "I ran + the ruff tool on the code, it found this issue, and here is the + fix". If the AI was fixing a bug for you, then write in the pull + request that "this bugfix was vibed up, I have no idea what the AI + has done here". End the commit message with "Assisted-By: ..." or + "Co-Authored-By: ...". * **YOU** should be ready to follow up and respond to feedback and questions on the contribution. If you're letting the AI do this for - you, then you're neither honest nor adding value to the project. - You should at least do a quick QA on the AI-answer and acknowledge - that it was generated by the AI. + you on your behalf, then it's a chance you're neither honest nor + adding value to the project. You should at least do a quick QA on + the AI-answer and acknowledge that it was generated by the AI. * The Contributors Guidelines aren't strongly enforced on this project as of 2026-02, and I can hardly see cases where the AI would break diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc0348d8..db40c3e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Contributions are mostly welcome. If the length of this text scares you, then I ## Usage of AI and other tools -A separate [AI POLICY](AI-POLICY.md) has been made. The gist of it, be transparent and inform if your contribution was a result of clever tool usage and/or AI-usage, don't submit code if you don't understand the code yourself, and you are supposed to contribute value to the project. If you're too lazy to read the AI Policy, then at least have a chat with the AI to work out if your contribution is within the policy or not. +A separate [AI POLICY](AI-POLICY.md) has been made. If you want to use AI and you're too lazy to read the AI Policy, then at least ask the AI to read it and chat with it to work out if your contribution is within the policy or not. ## GitHub diff --git a/README.md b/README.md index 946efb28..01813c09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # caldav -This project is a CalDAV ([RFC4791](http://www.ietf.org/rfc/rfc4791.txt)) client library for Python. +This project is a CalDAV ([RFC4791](https://datatracker.ietf.org/doc/html/rfc4791)) client library for Python. Features: diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index 6e9bcf05..81f8f88e 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -595,7 +595,7 @@ def add_attendee(self, attendee, no_default_parameters: bool = False, **paramete def is_invite_request(self) -> bool: """ Returns True if this object is a request, see - https://www.rfc-editor.org/rfc/rfc2446.html#section-3.2.2 + :rfc:`2446#section-3.2.2`. """ self.load(only_if_unloaded=True) return self.icalendar_instance.get("method", None) == "REQUEST" @@ -603,7 +603,7 @@ def is_invite_request(self) -> bool: def is_invite_reply(self) -> bool: """ Returns True if the object is a reply, see - https://www.rfc-editor.org/rfc/rfc2446.html#section-3.2.3 + :rfc:`2446#section-3.2.3`. """ self.load(only_if_unloaded=True) return self.icalendar_instance.get("method", None) == "REPLY" diff --git a/caldav/collection.py b/caldav/collection.py index f2e341ae..ccdd0141 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -528,7 +528,7 @@ class Calendar(DAVObject): """ The `Calendar` object is used to represent a calendar collection. Refer to the RFC for details: - https://tools.ietf.org/html/rfc4791#section-5.3.1 + :rfc:`4791#section-5.3.1`. """ def __init__( diff --git a/caldav/elements/cdav.py b/caldav/elements/cdav.py index 52186962..b77061cc 100644 --- a/caldav/elements/cdav.py +++ b/caldav/elements/cdav.py @@ -103,7 +103,7 @@ class TimeRange(BaseElement): def __init__(self, start: datetime | None = None, end: datetime | None = None) -> None: ## start and end should be an icalendar "date with UTC time", - ## ref https://tools.ietf.org/html/rfc4791#section-9.9 + ## ref https://datatracker.ietf.org/doc/html/rfc4791#section-9.9 super(TimeRange, self).__init__() if self.attributes is None: diff --git a/docs/design/FEATURE_COMPLETE_ROADMAP.md b/docs/design/FEATURE_COMPLETE_ROADMAP.md index 5ac28782..2a9e185d 100644 --- a/docs/design/FEATURE_COMPLETE_ROADMAP.md +++ b/docs/design/FEATURE_COMPLETE_ROADMAP.md @@ -29,7 +29,7 @@ This roadmap covers the **remaining gaps** to achieve full RFC compliance and ad **Priority:** High **Estimated effort:** 40-60 hours -**RFC:** [RFC 3744](https://www.rfc-editor.org/rfc/rfc3744) +**RFC:** [RFC 3744](https://datatracker.ietf.org/doc/html/rfc3744) Current state: The library has basic principal support but lacks ACL manipulation. @@ -50,7 +50,7 @@ Current state: The library has basic principal support but lacks ACL manipulatio **Priority:** High **Estimated effort:** 40 hours (partially covered in #599 for v3.2) -**RFC:** [RFC 6638](https://www.rfc-editor.org/rfc/rfc6638) +**RFC:** [RFC 6638](https://datatracker.ietf.org/doc/html/rfc6638) The v3.2 roadmap covers basic scheduling improvements. Additional work for full compliance: @@ -71,7 +71,7 @@ The v3.2 roadmap covers basic scheduling improvements. Additional work for full **Priority:** Medium **Estimated effort:** 16-24 hours -**RFC:** [RFC 7953](https://www.rfc-editor.org/rfc/rfc7953) +**RFC:** [RFC 7953](https://datatracker.ietf.org/doc/html/rfc7953) **Related issue:** #425 **Tasks:** @@ -88,7 +88,7 @@ The v3.2 roadmap covers basic scheduling improvements. Additional work for full **Priority:** Medium **Estimated effort:** 8-12 hours -**RFC:** [RFC 7986](https://www.rfc-editor.org/rfc/rfc7986) +**RFC:** [RFC 7986](https://datatracker.ietf.org/doc/html/rfc7986) **Tasks:** - [ ] Support calendar-level properties: `NAME`, `DESCRIPTION`, `COLOR`, `REFRESH-INTERVAL`, `SOURCE` @@ -148,7 +148,7 @@ The v3.2 roadmap covers basic scheduling improvements. Additional work for full **Priority:** Low **Estimated effort:** 24-32 hours -**RFC:** [RFC 8607](https://www.rfc-editor.org/rfc/rfc8607) +**RFC:** [RFC 8607](https://datatracker.ietf.org/doc/html/rfc8607) **Tasks:** - [ ] Detect server support for `calendar-managed-attachments` @@ -181,7 +181,7 @@ Note: This is a draft standard but widely implemented by major servers. **Priority:** Low **Estimated effort:** 4-8 hours -**RFC:** [RFC 5689](https://www.rfc-editor.org/rfc/rfc5689) +**RFC:** [RFC 5689](https://datatracker.ietf.org/doc/html/rfc5689) **Tasks:** - [ ] Support extended MKCOL as alternative to MKCALENDAR @@ -194,7 +194,7 @@ Note: This is a draft standard but widely implemented by major servers. **Priority:** Low **Estimated effort:** 4-8 hours -**RFC:** [RFC 4331](https://www.rfc-editor.org/rfc/rfc4331) +**RFC:** [RFC 4331](https://datatracker.ietf.org/doc/html/rfc4331) **Tasks:** - [ ] Add `calendar.get_quota()` method @@ -270,7 +270,7 @@ Note: This is a draft standard but widely implemented by major servers. **Priority:** Medium **Estimated effort:** 16-24 hours **Related issue:** #571 -**RFC:** [RFC 6764 Section 8](https://www.rfc-editor.org/rfc/rfc6764#section-8) +**RFC:** [RFC 6764 Section 8](https://datatracker.ietf.org/doc/html/rfc6764#section-8) **Tasks:** - [ ] Add optional DNSSEC validation for SRV/TXT lookups @@ -301,7 +301,7 @@ Note: This is a draft standard but widely implemented by major servers. **Priority:** Low **Estimated effort:** 16-24 hours -**RFC:** [RFC 7265](https://www.rfc-editor.org/rfc/rfc7265) +**RFC:** [RFC 7265](https://datatracker.ietf.org/doc/html/rfc7265) **Tasks:** - [ ] Accept `application/calendar+json` responses @@ -314,7 +314,7 @@ Note: This is a draft standard but widely implemented by major servers. **Priority:** Low **Estimated effort:** 16-24 hours -**RFC:** [RFC 6321](https://www.rfc-editor.org/rfc/rfc6321) +**RFC:** [RFC 6321](https://datatracker.ietf.org/doc/html/rfc6321) **Tasks:** - [ ] Accept `application/calendar+xml` responses @@ -488,23 +488,23 @@ Based on the roadmap, suggested version milestones after v3.2: ## References ### Core Standards -- [RFC 4791 - CalDAV](https://www.rfc-editor.org/rfc/rfc4791) -- [RFC 6638 - CalDAV Scheduling](https://www.rfc-editor.org/rfc/rfc6638) -- [RFC 4918 - WebDAV](https://www.rfc-editor.org/rfc/rfc4918) -- [RFC 3744 - WebDAV ACL](https://www.rfc-editor.org/rfc/rfc3744) -- [RFC 5545 - iCalendar](https://www.rfc-editor.org/rfc/rfc5545) +- [RFC 4791 - CalDAV](https://datatracker.ietf.org/doc/html/rfc4791) +- [RFC 6638 - CalDAV Scheduling](https://datatracker.ietf.org/doc/html/rfc6638) +- [RFC 4918 - WebDAV](https://datatracker.ietf.org/doc/html/rfc4918) +- [RFC 3744 - WebDAV ACL](https://datatracker.ietf.org/doc/html/rfc3744) +- [RFC 5545 - iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) ### Extensions -- [RFC 6764 - Service Discovery](https://www.rfc-editor.org/rfc/rfc6764) -- [RFC 6578 - WebDAV Sync](https://www.rfc-editor.org/rfc/rfc6578) -- [RFC 7953 - Calendar Availability](https://www.rfc-editor.org/rfc/rfc7953) -- [RFC 7986 - New iCalendar Properties](https://www.rfc-editor.org/rfc/rfc7986) -- [RFC 8607 - Managed Attachments](https://www.rfc-editor.org/rfc/rfc8607) +- [RFC 6764 - Service Discovery](https://datatracker.ietf.org/doc/html/rfc6764) +- [RFC 6578 - WebDAV Sync](https://datatracker.ietf.org/doc/html/rfc6578) +- [RFC 7953 - Calendar Availability](https://datatracker.ietf.org/doc/html/rfc7953) +- [RFC 7986 - New iCalendar Properties](https://datatracker.ietf.org/doc/html/rfc7986) +- [RFC 8607 - Managed Attachments](https://datatracker.ietf.org/doc/html/rfc8607) ### Related -- [RFC 5546 - iTIP](https://www.rfc-editor.org/rfc/rfc5546) -- [RFC 6321 - xCal](https://www.rfc-editor.org/rfc/rfc6321) -- [RFC 7265 - jCal](https://www.rfc-editor.org/rfc/rfc7265) +- [RFC 5546 - iTIP](https://datatracker.ietf.org/doc/html/rfc5546) +- [RFC 6321 - xCal](https://datatracker.ietf.org/doc/html/rfc6321) +- [RFC 7265 - jCal](https://datatracker.ietf.org/doc/html/rfc7265) - [CalConnect Developer Guide](https://devguide.calconnect.org/) --- diff --git a/docs/source/about.rst b/docs/source/about.rst index a9a35d71..57ac8fa4 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -65,40 +65,40 @@ Support for Python2 was officially not supported starting from caldav version 1.0. -RFC 4791, 2518, 5545, 6638 et al --------------------------------- +RFC compliance +-------------- -RFC 4791 (CalDAV) outlines the standard way of communicating with a -calendar server. RFC 4791 is an extension of RFC 4918 (WebDAV). The -scope of this library is basically to cover RFC 4791/4918, the actual +:rfc:`4791` (CalDAV) outlines the standard way of communicating with a +calendar server. :rfc:`4791` is an extension of :rfc:`4918` (WebDAV). The +scope of this library is basically to cover :rfc:`4791` and :rfc:`4918`, the actual communication with the caldav server. (The WebDAV standard also has quite some extensions, this library supports some of the relevant extensions as well). -There exists another library webdavclient3 for handling RFC 4918 +There exists another library webdavclient3 for handling :rfc:`4918` (WebDAV), ideally we should be depending on it rather than overlap it. -RFC 6638/RFC 6047 is extending the CalDAV and iCalendar protocols for -scheduling purposes, work is in progress to support RFC 6638. Support -for RFC 6047 is considered mostly outside the scope of this library, +:rfc:`6638` and :rfc:`6047` extend the CalDAV and iCalendar protocols for +scheduling purposes, work is in progress to support :rfc:`6638`. Support +for :rfc:`6047` is considered mostly outside the scope of this library, though for convenience this library may contain methods like accept() on a calendar invite (which involves fetching the invite from the server, editing the calendar data and putting it to the server). This library should make it trivial to fetch an event, modify the data and save it back to the server - but to do that it's also needed to -support RFC 5545 (icalendar). It's outside the scope of this library -to implement logic for parsing and modifying RFC 5545, instead we +support :rfc:`5545` (icalendar). It's outside the scope of this library +to implement logic for parsing and modifying :rfc:`5545`, instead we depend on another library for that. -RFC 5545 describes the icalendar format. Constructing or parsing +:rfc:`5545` describes the icalendar format. Constructing or parsing icalendar data was considered out of the scope of this library, but we do make exceptions - like, there is a method to complete a task - it involves editing the icalendar data, and now the ``add_event``, ``add_todo`` and ``add_journal`` methods are able to construct icalendar data if needed. -There exists two libraries supporting RFC 5545, vobject and icalendar. +There exists two libraries supporting :rfc:`5545`, vobject and icalendar. vobject was unmaintained for several years, but seems to be actively maintained now. The caldav library originally came with vobject support, but as many people requested the vobject dependency to be @@ -109,7 +109,7 @@ Misbehaving server implementations ---------------------------------- Some server implementations may have some "caldav"-support that either -doesn't implement all of RFC 4791, breaks the standard a bit, or has +doesn't implement all of :rfc:`4791`, breaks the standard a bit, or has extra features. As long as it doesn't add too much complexity to the code, hacks and workarounds for "badly behaving caldav servers" are considered to be within the scope. Ideally, users of the caldav @@ -253,7 +253,7 @@ Server-specific highlights VTODOs must be stored in a calendar explicitly created for the ``VTODO`` component type. -* **Calendar creation** is not mandatory under RFC 4791. Most self-hosted +* **Calendar creation** is not mandatory under :rfc:`4791`. Most self-hosted servers support it; Google's CalDAV adapter does not. * **Recurring events and tasks** are non-trivial to implement correctly on @@ -263,7 +263,7 @@ Server-specific highlights Some notes on CalDAV URLs ========================= -From v2.1, well-known URLs were hard-coded into the compatibility_hints. As of v2.2, auto-detection based on RFC6764 is supported. This protocol is widely used. For servers supporting it, it's sufficient to add something like "demo2.nextcloud.com" in the URL. For well-known calendar providers, it's not needed to enter anything in the URL, it suffices to put i.e. `features="ecloud"` into the connection parameters. +From v2.1, well-known URLs were hard-coded into the compatibility_hints. As of v2.2, auto-detection based on :rfc:`6764` is supported. This protocol is widely used. For servers supporting it, it's sufficient to add something like "demo2.nextcloud.com" in the URL. For well-known calendar providers, it's not needed to enter anything in the URL, it suffices to put i.e. `features="ecloud"` into the connection parameters. CalDAV URLs can be quite confusing, some software requires the URL to the calendar, other requires the URL to the principal. The Python CalDAV library does support accessing calendars and principals using such URLs, but the recommended practice is to configure up the CalDAV root URL and tell the library to find the principal and calendars from that. Typical examples of CalDAV URLs: diff --git a/docs/source/async.rst b/docs/source/async.rst index a08619b8..2071a935 100644 --- a/docs/source/async.rst +++ b/docs/source/async.rst @@ -57,7 +57,7 @@ The ``caldav.aio`` module exports: * ``AsyncCalendarSet`` - Collection of calendars * ``AsyncPrincipal`` - User principal -**Scheduling (RFC6638):** +**Scheduling (:rfc:`6638`):** * ``AsyncScheduleInbox`` - Incoming invitations * ``AsyncScheduleOutbox`` - Outgoing invitations diff --git a/docs/source/jmap.rst b/docs/source/jmap.rst index 32212501..bbeee9ba 100644 --- a/docs/source/jmap.rst +++ b/docs/source/jmap.rst @@ -5,9 +5,9 @@ JMAP **The JMAP support in v3.0 is experimental, the API may change in v3.1 of the library** The caldav library includes a JMAP client for servers that speak -`RFC 8620 `_ (JMAP Core) and +:rfc:`8620` (JMAP Core) and the JMAP Calendars protocol (``urn:ietf:params:jmap:calendars``), which uses -`RFC 8984 `_ (JSCalendar) as its data format. +:rfc:`8984` (JSCalendar) as its data format. It covers calendar listing, event CRUD, incremental sync, and task CRUD — the same operations as the CalDAV client — so the choice of protocol comes down to what the server supports. @@ -361,7 +361,7 @@ The three specific error classes: * :class:`~caldav.jmap.error.JMAPCapabilityError` — the server's Session object does not advertise ``urn:ietf:params:jmap:calendars``. * :class:`~caldav.jmap.error.JMAPMethodError` — a JMAP method call returned an error - response. The ``error_type`` attribute holds the RFC 8620 error type string + response. The ``error_type`` attribute holds the :rfc:`8620` error type string (e.g. ``"invalidArguments"``, ``"notFound"``, ``"stateMismatch"``). Configuration File diff --git a/docs/source/v3-migration.rst b/docs/source/v3-migration.rst new file mode 100644 index 00000000..d0a5de98 --- /dev/null +++ b/docs/source/v3-migration.rst @@ -0,0 +1,512 @@ +========================================= +Migrating from caldav 2.x to 3.x +========================================= + +v3.0 is mostly backward-compatible with v2.x. Existing code should +generally continue to run. New method names and usage patterns exists +alongside the old ones. The purpose of this document is to give a +primer on the "best current usage practice" as of v3.0. It may be +needed to implement some of the changes below before using future +major versions like v4 or v5. + +.. contents:: Contents + :local: + :depth: 2 + + +Breaking Changes +================ + +Python version +-------------- + +Python 3.10 or later is now required. Python 3.8 and 3.9 are no longer +supported. + +``caldav.objects`` import shim removed +--------------------------------------- + +The ``caldav/objects.py`` backward-compatibility re-export module has been +deleted. If you have: + +.. code-block:: python + + from caldav.objects import Event, Todo, Calendar # REMOVED + +replace it with: + +.. code-block:: python + + from caldav import Event, Todo, Calendar # OK + +All public symbols that were in ``caldav.objects`` should remain available directly +from the ``caldav`` namespace + +Wildcard import into caldav.* removed +------------------------------------- +Earlier a wildcard import ``from caldav.objects import *`` was done into the ``caldav`` namespace. This has been removed. In normal circumstances, your imports should continue to work - but I cannot give any guarantees that YOUR imports will continue working. If you have any issues, then reach out. + +..todo:: how to add a link from "reach out" to the contact page? + +Config-file parse errors now raise exceptions +--------------------------------------------- + +``read_config()`` used to log and return an empty dict on YAML/JSON parse +errors. It now raises ``ValueError``. This means misconfigured files fail +loudly rather than silently. + + +Recommended API Changes +======================== + +The changes below are not yet breaking — the old names still work. Some of them +emits ``DeprecationWarning``, others will do so in an upcoming release. New code +should use the new names. + + +Factory function instead of direct instantiation +------------------------------------------------- + +.. list-table:: + :widths: 45 55 + :header-rows: 1 + + * - v2.x + - v3.x (recommended) + * - ``DAVClient(url=..., username=..., password=...)`` + - ``get_davclient(url=..., username=..., password=...)`` + +:func:`caldav.get_davclient` reads credentials from environment variables and +config files, selects an appropriate HTTP library, and handles server-specific +compatibility hints automatically: + +.. code-block:: python + + from caldav import get_davclient + + # Credentials from env vars or ~/.config/caldav/calendar.conf + with get_davclient() as client: + principal = client.get_principal() + ... + + # Or supply them explicitly + with get_davclient(url="https://caldav.example.com/", + username="alice", password="secret") as client: + principal = client.get_principal() + ... + + # Use a named compatibility profile (iCloud, google, nextcloud, …) + with get_davclient(features="icloud", + username="alice@icloud.com", password="...") as client: + ... + +Calendars may be configured in the config file, and it's also possible to get a celndar directly through factory method: + +.. code-block::python + + from caldav import get_calendar + with my_calendar as get_calendar(config_section='work_calendar'): + ... + +See :doc:`configfile` for the config-file format and the full list of +parameters. + + +Principal and calendar access +------------------------------ + +Quite some methods have been renamed for consistency both within the package and with the python ecosystem as such. + +.. list-table:: + :widths: 45 55 + :header-rows: 1 + + * - v2.x + - v3.x + * - ``client.principal()`` + - ``client.get_principal()`` + * - ``principal.calendars()`` + - ``principal.get_calendars()`` + * - ``client.principals(name=...)`` + - ``client.search_principals(name=...)`` + +.. code-block:: python + + # v2.x + principal = client.principal() + calendars = principal.calendars() + + # v3.x + principal = client.get_principal() + calendars = principal.get_calendars() + +Rationale: Those methods are actively querying the server for data, hence a verb is more fitting. + +Adding calendar objects +------------------------ + +The ``save_*`` family is deprecated. Use ``add_*`` for adding *new* objects: + +.. list-table:: + :widths: 45 55 + :header-rows: 1 + + * - v2.x + - v3.x + * - ``calendar.save_event(ical_str)`` + - ``calendar.add_event(ical_str)`` + * - ``calendar.save_todo(ical_str)`` + - ``calendar.add_todo(ical_str)`` + * - ``calendar.save_journal(ical_str)`` + - ``calendar.add_journal(ical_str)`` + * - ``calendar.save_object(ical_str)`` + - ``calendar.add_object(ical_str)`` + +To **update** an existing object, fetch it and call ``object.save()``: + +.. code-block:: python + + event = calendar.get_event_by_uid("some-uid") + event.data = new_ical_string + event.save() + +Rationale: In the tests, documentation and examples I'm always adding new content with those methods, so add feels more right than save. This is a revert of a change that was done in v0.7.0. See https://github.com/python-caldav/caldav/issues/71 for details. While the ``add_object``-method possibly MAY be used for updating an object, it SHOULD not be used for this purpose. + +Listing and fetching objects +----------------------------- + +.. list-table:: + :widths: 45 55 + :header-rows: 1 + + * - v2.x + - v3.x + * - ``calendar.events()`` + - ``calendar.get_events()`` + * - ``calendar.todos()`` + - ``calendar.get_todos()`` + * - ``calendar.journals()`` + - ``calendar.get_journals()`` + * - ``calendar.event_by_uid(uid)`` + - ``calendar.get_event_by_uid(uid)`` + * - ``calendar.todo_by_uid(uid)`` + - ``calendar.get_todo_by_uid(uid)`` + * - ``calendar.journal_by_uid(uid)`` + - ``calendar.get_journal_by_uid(uid)`` + * - ``calendar.object_by_uid(uid)`` + - ``calendar.get_object_by_uid(uid)`` + * - ``calendar.objects_by_sync_token()`` + - ``calendar.get_objects_by_sync_token()`` + +Rationale: Those methods are actively querying the server for data, hence a verb is more fitting. + +Searching +---------- + +``date_search()`` is deprecated. Use ``search()`` instead: + +.. code-block:: python + + from datetime import datetime + + # v2.x + events = calendar.date_search(start=datetime(2024,1,1), + end=datetime(2024,12,31), + expand=True) + + # v3.x — note the keyword arguments and the explicit event=True flag + events = calendar.search(start=datetime(2024,1,1), + end=datetime(2024,12,31), + event=True, expand=True) + +``search()`` also accepts ``todo=True``, ``journal=True``, ``comp_class=Event``, +free-text filters, category filters, and more. See the API reference for the +full signature. + +Rationale: ``date_search`` has actually been deprecated since 2.0, if not longer. It's just a special case of ``search``. + +Capability checks +----------------- + +.. list-table:: + :widths: 45 55 + :header-rows: 1 + + * - v2.x + - v3.x + * - ``client.check_dav_support()`` + - ``client.supports_dav()`` + * - ``client.check_cdav_support()`` + - ``client.supports_caldav()`` + * - ``client.check_scheduling_support()`` + - ``client.supports_scheduling()`` + +Rationale: Those methods are also querying the server actively, and not just looking up things in a feature-matrix, hnence a verb is more fitting. + +Accessing and editing calendar data +===================================== + +This is the most significant new API in v3.0, addressing a long-standing +ambiguity in how calendar object data was accessed and modified. + +The old ``vobject_instance``, ``icalendar_instance``, ``icalendar_component`` are now deprecated. + +The Problem with the 2.x API +------------------------------ + +In 2.x, the properties ``data``, ``icalendar_instance``, and +``vobject_instance`` on a ``CalendarObjectResource`` all shared a single +internal slot. Accessing one representation could silently invalidate another: + +.. code-block:: python + + # 2.x — silent bug + event = calendar.search(...)[0] + comp = event.icalendar_component # get a reference + _ = event.data # accessing data invalidates comp! + comp["SUMMARY"] = "Updated" + event.save() # change is NOT saved + +The 3.x Solution: read and edit methods +----------------------------------------- + +v3.0 adds explicit read-only getters that always return **copies**, and +context-manager "borrow" methods that give exclusive, safe write access. + +**Read-only access** (safe at any time, returns a copy): + +.. code-block:: python + + # Get raw iCalendar string + raw = event.get_data() + + # Get a copy of the icalendar.Calendar object — changes are NOT saved + cal_copy = event.get_icalendar_instance() + summary = cal_copy.subcomponents[0]["SUMMARY"] + + # Get a copy of the vobject component — changes are NOT saved + vobj_copy = event.get_vobject_instance() + summary = vobj_copy.vevent.summary.value + + # Quick access to the inner VEVENT/VTODO/VJOURNAL component + summary = event.component["SUMMARY"] # component is always a copy + +**Editing with icalendar** (borrowing pattern): + +.. code-block:: python + + with event.edit_icalendar_instance() as cal: + for comp in cal.subcomponents: + if comp.name == "VEVENT": + comp["SUMMARY"] = "Updated summary" + event.save() + +**Editing with vobject** (borrowing pattern): + +.. code-block:: python + + with event.edit_vobject_instance() as vobj: + vobj.vevent.summary.value = "Updated summary" + event.save() + +While inside the ``with`` block, the borrowed representation is the +single source of truth. Attempting to borrow a *different* +representation raises ``RuntimeError``. This means the current +pattern is not completely thread-safe as of v3.0 - but an explicit +error is often better than updates silently being dropped. + +**The data representation remains the same:** + +The interface for the string representation is still the same. Strings are immutable, so the concern above is not relevant for strings. (Of course, the code below will have bad side effects if the event was modified simultaneously by another thread, as well as if the event was modified on the server by another client). + +.. code-block:: python + + ## Get the raw iCalendar string + ical_string = event.data + new_ical_string = modify_ical(ical_string) + + # Replace all data from a raw iCalendar string + event.data = new_ical_string + event.save() + +**Summary of the new data API:** + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Method / property + - Purpose + * - ``event.get_data()`` + - Raw iCalendar string, always a copy + * - ``event.get_icalendar_instance()`` + - icalendar.Calendar copy, safe for read-only use + * - ``event.get_vobject_instance()`` + - vobject component copy, safe for read-only use + * - ``event.component`` + - Alias for the inner VEVENT/VTODO/VJOURNAL component (copy) + * - ``event.edit_icalendar_instance()`` + - Context manager — exclusive write access via icalendar + * - ``event.edit_vobject_instance()`` + - Context manager — exclusive write access via vobject + * - ``event.data = "..."`` + - Replace all data from a raw string + + +New in v3.0 +============ + +Async client +------------- + +Async operations are often the "best current practice" in the Python world. Now it's possible also with the CalDAV library. + +A new :class:`caldav.async_davclient.AsyncDAVClient` provides the same API +with ``async/await`` support. All domain objects (``Calendar``, ``Event``, +``Todo``, …) work with both the sync and async clients: + +.. code-block:: python + + from caldav.async_davclient import get_davclient + + async def main(): + async with await get_davclient(url="...", username="...", + password="...") as client: + principal = await client.get_principal() + calendars = await principal.get_calendars() + for cal in calendars: + events = await cal.get_events() + +See :doc:`async` for more details. + +JMAP client (experimental) +--------------------------- + +A new ``caldav.jmap`` package provides ``JMAPClient`` and ``AsyncJMAPClient`` +for servers implementing :rfc:`8620` (JMAP Core) and :rfc:`8984` (JMAP Calendars). +The public API may change in minor releases. See :doc:`jmap`. + +Advanced search +--------------- + +The new ``CalDAVSearcher`` / ``calendar.searcher()`` API allows building +composite search queries: + +.. code-block:: python + + from caldav import CalDAVSearcher, Todo + + searcher = calendar.searcher() + searcher.add_property_filter("category", "WORK") + searcher.add_property_filter("status", "NEEDS-ACTION") + todos = searcher.search(calendar) + + # Or as a one-liner + searcher = CalDAVSearcher(comp_class=Todo) + searcher.add_property_filter("category", "WORK", case_sensitive=False) + results = searcher.search(calendar) + +Compatibility hints / ``features`` parameter +-------------------------------------------- + +Server-specific quirks are now encoded in named profiles in +``caldav/compatibility_hints.py``. Pass the profile name via the ``features`` +parameter to get automatic workarounds: + +.. code-block:: python + + client = get_davclient(url="https://...", features="nextcloud", + username="alice", password="secret") + +You can also override individual flags: + +.. code-block:: python + + client = get_davclient(url="https://...", + features={"search.text": {"support": "unsupported"}}) + +In the config file it's possible to combine a base profile with overrides in the config file: + +.. code-block:: yaml + + my-server: + caldav_url: https://caldav.example.com/ + caldav_username: alice + caldav_password: secret + features: + base: nextcloud + search.text: unsupported + +See :ref:`about:Compatibility` for more on the compatibility hints system. + +Object UID as ``.id`` +---------------------- + +``CalendarObjectResource.id`` is now available as a shortcut for the +``UID`` property: + +.. code-block:: python + + event = calendar.add_event(ical_string) + print(event.id) # same as event.icalendar_component["UID"] + +Other notable changes +--------------------- + +caldav 3.x uses **niquests** by default for HTTP communication. Niquests is a +backward-compatible fork of requests that adds HTTP/2, HTTP/3, and async +support. If you need to switch to ``requests`` or ``httpx``, see +:doc:`http-libraries`. + +Rate-limit support is now built into the CalDAV library. Pass +``rate_limit_handle=True`` to automatically sleep and retry on ``429 +Too Many Requests`` / ``503 Service Unavailable`` responses that +include a ``Retry-After`` header: + +.. code-block:: python + + client = get_davclient(url="...", rate_limit_handle=True) + +This may also be configured: + +.. code-block:: yaml + + my-server: + caldav_url: https://caldav.example.com/ + caldav_username: alice + caldav_password: secret + features: + rate_limit: + enable: True + default_sleep: 4 + max_sleep: 120 + +If the server gives a `retry-after`-header on 429 or 530 it will be respected, otherwise the `default_sleep` will be utilized on 429. This happens in a loop, the sleep period will be multiplied with 1.5 on every retry. + +The total sleep period will never exceed 120, no matter if retry-after is given or not. + +Deprecated in v3.0 (will be removed in v4.0) +============================================= + +The following emit ``DeprecationWarning``: + +* ``calendar.date_search()`` — use ``calendar.search()`` +* ``client.principals()`` — use ``client.search_principals()`` +* ``obj.split_expanded`` attribute +* ``obj.expand_rrule`` attribute +* ``.instance`` property on calendar objects — use ``.vobject_instance`` +* ``response.find_objects_and_props()`` — use ``response.results`` + +The following are deprecated but do **not yet** emit warnings: + +* All ``save_*`` methods → use ``add_*`` +* All ``*_by_uid()`` methods → use ``get_*_by_uid()`` +* ``principal.calendars()`` → ``principal.get_calendars()`` +* ``calendar.events()`` → ``calendar.get_events()`` +* ``calendar.todos()`` → ``calendar.get_todos()`` +* ``calendar.journals()`` → ``calendar.get_journals()`` +* ``calendar.objects_by_sync_token()`` → ``calendar.get_objects_by_sync_token()`` +* ``client.principal()`` → ``client.get_principal()`` +* ``client.check_dav_support()`` → ``client.supports_dav()`` +* ``client.check_cdav_support()`` → ``client.supports_caldav()`` +* ``client.check_scheduling_support()`` → ``client.supports_scheduling()`` diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 6d1b616e..837d22c5 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -164,7 +164,7 @@ def _make_client( END:VCALENDAR """ -# example from http://www.rfc-editor.org/rfc/rfc5545.txt +# example from https://datatracker.ietf.org/doc/html/rfc5545 evr = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN @@ -209,7 +209,7 @@ def _make_client( END:VEVENT END:VCALENDAR""" -# example from http://www.rfc-editor.org/rfc/rfc5545.txt +# example from https://datatracker.ietf.org/doc/html/rfc5545 todo = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN @@ -3249,7 +3249,7 @@ def testRecurringDateSearch(self): expand=True, ) - ## According to https://tools.ietf.org/html/rfc4791#section-7.8.3, the + ## According to https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.3, the ## resultset should be one vcalendar with two events. assert len(r1) == 1 assert "RRULE" not in r1[0].data diff --git a/tests/test_caldav_unit.py b/tests/test_caldav_unit.py index 2321789f..47850476 100755 --- a/tests/test_caldav_unit.py +++ b/tests/test_caldav_unit.py @@ -126,7 +126,7 @@ END:VCALENDAR """ -# example from http://www.rfc-editor.org/rfc/rfc5545.txt +# example from https://datatracker.ietf.org/doc/html/rfc5545 evr = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN diff --git a/tests/test_vcal.py b/tests/test_vcal.py index c1ac0c3e..4593864a 100644 --- a/tests/test_vcal.py +++ b/tests/test_vcal.py @@ -13,7 +13,7 @@ utc = timezone.utc -# example from http://www.rfc-editor.org/rfc/rfc5545.txt +# example from https://datatracker.ietf.org/doc/html/rfc5545 ev = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN