mirror of
https://github.com/Kozea/Radicale.git
synced 2025-09-15 20:36:55 +00:00
More type hints
This commit is contained in:
parent
12fe5ce637
commit
cecb17df03
51 changed files with 1374 additions and 957 deletions
|
@ -23,37 +23,44 @@ Take a look at the class ``BaseCollection`` if you want to implement your own.
|
|||
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from hashlib import sha256
|
||||
from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set,
|
||||
Tuple, Union, overload)
|
||||
|
||||
import pkg_resources
|
||||
import vobject
|
||||
|
||||
from radicale import utils
|
||||
from radicale import config
|
||||
from radicale import item as radicale_item
|
||||
from radicale import types, utils
|
||||
from radicale.item import filter as radicale_filter
|
||||
|
||||
INTERNAL_TYPES = ("multifilesystem",)
|
||||
INTERNAL_TYPES: Sequence[str] = ("multifilesystem",)
|
||||
|
||||
CACHE_DEPS = ("radicale", "vobject", "python-dateutil",)
|
||||
CACHE_VERSION = (";".join(pkg_resources.get_distribution(pkg).version
|
||||
for pkg in CACHE_DEPS) + ";").encode()
|
||||
CACHE_DEPS: Sequence[str] = ("radicale", "vobject", "python-dateutil",)
|
||||
CACHE_VERSION: bytes = "".join(
|
||||
"%s=%s;" % (pkg, pkg_resources.get_distribution(pkg).version)
|
||||
for pkg in CACHE_DEPS).encode()
|
||||
|
||||
|
||||
def load(configuration):
|
||||
def load(configuration: "config.Configuration") -> "BaseStorage":
|
||||
"""Load the storage module chosen in configuration."""
|
||||
return utils.load_plugin(
|
||||
INTERNAL_TYPES, "storage", "Storage", configuration)
|
||||
return utils.load_plugin(INTERNAL_TYPES, "storage", "Storage", BaseStorage,
|
||||
configuration)
|
||||
|
||||
|
||||
class ComponentExistsError(ValueError):
|
||||
def __init__(self, path):
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
message = "Component already exists: %r" % path
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComponentNotFoundError(ValueError):
|
||||
def __init__(self, path):
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
message = "Component doesn't exist: %r" % path
|
||||
super().__init__(message)
|
||||
|
||||
|
@ -61,47 +68,58 @@ class ComponentNotFoundError(ValueError):
|
|||
class BaseCollection:
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
def path(self) -> str:
|
||||
"""The sanitized path of the collection without leading or
|
||||
trailing ``/``."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
def owner(self) -> str:
|
||||
"""The owner of the collection."""
|
||||
return self.path.split("/", maxsplit=1)[0]
|
||||
|
||||
@property
|
||||
def is_principal(self):
|
||||
def is_principal(self) -> bool:
|
||||
"""Collection is a principal."""
|
||||
return bool(self.path) and "/" not in self.path
|
||||
|
||||
@property
|
||||
def etag(self):
|
||||
def etag(self) -> str:
|
||||
"""Encoded as quoted-string (see RFC 2616)."""
|
||||
etag = sha256()
|
||||
for item in self.get_all():
|
||||
assert item.href
|
||||
etag.update((item.href + "/" + item.etag).encode())
|
||||
etag.update(json.dumps(self.get_meta(), sort_keys=True).encode())
|
||||
return '"%s"' % etag.hexdigest()
|
||||
|
||||
def sync(self, old_token=None):
|
||||
@property
|
||||
def tag(self) -> str:
|
||||
"""The tag of the collection."""
|
||||
return self.get_meta("tag") or ""
|
||||
|
||||
def sync(self, old_token: str = "") -> Tuple[str, Iterable[str]]:
|
||||
"""Get the current sync token and changed items for synchronization.
|
||||
|
||||
``old_token`` an old sync token which is used as the base of the
|
||||
delta update. If sync token is missing, all items are returned.
|
||||
delta update. If sync token is empty, all items are returned.
|
||||
ValueError is raised for invalid or old tokens.
|
||||
|
||||
WARNING: This simple default implementation treats all sync-token as
|
||||
invalid.
|
||||
|
||||
"""
|
||||
def hrefs_iter() -> Iterator[str]:
|
||||
for item in self.get_all():
|
||||
assert item.href
|
||||
yield item.href
|
||||
token = "http://radicale.org/ns/sync/%s" % self.etag.strip("\"")
|
||||
if old_token:
|
||||
raise ValueError("Sync token are not supported")
|
||||
return token, (item.href for item in self.get_all())
|
||||
return token, hrefs_iter()
|
||||
|
||||
def get_multi(self, hrefs):
|
||||
def get_multi(self, hrefs: Iterable[str]
|
||||
) -> Iterable[Tuple[str, Optional["radicale_item.Item"]]]:
|
||||
"""Fetch multiple items.
|
||||
|
||||
It's not required to return the requested items in the correct order.
|
||||
|
@ -113,11 +131,12 @@ class BaseCollection:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_all(self):
|
||||
def get_all(self) -> Iterable["radicale_item.Item"]:
|
||||
"""Fetch all items."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_filtered(self, filters):
|
||||
def get_filtered(self, filters: Iterable[ET.Element]
|
||||
) -> Iterable[Tuple["radicale_item.Item", bool]]:
|
||||
"""Fetch all items with optional filtering.
|
||||
|
||||
This can largely improve performance of reports depending on
|
||||
|
@ -128,32 +147,31 @@ class BaseCollection:
|
|||
matched.
|
||||
|
||||
"""
|
||||
if not self.tag:
|
||||
return
|
||||
tag, start, end, simple = radicale_filter.simplify_prefilters(
|
||||
filters, collection_tag=self.get_meta("tag"))
|
||||
filters, self.tag)
|
||||
for item in self.get_all():
|
||||
if tag:
|
||||
if tag != item.component_name:
|
||||
continue
|
||||
istart, iend = item.time_range
|
||||
if istart >= end or iend <= start:
|
||||
continue
|
||||
item_simple = simple and (start <= istart or iend <= end)
|
||||
else:
|
||||
item_simple = simple
|
||||
yield item, item_simple
|
||||
if tag is not None and tag != item.component_name:
|
||||
continue
|
||||
istart, iend = item.time_range
|
||||
if istart >= end or iend <= start:
|
||||
continue
|
||||
yield item, simple and (start <= istart or iend <= end)
|
||||
|
||||
def has_uid(self, uid):
|
||||
def has_uid(self, uid: str) -> bool:
|
||||
"""Check if a UID exists in the collection."""
|
||||
for item in self.get_all():
|
||||
if item.uid == uid:
|
||||
return True
|
||||
return False
|
||||
|
||||
def upload(self, href, item):
|
||||
def upload(self, href: str, item: "radicale_item.Item") -> (
|
||||
"radicale_item.Item"):
|
||||
"""Upload a new or replace an existing item."""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, href=None):
|
||||
def delete(self, href: Optional[str] = None) -> None:
|
||||
"""Delete an item.
|
||||
|
||||
When ``href`` is ``None``, delete the collection.
|
||||
|
@ -161,7 +179,14 @@ class BaseCollection:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_meta(self, key=None):
|
||||
@overload
|
||||
def get_meta(self, key: None = None) -> Mapping[str, str]: ...
|
||||
|
||||
@overload
|
||||
def get_meta(self, key: str) -> Optional[str]: ...
|
||||
|
||||
def get_meta(self, key: Optional[str] = None
|
||||
) -> Union[Mapping[str, str], Optional[str]]:
|
||||
"""Get metadata value for collection.
|
||||
|
||||
Return the value of the property ``key``. If ``key`` is ``None`` return
|
||||
|
@ -170,7 +195,7 @@ class BaseCollection:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_meta(self, props):
|
||||
def set_meta(self, props: Mapping[str, str]) -> None:
|
||||
"""Set metadata values for collection.
|
||||
|
||||
``props`` a dict with values for properties.
|
||||
|
@ -179,16 +204,16 @@ class BaseCollection:
|
|||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def last_modified(self):
|
||||
def last_modified(self) -> str:
|
||||
"""Get the HTTP-datetime of when the collection was modified."""
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> str:
|
||||
"""Get the unicode string representing the whole collection."""
|
||||
if self.get_meta("tag") == "VCALENDAR":
|
||||
if self.tag == "VCALENDAR":
|
||||
in_vcalendar = False
|
||||
vtimezones = ""
|
||||
included_tzids = set()
|
||||
included_tzids: Set[str] = set()
|
||||
vtimezone = []
|
||||
tzid = None
|
||||
components = ""
|
||||
|
@ -216,6 +241,7 @@ class BaseCollection:
|
|||
elif depth == 2 and line.startswith("END:"):
|
||||
if tzid is None or tzid not in included_tzids:
|
||||
vtimezones += "".join(vtimezone)
|
||||
if tzid is not None:
|
||||
included_tzids.add(tzid)
|
||||
vtimezone.clear()
|
||||
tzid = None
|
||||
|
@ -240,13 +266,14 @@ class BaseCollection:
|
|||
return (template[:template_insert_pos] +
|
||||
vtimezones + components +
|
||||
template[template_insert_pos:])
|
||||
if self.get_meta("tag") == "VADDRESSBOOK":
|
||||
if self.tag == "VADDRESSBOOK":
|
||||
return "".join((item.serialize() for item in self.get_all()))
|
||||
return ""
|
||||
|
||||
|
||||
class BaseStorage:
|
||||
def __init__(self, configuration):
|
||||
|
||||
def __init__(self, configuration: "config.Configuration") -> None:
|
||||
"""Initialize BaseStorage.
|
||||
|
||||
``configuration`` see ``radicale.config`` module.
|
||||
|
@ -256,7 +283,8 @@ class BaseStorage:
|
|||
"""
|
||||
self.configuration = configuration
|
||||
|
||||
def discover(self, path, depth="0"):
|
||||
def discover(self, path: str, depth: str = "0") -> Iterable[
|
||||
"types.CollectionOrItem"]:
|
||||
"""Discover a list of collections under the given ``path``.
|
||||
|
||||
``path`` is sanitized.
|
||||
|
@ -272,7 +300,8 @@ class BaseStorage:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def move(self, item, to_collection, to_href):
|
||||
def move(self, item: "radicale_item.Item", to_collection: BaseCollection,
|
||||
to_href: str) -> None:
|
||||
"""Move an object.
|
||||
|
||||
``item`` is the item to move.
|
||||
|
@ -285,7 +314,10 @@ class BaseStorage:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_collection(self, href, items=None, props=None):
|
||||
def create_collection(
|
||||
self, href: str,
|
||||
items: Optional[Iterable["radicale_item.Item"]] = None,
|
||||
props: Optional[Mapping[str, str]] = None) -> BaseCollection:
|
||||
"""Create a collection.
|
||||
|
||||
``href`` is the sanitized path.
|
||||
|
@ -298,15 +330,14 @@ class BaseStorage:
|
|||
|
||||
``props`` are metadata values for the collection.
|
||||
|
||||
``props["tag"]`` is the type of collection (VCALENDAR or
|
||||
VADDRESSBOOK). If the key ``tag`` is missing, it is guessed from the
|
||||
collection.
|
||||
``props["tag"]`` is the type of collection (VCALENDAR or VADDRESSBOOK).
|
||||
If the key ``tag`` is missing, ``items`` is ignored.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@contextlib.contextmanager
|
||||
def acquire_lock(self, mode, user=None):
|
||||
@types.contextmanager
|
||||
def acquire_lock(self, mode: str, user: str = "") -> Iterator[None]:
|
||||
"""Set a context manager to lock the whole storage.
|
||||
|
||||
``mode`` must either be "r" for shared access or "w" for exclusive
|
||||
|
@ -317,6 +348,6 @@ class BaseStorage:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def verify(self):
|
||||
def verify(self) -> bool:
|
||||
"""Check the storage for errors."""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
|
@ -72,14 +73,10 @@ class CollectionCacheMixin:
|
|||
"item")
|
||||
content = self._item_cache_content(item, cache_hash)
|
||||
self._storage._makedirs_synced(cache_folder)
|
||||
try:
|
||||
# Race: Other processes might have created and locked the
|
||||
# file.
|
||||
with self._atomic_write(os.path.join(cache_folder, href),
|
||||
"wb") as f:
|
||||
pickle.dump(content, f)
|
||||
except PermissionError:
|
||||
pass
|
||||
# Race: Other processes might have created and locked the file.
|
||||
with contextlib.suppress(PermissionError), self._atomic_write(
|
||||
os.path.join(cache_folder, href), "wb") as f:
|
||||
pickle.dump(content, f)
|
||||
return content
|
||||
|
||||
def _load_item_cache(self, href, input_hash):
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import vobject
|
||||
|
||||
from radicale import item as radicale_item
|
||||
import radicale.item as radicale_item
|
||||
from radicale import pathutils
|
||||
from radicale.log import logger
|
||||
|
||||
|
@ -63,7 +64,7 @@ class CollectionGetMixin:
|
|||
return None
|
||||
except PermissionError:
|
||||
# Windows raises ``PermissionError`` when ``path`` is a directory
|
||||
if (os.name == "nt" and
|
||||
if (sys.platform == "win32" and
|
||||
os.path.isdir(path) and os.access(path, os.R_OK)):
|
||||
return None
|
||||
raise
|
||||
|
@ -83,10 +84,10 @@ class CollectionGetMixin:
|
|||
self._load_item_cache(href, input_hash)
|
||||
if input_hash != cache_hash:
|
||||
try:
|
||||
vobject_items = tuple(vobject.readComponents(
|
||||
vobject_items = list(vobject.readComponents(
|
||||
raw_text.decode(self._encoding)))
|
||||
radicale_item.check_and_sanitize_items(
|
||||
vobject_items, tag=self.get_meta("tag"))
|
||||
vobject_items, tag=self.tag)
|
||||
vobject_item, = vobject_items
|
||||
temp_item = radicale_item.Item(
|
||||
collection=self, vobject_item=vobject_item)
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import binascii
|
||||
import contextlib
|
||||
import os
|
||||
import pickle
|
||||
|
||||
from radicale import item as radicale_item
|
||||
import radicale.item as radicale_item
|
||||
from radicale import pathutils
|
||||
from radicale.log import logger
|
||||
|
||||
|
@ -53,13 +54,10 @@ class CollectionHistoryMixin:
|
|||
self._storage._makedirs_synced(history_folder)
|
||||
history_etag = radicale_item.get_etag(
|
||||
history_etag + "/" + etag).strip("\"")
|
||||
try:
|
||||
# Race: Other processes might have created and locked the file.
|
||||
with self._atomic_write(os.path.join(history_folder, href),
|
||||
"wb") as f:
|
||||
pickle.dump([etag, history_etag], f)
|
||||
except PermissionError:
|
||||
pass
|
||||
# Race: Other processes might have created and locked the file.
|
||||
with contextlib.suppress(PermissionError), self._atomic_write(
|
||||
os.path.join(history_folder, href), "wb") as f:
|
||||
pickle.dump([etag, history_etag], f)
|
||||
return history_etag
|
||||
|
||||
def _get_deleted_history_hrefs(self):
|
||||
|
@ -67,7 +65,7 @@ class CollectionHistoryMixin:
|
|||
history cache."""
|
||||
history_folder = os.path.join(self._filesystem_path,
|
||||
".Radicale.cache", "history")
|
||||
try:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
for entry in os.scandir(history_folder):
|
||||
href = entry.name
|
||||
if not pathutils.is_safe_filesystem_path_component(href):
|
||||
|
@ -75,8 +73,6 @@ class CollectionHistoryMixin:
|
|||
if os.path.isfile(os.path.join(self._filesystem_path, href)):
|
||||
continue
|
||||
yield href
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _clean_history(self):
|
||||
# Delete all expired history entries of deleted items.
|
||||
|
|
|
@ -22,6 +22,7 @@ import os
|
|||
import shlex
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from radicale import pathutils
|
||||
from radicale.log import logger
|
||||
|
@ -48,7 +49,7 @@ class StorageLockMixin:
|
|||
self._lock = pathutils.RwLock(lock_path)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def acquire_lock(self, mode, user=None):
|
||||
def acquire_lock(self, mode, user=""):
|
||||
with self._lock.acquire(mode):
|
||||
yield
|
||||
# execute hook
|
||||
|
@ -66,7 +67,7 @@ class StorageLockMixin:
|
|||
if os.name == "posix":
|
||||
# Process group is also used to identify child processes
|
||||
popen_kwargs["preexec_fn"] = os.setpgrp
|
||||
elif os.name == "nt":
|
||||
elif sys.platform == "win32":
|
||||
popen_kwargs["creationflags"] = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP)
|
||||
command = hook % {"user": shlex.quote(user or "Anonymous")}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
from radicale import item as radicale_item
|
||||
import radicale.item as radicale_item
|
||||
|
||||
|
||||
class CollectionMetaMixin:
|
||||
|
@ -35,14 +35,15 @@ class CollectionMetaMixin:
|
|||
try:
|
||||
try:
|
||||
with open(self._props_path, encoding=self._encoding) as f:
|
||||
self._meta_cache = json.load(f)
|
||||
temp_meta = json.load(f)
|
||||
except FileNotFoundError:
|
||||
self._meta_cache = {}
|
||||
radicale_item.check_and_sanitize_props(self._meta_cache)
|
||||
temp_meta = {}
|
||||
self._meta_cache = radicale_item.check_and_sanitize_props(
|
||||
temp_meta)
|
||||
except ValueError as e:
|
||||
raise RuntimeError("Failed to load properties of collection "
|
||||
"%r: %s" % (self.path, e)) from e
|
||||
return self._meta_cache.get(key) if key else self._meta_cache
|
||||
return self._meta_cache if key is None else self._meta_cache.get(key)
|
||||
|
||||
def set_meta(self, props):
|
||||
with self._atomic_write(self._props_path, "w") as f:
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import contextlib
|
||||
import itertools
|
||||
import os
|
||||
import pickle
|
||||
|
@ -25,7 +26,7 @@ from radicale.log import logger
|
|||
|
||||
|
||||
class CollectionSyncMixin:
|
||||
def sync(self, old_token=None):
|
||||
def sync(self, old_token=""):
|
||||
# The sync token has the form http://radicale.org/ns/sync/TOKEN_NAME
|
||||
# where TOKEN_NAME is the sha256 hash of all history etags of present
|
||||
# and past items of the collection.
|
||||
|
@ -37,7 +38,7 @@ class CollectionSyncMixin:
|
|||
return False
|
||||
return True
|
||||
|
||||
old_token_name = None
|
||||
old_token_name = ""
|
||||
if old_token:
|
||||
# Extract the token name from the sync token
|
||||
if not old_token.startswith("http://radicale.org/ns/sync/"):
|
||||
|
@ -78,10 +79,9 @@ class CollectionSyncMixin:
|
|||
"Failed to load stored sync token %r in %r: %s",
|
||||
old_token_name, self.path, e, exc_info=True)
|
||||
# Delete the damaged file
|
||||
try:
|
||||
with contextlib.suppress(FileNotFoundError,
|
||||
PermissionError):
|
||||
os.remove(old_token_path)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
raise ValueError("Token not found: %r" % old_token)
|
||||
# write the new token state or update the modification time of
|
||||
# existing token state
|
||||
|
@ -101,11 +101,9 @@ class CollectionSyncMixin:
|
|||
self._clean_history()
|
||||
else:
|
||||
# Try to update the modification time
|
||||
try:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
# Race: Another process might have deleted the file.
|
||||
os.utime(token_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
changes = []
|
||||
# Find all new, changed and deleted (that are still in the item cache)
|
||||
# items
|
||||
|
|
|
@ -18,8 +18,9 @@
|
|||
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
from radicale import item as radicale_item
|
||||
import radicale.item as radicale_item
|
||||
from radicale import pathutils
|
||||
|
||||
|
||||
|
@ -63,7 +64,7 @@ class CollectionUploadMixin:
|
|||
"Failed to store item %r in temporary collection %r: %s" %
|
||||
(uid, self.path, e)) from e
|
||||
href_candidate_funtions = []
|
||||
if os.name in ("nt", "posix"):
|
||||
if os.name == "posix" or sys.platform == "win32":
|
||||
href_candidate_funtions.append(
|
||||
lambda: uid if uid.lower().endswith(suffix.lower())
|
||||
else uid + suffix)
|
||||
|
@ -88,7 +89,7 @@ class CollectionUploadMixin:
|
|||
except OSError as e:
|
||||
if href_candidate_funtions and (
|
||||
os.name == "posix" and e.errno == 22 or
|
||||
os.name == "nt" and e.errno == 123):
|
||||
sys.platform == "win32" and e.errno == 123):
|
||||
continue
|
||||
raise
|
||||
with f:
|
||||
|
|
|
@ -67,8 +67,8 @@ class StorageVerifyMixin:
|
|||
item.href, sane_path)
|
||||
if item_errors == saved_item_errors:
|
||||
collection.sync()
|
||||
if has_child_collections and collection.get_meta("tag"):
|
||||
if has_child_collections and collection.tag:
|
||||
logger.error("Invalid collection %r: %r must not have "
|
||||
"child collections", sane_path,
|
||||
collection.get_meta("tag"))
|
||||
collection.tag)
|
||||
return item_errors == 0 and collection_errors == 0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue