Files
syn-chat-bot/.venv/lib/python3.9/site-packages/caldav/davobject.py
Hyungi Ahn c2257d3a86 fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:53:55 +09:00

431 lines
15 KiB
Python

import logging
import sys
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from urllib.parse import ParseResult
from urllib.parse import quote
from urllib.parse import SplitResult
from urllib.parse import unquote
from lxml import etree
try:
from typing import ClassVar, Optional, Union, Type
TimeStamp = Optional[Union[date, datetime]]
except:
pass
if TYPE_CHECKING:
from icalendar import vCalAddress
from .davclient import DAVClient
if sys.version_info < (3, 9):
from typing import Callable, Container, Iterable, Iterator, Sequence
from typing_extensions import DefaultDict, Literal
else:
from collections import defaultdict as DefaultDict
from collections.abc import Callable, Container, Iterable, Iterator, Sequence
from typing import Literal
if sys.version_info < (3, 11):
from typing_extensions import Self
else:
from typing import Self
from .elements import cdav, dav
from .elements.base import BaseElement
from .lib import error
from .lib.error import errmsg
from .lib.python_utilities import to_wire
from .lib.url import URL
_CC = TypeVar("_CC", bound="CalendarObjectResource")
log = logging.getLogger("caldav")
"""
This file contains one class, the DAVObject which is the base
class for Calendar, Principal, CalendarObjectResource (Event) and many
others. There is some code here for handling some of the DAV-related
communication, and the class lists some common methods that are shared
on all kind of objects. Library users should not need to know a lot
about the DAVObject class, should never need to initialize one, but
may encounter inheritated methods coming from this class.
"""
class DAVObject:
"""
Base class for all DAV objects. Can be instantiated by a client
and an absolute or relative URL, or from the parent object.
"""
id: Optional[str] = None
url: Optional[URL] = None
client: Optional["DAVClient"] = None
parent: Optional["DAVObject"] = None
name: Optional[str] = None
def __init__(
self,
client: Optional["DAVClient"] = None,
url: Union[str, ParseResult, SplitResult, URL, None] = None,
parent: Optional["DAVObject"] = None,
name: Optional[str] = None,
id: Optional[str] = None,
props=None,
**extra,
) -> None:
"""
Default constructor.
Args:
client: A DAVClient instance
url: The url for this object. May be a full URL or a relative URL.
parent: The parent object - used when creating objects
name: A displayname - to be removed at some point, see https://github.com/python-caldav/caldav/issues/128 for details
props: a dict with known properties for this object
id: The resource id (UID for an Event)
"""
if client is None and parent is not None:
client = parent.client
self.client = client
self.parent = parent
self.name = name
self.id = id
self.props = props or {}
self.extra_init_options = extra
# url may be a path relative to the caldav root
if client and url:
self.url = client.url.join(url)
elif url is None:
self.url = None
else:
self.url = URL.objectify(url)
@property
def canonical_url(self) -> str:
if self.url is None:
raise ValueError("Unexpected value None for self.url")
return str(self.url.canonical())
def children(self, type: Optional[str] = None) -> List[Tuple[URL, Any, Any]]:
"""List children, using a propfind (resourcetype) on the parent object,
at depth = 1.
TODO: This is old code, it's querying for DisplayName and
ResourceTypes prop and returning a tuple of those. Those two
are relatively arbitrary. I think it's mostly only calendars
having DisplayName, but it may make sense to ask for the
children of a calendar also as an alternative way to get all
events? It should be redone into a more generic method, and
it should probably return a dict rather than a tuple. We
should also look over to see if there is any code duplication.
"""
## Late import to avoid circular imports
from .collection import CalendarSet
c = []
depth = 1
if self.url is None:
raise ValueError("Unexpected value None for self.url")
props = [dav.DisplayName()]
multiprops = [dav.ResourceType()]
props_multiprops = props + multiprops
response = self._query_properties(props_multiprops, depth)
properties = response.expand_simple_props(
props=props, multi_value_props=multiprops
)
for path in properties:
resource_types = properties[path][dav.ResourceType.tag]
resource_name = properties[path][dav.DisplayName.tag]
if type is None or type in resource_types:
url = URL(path)
if url.hostname is None:
# Quote when path is not a full URL
path = quote(path)
# TODO: investigate the RFCs thoroughly - why does a "get
# members of this collection"-request also return the
# collection URL itself?
# And why is the strip_trailing_slash-method needed?
# The collection URL should always end with a slash according
# to RFC 2518, section 5.2.
if (isinstance(self, CalendarSet) and type == cdav.Calendar.tag) or (
self.url.canonical().strip_trailing_slash()
!= self.url.join(path).canonical().strip_trailing_slash()
):
c.append((self.url.join(path), resource_types, resource_name))
## TODO: return objects rather than just URLs, and include
## the properties we've already fetched
return c
def _query_properties(
self, props: Optional[Sequence[BaseElement]] = None, depth: int = 0
):
"""
This is an internal method for doing a propfind query. It's a
result of code-refactoring work, attempting to consolidate
similar-looking code into a common method.
"""
root = None
# build the propfind request
if props is not None and len(props) > 0:
prop = dav.Prop() + props
root = dav.Propfind() + prop
return self._query(root, depth)
def _query(
self,
root=None,
depth=0,
query_method="propfind",
url=None,
expected_return_value=None,
):
"""
This is an internal method for doing a query. It's a
result of code-refactoring work, attempting to consolidate
similar-looking code into a common method.
"""
body = ""
if root:
if hasattr(root, "xmlelement"):
body = etree.tostring(
root.xmlelement(),
encoding="utf-8",
xml_declaration=True,
pretty_print=error.debug_dump_communication,
)
else:
body = root
if url is None:
url = self.url
ret = getattr(self.client, query_method)(url, body, depth)
if ret.status == 404:
raise error.NotFoundError(errmsg(ret))
if (
expected_return_value is not None and ret.status != expected_return_value
) or ret.status >= 400:
## COMPATIBILITY HACK - see https://github.com/python-caldav/caldav/issues/309
## TODO: server quirks!
body = to_wire(body)
if (
ret.status == 500
and b"D:getetag" not in body
and b"<C:calendar-data" in body
):
body = body.replace(
b"<C:calendar-data", b"<D:getetag/><C:calendar-data"
)
return self._query(
body, depth, query_method, url, expected_return_value
)
raise error.exception_by_method[query_method](errmsg(ret))
return ret
def get_property(
self, prop: BaseElement, use_cached: bool = False, **passthrough
) -> Optional[str]:
"""
Wrapper for the :class:`get_properties`, when only one property is wanted
Args:
prop: the property to search for
use_cached: don't send anything to the server if we've asked before
Other parameters are sent directly to the :class:`get_properties` method
"""
## TODO: use_cached should probably be true
if use_cached:
if prop.tag in self.props:
return self.props[prop.tag]
foo = self.get_properties([prop], **passthrough)
return foo.get(prop.tag, None)
def get_properties(
self,
props: Optional[Sequence[BaseElement]] = None,
depth: int = 0,
parse_response_xml: bool = True,
parse_props: bool = True,
):
"""Get properties (PROPFIND) for this object.
With parse_response_xml and parse_props set to True a
best-attempt will be done on decoding the XML we get from the
server - but this works only for properties that don't have
complex types. With parse_response_xml set to False, a
DAVResponse object will be returned, and it's up to the caller
to decode. With parse_props set to false but
parse_response_xml set to true, xml elements will be returned
rather than values.
Args:
props: ``[dav.ResourceType(), dav.DisplayName(), ...]``
Returns:
``{proptag: value, ...}``
"""
from .collection import Principal ## late import to avoid cyclic dependencies
rc = None
response = self._query_properties(props, depth)
if not parse_response_xml:
return response
if not parse_props:
properties = response.find_objects_and_props()
else:
properties = response.expand_simple_props(props)
error.assert_(properties)
if self.url is None:
raise ValueError("Unexpected value None for self.url")
path = unquote(self.url.path)
if path.endswith("/"):
exchange_path = path[:-1]
else:
exchange_path = path + "/"
if path in properties:
rc = properties[path]
elif exchange_path in properties:
if not isinstance(self, Principal):
## Some caldav servers reports the URL for the current
## principal to end with / when doing a propfind for
## current-user-principal - I believe that's a bug,
## the principal is not a collection and should not
## end with /. (example in rfc5397 does not end with /).
## ... but it gets worse ... when doing a propfind on the
## principal, the href returned may be without the slash.
## Such inconsistency is clearly a bug.
log.warning(
"potential path handling problem with ending slashes. Path given: %s, path found: %s. %s"
% (path, exchange_path, error.ERR_FRAGMENT)
)
error.assert_(False)
rc = properties[exchange_path]
elif self.url in properties:
rc = properties[self.url]
elif "/principal/" in properties and path.endswith("/principal/"):
## Workaround for a known iCloud bug.
## The properties key is expected to be the same as the path.
## path is on the format /123456/principal/ but properties key is /principal/
## tests apparently passed post bc589093a34f0ed0ef489ad5e9cba048750c9837 and 3ee4e42e2fa8f78b71e5ffd1ef322e4007df7a60, even without this workaround
## TODO: should probably be investigated more.
## (observed also by others, ref https://github.com/python-caldav/caldav/issues/168)
rc = properties["/principal/"]
elif "//" in path and path.replace("//", "/") in properties:
## ref https://github.com/python-caldav/caldav/issues/302
## though, it would be nice to find the root cause,
## self.url should not contain double slashes in the first place
rc = properties[path.replace("//", "/")]
elif len(properties) == 1:
## Ref https://github.com/python-caldav/caldav/issues/191 ...
## let's be pragmatic and just accept whatever the server is
## throwing at us. But we'll log an error anyway.
log.warning(
"Possibly the server has a path handling problem, possibly the URL configured is wrong.\n"
"Path expected: %s, path found: %s %s.\n"
"Continuing, probably everything will be fine"
% (path, str(list(properties)), error.ERR_FRAGMENT)
)
rc = list(properties.values())[0]
else:
log.warning(
"Possibly the server has a path handling problem. Path expected: %s, paths found: %s %s"
% (path, str(list(properties)), error.ERR_FRAGMENT)
)
error.assert_(False)
if parse_props:
if rc is None:
raise ValueError("Unexpected value None for rc")
self.props.update(rc)
return rc
def set_properties(self, props: Optional[Any] = None) -> Self:
"""
Set properties (PROPPATCH) for this object.
* props = [dav.DisplayName('name'), ...]
Returns:
* self
"""
props = [] if props is None else props
prop = dav.Prop() + props
set = dav.Set() + prop
root = dav.PropertyUpdate() + set
r = self._query(root, query_method="proppatch")
statuses = r.tree.findall(".//" + dav.Status.tag)
for s in statuses:
if " 200 " not in s.text:
raise error.PropsetError(s.text)
return self
def save(self) -> Self:
"""
Save the object. This is an abstract method, that all classes
derived from DAVObject implement.
Returns:
* self
"""
raise NotImplementedError()
def delete(self) -> None:
"""
Delete the object.
"""
if self.url is not None:
if self.client is None:
raise ValueError("Unexpected value None for self.client")
r = self.client.delete(str(self.url))
# TODO: find out why we get 404
if r.status not in (200, 204, 404):
raise error.DeleteError(errmsg(r))
def get_display_name(self):
"""
Get display name (calendar, principal, ...more?)
"""
return self.get_property(dav.DisplayName(), use_cached=True)
def __str__(self) -> str:
try:
return (
str(self.get_property(dav.DisplayName(), use_cached=True)) or self.url
)
except:
return str(self.url)
def __repr__(self) -> str:
return "%s(%s)" % (self.__class__.__name__, self.url)