Skip to content

Commit 7785774

Browse files
committed
refactor: delete caldav/operations/ directory
As per docs/design/OPERATIONS_PROTOCOL_AUDIT.md: ~85% of the directory was dead code. The 7 production-used functions have been moved to their natural homes: - _quote_uid → calendarobjectresource.py (inlined, removes import) - _extract_calendars_from_propfind_results → collection.py - _extract_calendar_home_set_from_results + _sanitize_calendar_home_set_url → collection.py - _extract_calendar_id_from_url, _quote_url_path, _is_calendar_resource → collection.py - _build_search_xml_query, _filter_search_results, _collation_to_caldav → search.py davclient.py and async_davclient.py updated to import from collection.py. CalendarInfo dataclass moved to collection.py. All 6 test_operations_*.py files deleted (they only tested dead code; behaviour is covered by integration tests against the classes). 8 operations/ source files deleted (2,205 lines removed). prompt: Kill the operations directory ref the document Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> AI Prompts: claude-sonnet-4-6: Kill the operations directory ref the document
1 parent e68138f commit 7785774

19 files changed

Lines changed: 311 additions & 4083 deletions

caldav/async_davclient.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -968,12 +968,12 @@ async def get_calendars(self, principal: Optional["Principal"] = None) -> list["
968968
print(f"Calendar: {cal.get_display_name()}")
969969
"""
970970
from caldav.collection import Calendar
971-
from caldav.operations.calendarset_ops import (
972-
_extract_calendars_from_propfind_results as extract_calendars,
973-
)
974-
from caldav.operations.principal_ops import (
971+
from caldav.collection import (
975972
_extract_calendar_home_set_from_results as extract_home_set,
976973
)
974+
from caldav.collection import (
975+
_extract_calendars_from_propfind_results as extract_calendars,
976+
)
977977

978978
if principal is None:
979979
principal = await self.get_principal()

caldav/calendarobjectresource.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from collections import defaultdict
1919
from datetime import datetime, timedelta, timezone
2020
from typing import TYPE_CHECKING, Any, ClassVar, Optional
21-
from urllib.parse import ParseResult, SplitResult
21+
from urllib.parse import ParseResult, SplitResult, quote
2222

2323
import icalendar
2424
from dateutil.rrule import rrulestr
@@ -47,11 +47,19 @@
4747
from .lib.error import errmsg
4848
from .lib.python_utilities import to_normal_str, to_unicode, to_wire
4949
from .lib.url import URL
50-
from .operations.calendarobject_ops import _quote_uid
5150

5251
log = logging.getLogger("caldav")
5352

5453

54+
def _quote_uid(uid: str) -> str:
55+
"""URL-quote a UID for use in a CalDAV object URL.
56+
57+
Slashes are double-quoted (replaced with %2F before percent-encoding)
58+
per https://github.com/python-caldav/caldav/issues/143.
59+
"""
60+
return quote(uid.replace("/", "%2F"))
61+
62+
5563
class CalendarObjectResource(DAVObject):
5664
"""Ref RFC 4791, section 4.1, a "Calendar Object Resource" can be an
5765
event, a todo-item, a journal entry, or a free/busy entry

caldav/collection.py

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
import logging
1414
import uuid
1515
import warnings
16+
from dataclasses import dataclass
1617
from datetime import date as _date
1718
from datetime import datetime, timezone
1819
from time import sleep
1920
from typing import TYPE_CHECKING, Any, Optional, TypeVar
20-
from urllib.parse import ParseResult, SplitResult, quote, unquote
21+
from urllib.parse import ParseResult, SplitResult, quote, unquote, urlparse, urlunparse
2122

2223
import icalendar
2324

@@ -48,17 +49,96 @@
4849
log = logging.getLogger("caldav")
4950

5051

52+
# ---------------------------------------------------------------------------
53+
# Helpers for extracting calendar / principal info from PROPFIND results.
54+
# These were previously in caldav/operations/calendarset_ops.py and
55+
# caldav/operations/principal_ops.py.
56+
# ---------------------------------------------------------------------------
57+
58+
59+
@dataclass
60+
class CalendarInfo:
61+
"""Data for a calendar extracted from a PROPFIND response."""
62+
63+
url: str
64+
cal_id: str | None
65+
name: str | None
66+
resource_types: list[str]
67+
68+
69+
def _extract_calendar_id_from_url(url: str) -> str | None:
70+
try:
71+
parts = str(url).rstrip("/").split("/")
72+
if parts:
73+
cal_id = parts[-1]
74+
if cal_id:
75+
return cal_id
76+
except Exception:
77+
log.error(f"Calendar has unexpected url {url}")
78+
return None
79+
80+
81+
def _quote_url_path(url: str) -> str:
82+
"""Quote the path component of a URL to handle unencoded spaces (e.g. Zimbra)."""
83+
parsed = urlparse(url)
84+
quoted_path = quote(unquote(parsed.path), safe="/@")
85+
return urlunparse(parsed._replace(path=quoted_path))
86+
87+
88+
def _is_calendar_resource(properties: dict[str, Any]) -> bool:
89+
rt = properties.get("{DAV:}resourcetype", [])
90+
if not isinstance(rt, list):
91+
rt = [rt] if rt else []
92+
return "{urn:ietf:params:xml:ns:caldav}calendar" in rt
93+
94+
95+
def _extract_calendars_from_propfind_results(results: list[Any] | None) -> list[CalendarInfo]:
96+
"""Extract CalendarInfo objects from a list of PropfindResult objects."""
97+
calendars = []
98+
for result in results or []:
99+
if not _is_calendar_resource(result.properties):
100+
continue
101+
url = _quote_url_path(result.href)
102+
name = result.properties.get("{DAV:}displayname")
103+
cal_id = _extract_calendar_id_from_url(url)
104+
if not cal_id:
105+
continue
106+
calendars.append(
107+
CalendarInfo(
108+
url=url,
109+
cal_id=cal_id,
110+
name=name,
111+
resource_types=result.properties.get("{DAV:}resourcetype", []),
112+
)
113+
)
114+
return calendars
115+
116+
117+
def _sanitize_calendar_home_set_url(url: str | None) -> str | None:
118+
"""Quote @ in owncloud-style URLs that are not full URLs."""
119+
if url is None:
120+
return None
121+
if "@" in url and "://" not in url and "%40" not in url:
122+
return quote(url)
123+
return url
124+
125+
126+
def _extract_calendar_home_set_from_results(results: list[Any] | None) -> str | None:
127+
"""Extract calendar-home-set URL from a list of PropfindResult objects."""
128+
for result in results or []:
129+
home_set = result.properties.get("{urn:ietf:params:xml:ns:caldav}calendar-home-set")
130+
if home_set:
131+
return _sanitize_calendar_home_set_url(home_set)
132+
return None
133+
134+
51135
class CalendarSet(DAVObject):
52136
"""
53137
A CalendarSet is a set of calendars.
54138
"""
55139

56140
def _calendars_from_results(self, results) -> list["Calendar"]:
57141
"""Convert PropfindResult list into Calendar objects."""
58-
from caldav.operations.calendarset_ops import (
59-
_extract_calendars_from_propfind_results,
60-
)
61-
62142
calendar_infos = _extract_calendars_from_propfind_results(results)
63143
return [
64144
Calendar(client=self.client, url=info.url, name=info.name, id=info.cal_id, parent=self)

caldav/davclient.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,12 +501,12 @@ def get_calendars(self, principal: Principal | None = None) -> list[Calendar]:
501501
for cal in calendars:
502502
print(f"Calendar: {cal.get_display_name()}")
503503
"""
504-
from caldav.operations.calendarset_ops import (
505-
_extract_calendars_from_propfind_results as extract_calendars,
506-
)
507-
from caldav.operations.principal_ops import (
504+
from caldav.collection import (
508505
_extract_calendar_home_set_from_results as extract_home_set,
509506
)
507+
from caldav.collection import (
508+
_extract_calendars_from_propfind_results as extract_calendars,
509+
)
510510

511511
if principal is None:
512512
principal = self.principal()

caldav/operations/__init__.py

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)