Skip to content
Open
4 changes: 2 additions & 2 deletions caldav/async_davclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,9 @@ def build_auth_object(self, auth_types: list[str] | None = None) -> None:
if _USE_HTTPX:
self.auth = httpx.DigestAuth(self.username, self.password)
else:
from niquests.auth import HTTPDigestAuth
from niquests.auth import AsyncHTTPDigestAuth

self.auth = HTTPDigestAuth(self.username, self.password)
self.auth = AsyncHTTPDigestAuth(self.username, self.password)
elif auth_type == "basic":
if _USE_HTTPX:
self.auth = httpx.BasicAuth(self.username, self.password)
Expand Down
1 change: 1 addition & 0 deletions caldav/calendarobjectresource.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,7 @@ async def _async_load_by_multiget(self) -> Self:
def _post_load_by_multiget(self, items):
if not items:
raise error.NotFoundError(self.url)
items = iter(items)
url_data = next(items, None)
if url_data is None:
## We shouldn't come here. Something is wrong.
Expand Down
61 changes: 45 additions & 16 deletions caldav/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,14 @@ def _create(

prop = dav.Prop()
display_name = None
if name:
# Some servers (e.g. Zimbra) use the DisplayName from the MKCALENDAR body
# as the calendar URL, ignoring the actual request path. When the server
# does not support setting a separate display name, omit it from the body so
# the request URL path is used as the calendar identifier.
supports_displayname = not self.client or self.client.features.is_supported(
"create-calendar.set-displayname"
)
if name and supports_displayname:
display_name = dav.DisplayName(name)
prop += [display_name]
if supported_calendar_component_set:
Expand All @@ -747,7 +754,7 @@ def _create(
# on setting the DisplayName on calendar creation
# (DAViCal, Zimbra, ...). Doing an attempt on explicitly setting the
# display name using PROPPATCH.
if name:
if display_name:
try:
self.set_properties([display_name])
except Exception:
Expand All @@ -766,7 +773,7 @@ async def _async_create(self, path, mkcol, method, name, display_name) -> None:
await self._query(root=mkcol, query_method=method, url=path, expected_return_value=201)

# COMPATIBILITY ISSUE - try to set display name explicitly
if name:
if display_name:
try:
await self.set_properties([display_name])
except Exception:
Expand Down Expand Up @@ -1080,6 +1087,7 @@ def _multiget(self, event_urls: Iterable[URL], raise_notfound: bool = False) ->
"""
get multiple events' data.
TODO: Does it overlap the _request_report_build_resultlist method
## WARNING: async logic is duplicated in _async_multiget — mirror any changes there
"""
if self.url is None:
raise ValueError("Unexpected value None for self.url")
Expand All @@ -1098,28 +1106,33 @@ def _multiget(self, event_urls: Iterable[URL], raise_notfound: bool = False) ->
for r in results:
yield (r, results[r][cdav.CalendarData.tag])

## Replace the last lines with
def _post_multiget(self, results: Iterable[tuple[str, str]]) -> list[_CC]:
"""Post-processing shared by multiget and _async_multiget_objects."""
return [
self._calendar_comp_class_by_data(data)(
self.client,
# Quote path to handle servers returning unencoded spaces (e.g., Zimbra)
url=self.url.join(quote(unquote(str(url)), safe="/:@")),
data=data,
parent=self,
)
for url, data in results
]

def multiget(self, event_urls: Iterable[URL], raise_notfound: bool = False) -> Iterable[_CC]:
"""
get multiple events' data
TODO: Does it overlap the _request_report_build_resultlist method?
@author mtorange@gmail.com (refactored by Tobias)
"""
results = self._multiget(event_urls, raise_notfound=raise_notfound)
for url, data in results:
# Quote path to handle servers returning unencoded spaces (e.g., Zimbra)
quoted_url = quote(unquote(str(url)), safe="/:@")
yield self._calendar_comp_class_by_data(data)(
self.client,
url=self.url.join(quoted_url),
data=data,
parent=self,
)
if self.is_async_client:
return self._async_multiget_objects(event_urls, raise_notfound=raise_notfound)
return self._post_multiget(self._multiget(event_urls, raise_notfound=raise_notfound))

async def _async_multiget(
self, event_urls: Iterable[URL], raise_notfound: bool = False
) -> list[tuple[str, str]]:
"""Async version of _multiget — returns a list of (url, data) tuples."""
## WARNING: sync logic is duplicated in _multiget — mirror any changes there
if self.url is None:
raise ValueError("Unexpected value None for self.url")

Expand All @@ -1134,13 +1147,29 @@ async def _async_multiget(
raise error.NotFoundError(f"Status {status} in {href}")
return [(r, results[r][cdav.CalendarData.tag]) for r in results]

async def _async_multiget_objects(
self, event_urls: Iterable[URL], raise_notfound: bool = False
) -> list[_CC]:
"""Async version of multiget."""
return self._post_multiget(
await self._async_multiget(event_urls, raise_notfound=raise_notfound)
)

def calendar_multiget(self, *largs, **kwargs):
"""
get multiple events' data
@author mtorange@gmail.com
(refactored by Tobias)
This is for backward compatibility. It may be removed in 3.0 or later release.
This is for backward compatibility. It may be removed in a later release.

.. deprecated::
Use :meth:`multiget` instead.
"""
warnings.warn(
"calendar_multiget is deprecated, use multiget instead",
DeprecationWarning,
stacklevel=2,
)
return list(self.multiget(*largs, **kwargs))

def date_search(
Expand Down
16 changes: 10 additions & 6 deletions caldav/compatibility_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class FeatureSet:
## TODO: in the future, templates for the principal URL, calendar URLs etc may also be added.
}
},
"url": {
"type": "client-hints",
},
"get-current-user-principal": {
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the DAV standard. Possibly observed missing on mail.ru, DavMail gateway and it is possible to configure the support in some sabre-based servers",
"links": ["https://datatracker.ietf.org/doc/html/rfc5397"],
Expand Down Expand Up @@ -177,6 +180,10 @@ class FeatureSet:
"default": {"support": "full"},
"links": ["https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.5"],
},
"save-load.mutable": {
"description": "A saved calendar object resource can be modified and PUT back to the server; the server accepts the update and returns the modified data on the next GET/REPORT. When 'unsupported', the server treats calendar objects as immutable after initial creation (e.g. Google Calendar's legacy CalDAV API). Replaces the old 'no_overwrite' compatibility flag.",
"default": {"support": "full"},
},
"search": {
"description": "calendar MUST support searching for objects using the REPORT method, as specified in RFC4791, section 7",
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-7"],
Expand Down Expand Up @@ -855,9 +862,6 @@ def dotted_feature_set_list(self, compact=False):
'vtodo-cannot-be-uncompleted':
"""If a VTODO object has been set with STATUS:COMPLETE, it's not possible to delete the COMPLTEDED attribute and change back to STATUS:IN-ACTION""",

'unique_calendar_ids':
"""For every test, generate a new and unique calendar id""",

'sticky_events':
"""Events should be deleted before the calendar is deleted, """
"""and/or deleting a calendar may not have immediate effect""",
Expand Down Expand Up @@ -961,7 +965,6 @@ def dotted_feature_set_list(self, compact=False):
'principal-search.by-name.self': {'support': 'unsupported'},
'principal-search': {'support': 'ungraceful'},
'search.time-range.open.start.duration': 'broken',
#'old_flags': ['unique_calendar_ids'],
## I'm surprised, I'm quite sure this was passing earlier. Caldav commit a98d50490b872e9b9d8e93e2e401c936ad193003, caldav server checker commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad
'search.combined-is-logical-and': False,
## Observed with Nextcloud 33: server delivers iTIP notification to the inbox AND
Expand Down Expand Up @@ -993,7 +996,9 @@ def dotted_feature_set_list(self, compact=False):
zimbra = {
'auto-connect.url': {'basepath': '/dav/'},
'delete-calendar': {'support': 'fragile', 'behaviour': 'may move to trashbin instead of deleting immediately'},
'save-load.get-by-url': {'support': 'fragile', 'behaviour': '404 most of the time - but sometimes 200. Weird, should be investigated more'},
## This is a zimbra bug when creating calendars with a display
## name. Now mitigated in the calendar creation code.
#'save-load.get-by-url': {'support': 'fragile', 'behaviour': '404 most of the time - but sometimes 200. Weird, should be investigated more'},
## Zimbra treats same-UID events across calendars as aliases of the same event
'save.duplicate-uid.cross-calendar': {'support': 'unsupported'},
'search.recurrences.expanded.exception': {'support': 'unsupported'}, ## TODO: verify
Expand Down Expand Up @@ -1156,7 +1161,6 @@ def dotted_feature_set_list(self, compact=False):

## See comments on https://github.com/python-caldav/caldav/issues/3
#icloud = [
# 'unique_calendar_ids',
# 'duplicate_in_other_calendar_with_same_uid_breaks',
# 'sticky_events',
# 'no_journal', ## it threw a 500 internal server error!
Expand Down
30 changes: 12 additions & 18 deletions docs/source/v3-migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
Migrating from caldav 2.x to 3.x
=========================================

v3.0 is mostly backward-compatible with v2.x. Existing code should
v3.x 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.
primer on the "best current usage practice" as of v3.x.

.. contents:: Contents
:local:
Expand Down Expand Up @@ -40,13 +38,11 @@ replace it with:
from caldav import Event, Todo, Calendar # OK

All public symbols that were in ``caldav.objects`` should remain available directly
from the ``caldav`` namespace
from the ``caldav`` namespace. Exceptions may apply.

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?
Earlier a wildcard import ``from caldav.objects import *`` was done into the ``caldav`` namespace. This has been removed. Imports should work, but corner cases may exists. If you have any issues, see :doc:`contact`.

Config-file parse errors now raise exceptions
---------------------------------------------
Expand Down Expand Up @@ -247,7 +243,7 @@ Rationale: Those methods are also querying the server actively, and not just loo
Accessing and editing calendar data
=====================================

This is the most significant new API in v3.0, addressing a long-standing
This is the most significant new API in v3.x, 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.
Expand All @@ -271,7 +267,7 @@ internal slot. Accessing one representation could silently invalidate another:
The 3.x Solution: read and edit methods
-----------------------------------------

v3.0 adds explicit read-only getters that always return **copies**, and
v3.x 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):
Expand Down Expand Up @@ -312,9 +308,7 @@ context-manager "borrow" methods that give exclusive, safe write access.

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.
representation raises ``RuntimeError``.

**The data representation remains the same:**

Expand Down Expand Up @@ -354,7 +348,7 @@ The interface for the string representation is still the same. Strings are immu
- Replace all data from a raw string


New in v3.0
New in v3.x
============

Async client
Expand Down Expand Up @@ -486,10 +480,10 @@ If the server gives a `retry-after`-header on 429 or 530 it will be respected, o

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)
=============================================
Deprecated in v3.x
==================

The following emit ``DeprecationWarning``:
The following emit ``DeprecationWarning`` and may be removed in v4.0:

* ``calendar.date_search()`` — use ``calendar.search()``
* ``client.principals()`` — use ``client.search_principals()``
Expand All @@ -498,7 +492,7 @@ The following emit ``DeprecationWarning``:
* ``.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:
The following are deprecated, do **not yet** emit warnings and may be removed in v5.0:

* All ``save_*`` methods → use ``add_*``
* All ``*_by_uid()`` methods → use ``get_*_by_uid()``
Expand Down
3 changes: 0 additions & 3 deletions examples/basic_usage_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,6 @@ def find_delete_calendar_demo(my_principal, calendar_name):
def add_stuff_to_calendar_demo(calendar):
"""
This demo adds some stuff to the calendar

Unfortunately the arguments that it's possible to pass to save_* is poorly documented.
https://github.com/python-caldav/caldav/issues/253
"""
## Add an event with some certain attributes
print("Saving an event")
Expand Down
Loading
Loading