Skip to content

Commit d131af0

Browse files
tobixenclaude
andcommitted
fix: export get_calendar/get_calendars from caldav.aio
The async factory functions already existed in caldav.async_davclient but were not re-exported from caldav.aio, leaving no parity with the sync API where caldav.get_calendars / caldav.get_calendar are available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 376c580 commit d131af0

3 files changed

Lines changed: 63 additions & 26 deletions

File tree

caldav/aio.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""
2828

2929
# Import the async client (this is truly async)
30-
from caldav.async_davclient import AsyncDAVClient, AsyncDAVResponse
30+
from caldav.async_davclient import AsyncDAVClient, AsyncDAVResponse, get_calendar, get_calendars
3131
from caldav.async_davclient import get_davclient as get_async_davclient
3232
from caldav.calendarobjectresource import CalendarObjectResource, Event, FreeBusy, Journal, Todo
3333
from caldav.collection import (
@@ -61,6 +61,9 @@
6161
"AsyncDAVClient",
6262
"AsyncDAVResponse",
6363
"get_async_davclient",
64+
# Factory functions (async equivalents of caldav.get_calendar / get_calendars)
65+
"get_calendar",
66+
"get_calendars",
6467
# Base objects (unified dual-mode)
6568
"DAVObject",
6669
"CalendarObjectResource",

caldav/async_davclient.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,7 @@ async def get_calendars(
12451245
calendar_name: Any | None = None,
12461246
raise_errors: bool = False,
12471247
**kwargs: Any,
1248-
) -> list["Calendar"]:
1248+
) -> "CalendarCollection":
12491249
"""
12501250
Get calendars from a CalDAV server asynchronously.
12511251
@@ -1258,15 +1258,14 @@ async def get_calendars(
12581258
**kwargs: Connection parameters (url, username, password, etc.)
12591259
12601260
Returns:
1261-
List of Calendar objects matching the criteria.
1261+
:class:`~caldav.base_client.CalendarCollection` of matching calendars.
1262+
Use as an async context manager to auto-close the connection::
12621263
1263-
Example::
1264-
1265-
from caldav.async_davclient import get_calendars
1266-
1267-
calendars = await get_calendars(url="...", username="...", password="...")
1264+
async with await aio.get_calendars() as calendars:
1265+
for cal in calendars:
1266+
print(await cal.get_display_name())
12681267
"""
1269-
from caldav.base_client import _normalize_to_list
1268+
from caldav.base_client import CalendarCollection, _normalize_to_list
12701269

12711270
def _try(coro_result, errmsg):
12721271
"""Handle errors based on raise_errors flag."""
@@ -1282,13 +1281,13 @@ def _try(coro_result, errmsg):
12821281
if raise_errors:
12831282
raise
12841283
log.error(f"Failed to create async client: {e}")
1285-
return []
1284+
return CalendarCollection()
12861285

12871286
try:
12881287
principal = await client.get_principal()
12891288
if not principal:
12901289
_try(None, "getting principal")
1291-
return []
1290+
return CalendarCollection(client=client)
12921291

12931292
calendars = []
12941293
calendar_urls = _normalize_to_list(calendar_url)
@@ -1332,31 +1331,34 @@ def _try(coro_result, errmsg):
13321331
if raise_errors:
13331332
raise
13341333

1335-
return calendars
1334+
return CalendarCollection(calendars, client=client)
13361335

1337-
finally:
1338-
# Don't close the client - let the caller manage its lifecycle
1339-
pass
1336+
except Exception:
1337+
await client.__aexit__(None, None, None)
1338+
raise
13401339

13411340

1342-
async def get_calendar(**kwargs: Any) -> Optional["Calendar"]:
1341+
async def get_calendar(**kwargs: Any) -> "CalendarResult":
13431342
"""
13441343
Get a single calendar from a CalDAV server asynchronously.
13451344
13461345
This is a convenience function for the common case where only one
1347-
calendar is needed. It returns the first matching calendar or None.
1346+
calendar is needed. Use as an async context manager to auto-close
1347+
the connection::
1348+
1349+
async with await aio.get_calendar() as cal:
1350+
events = await cal.search(event=True)
13481351
13491352
Args:
13501353
Same as :func:`get_calendars`.
13511354
13521355
Returns:
1353-
A single Calendar object, or None if no calendars found.
1354-
1355-
Example::
1356-
1357-
from caldav.async_davclient import get_calendar
1358-
1359-
calendar = await get_calendar(calendar_name="Work", url="...", ...)
1356+
:class:`~caldav.base_client.CalendarResult` wrapping the first
1357+
matching calendar (or None). Behaves like the calendar via
1358+
``__getattr__`` delegation when not used as a context manager.
13601359
"""
1361-
calendars = await get_calendars(**kwargs)
1362-
return calendars[0] if calendars else None
1360+
from caldav.base_client import CalendarResult
1361+
1362+
collection = await get_calendars(**kwargs)
1363+
cal = collection[0] if collection else None
1364+
return CalendarResult(cal, client=collection.client)

caldav/base_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,26 @@ def __exit__(self, exc_type, exc_val, exc_tb):
328328
self[0].client.__exit__(exc_type, exc_val, exc_tb)
329329
return False
330330

331+
async def __aenter__(self):
332+
return self
333+
334+
async def __aexit__(self, exc_type, exc_val, exc_tb):
335+
seen: set[int] = set()
336+
for c in self._clients:
337+
if id(c) not in seen:
338+
if hasattr(c, "__aexit__"):
339+
await c.__aexit__(exc_type, exc_val, exc_tb)
340+
else:
341+
c.__exit__(exc_type, exc_val, exc_tb)
342+
seen.add(id(c))
343+
if not self._clients and self:
344+
c = self[0].client
345+
if hasattr(c, "__aexit__"):
346+
await c.__aexit__(exc_type, exc_val, exc_tb)
347+
else:
348+
c.__exit__(exc_type, exc_val, exc_tb)
349+
return False
350+
331351
def close(self):
332352
"""Close all underlying DAV client connections."""
333353
seen: set[int] = set()
@@ -390,6 +410,18 @@ def __exit__(self, exc_type, exc_val, exc_tb):
390410
client.__exit__(exc_type, exc_val, exc_tb)
391411
return False
392412

413+
async def __aenter__(self):
414+
return self._calendar
415+
416+
async def __aexit__(self, exc_type, exc_val, exc_tb):
417+
client = self.client
418+
if client:
419+
if hasattr(client, "__aexit__"):
420+
await client.__aexit__(exc_type, exc_val, exc_tb)
421+
else:
422+
client.__exit__(exc_type, exc_val, exc_tb)
423+
return False
424+
393425
def close(self):
394426
"""Close the underlying DAV client connection."""
395427
client = self.client

0 commit comments

Comments
 (0)