-
Notifications
You must be signed in to change notification settings - Fork 182
Expand file tree
/
Copy pathbase.py
More file actions
270 lines (206 loc) Β· 8.61 KB
/
base.py
File metadata and controls
270 lines (206 loc) Β· 8.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import contextlib
import functools
from typing import Optional
from .. import exceptions
from ..utils import uniq
def mutating_storage_method(f):
@functools.wraps(f)
async def inner(self, *args, **kwargs):
if self.read_only:
raise exceptions.ReadOnlyError("This storage is read-only.")
return await f(self, *args, **kwargs)
return inner
class StorageMeta(type):
def __init__(cls, name, bases, d):
for method in ("update", "upload", "delete"):
setattr(cls, method, mutating_storage_method(getattr(cls, method)))
return super().__init__(name, bases, d)
class Storage(metaclass=StorageMeta):
"""Superclass of all storages, interface that all storages have to
implement.
Terminology:
- ITEM: Instance of the Item class, represents a calendar event, task or
contact.
- HREF: String; Per-storage identifier of item, might be UID. The reason
items aren't just referenced by their UID is because the CalDAV and
CardDAV specifications make this unperformant to implement.
- ETAG: String; Checksum of item, or something similar that changes when
the item does.
Strings can be either unicode strings or bytestrings. If bytestrings, an
ASCII encoding is assumed.
:param read_only: Whether the synchronization algorithm should avoid writes
to this storage. Some storages accept no value other than ``True``.
:param implicit: Whether the synchronization shall create/delete collections
in the destination, when these were created/removed from the source. Must
be a possibly empty list of strings.
"""
fileext = ".txt"
# The string used in the config to denote the type of storage. Should be
# overridden by subclasses.
storage_name = None
# The string used in the config to denote a particular instance. Will be
# overridden during instantiation.
instance_name = None
# The machine-readable name of this collection.
collection = None
# A value of True means the storage does not support write-methods such as
# upload, update and delete. A value of False means the storage does
# support those methods.
read_only = False
# The attribute values to show in the representation of the storage.
_repr_attributes = ()
def __init__(
self,
instance_name=None,
read_only=None,
collection=None,
implicit=None,
):
if read_only is None:
read_only = self.read_only
self.implicit = implicit # unused from within the Storage classes
if self.read_only and not read_only:
raise exceptions.UserError("This storage can only be read-only.")
self.read_only = bool(read_only)
if collection and instance_name:
instance_name = f"{instance_name}/{collection}"
self.instance_name = instance_name
self.collection = collection
@classmethod
async def discover(cls, **kwargs):
"""Discover collections given a basepath or -URL to many collections.
:param **kwargs: Keyword arguments to additionally pass to the storage
instances returned. You shouldn't pass `collection` here, otherwise
TypeError will be raised.
:returns: iterable of ``storage_args``.
``storage_args`` is a dictionary of ``**kwargs`` to pass to this
class to obtain a storage instance pointing to this collection. It
also must contain a ``"collection"`` key. That key's value is used
to match two collections together for synchronization. IOW it is a
machine-readable identifier for the collection, usually obtained
from the last segment of a URL or filesystem path.
"""
if False:
yield # Needs to be an async generator
raise NotImplementedError()
@classmethod
async def create_collection(cls, collection, **kwargs):
"""
Create the specified collection and return the new arguments.
``collection=None`` means the arguments are already pointing to a
possible collection location.
The returned args should contain the collection name, for UI purposes.
"""
raise NotImplementedError()
@classmethod
def delete_collection(cls, collection, **kwargs):
"""
Delete the specified collection and return the new arguments.
``collection=None`` means the arguments are already pointing to a
possible collection location.
The returned args should contain the collection name, for UI purposes.
"""
raise NotImplementedError()
def __repr__(self):
try:
if self.instance_name:
return str(self.instance_name)
except ValueError:
pass
return "<{}(**{})>".format(
self.__class__.__name__,
{x: getattr(self, x) for x in self._repr_attributes},
)
async def list(self):
"""
:returns: list of (href, etag)
"""
raise NotImplementedError()
async def get(self, href):
"""Fetch a single item.
:param href: href to fetch
:returns: (item, etag)
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` if item can't
be found.
"""
raise NotImplementedError()
async def get_multi(self, hrefs):
"""Fetch multiple items. Duplicate hrefs must be ignored.
Functionally similar to :py:meth:`get`, but might bring performance
benefits on some storages when used cleverly.
:param hrefs: list of hrefs to fetch
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` if one of the
items couldn't be found.
:returns: iterable of (href, item, etag)
"""
for href in uniq(hrefs):
item, etag = await self.get(href)
yield href, item, etag
async def has(self, href):
"""Check if an item exists by its href.
:returns: True or False
"""
try:
await self.get(href)
except exceptions.PreconditionFailed:
return False
else:
return True
async def upload(self, item):
"""Upload a new item.
In cases where the new etag cannot be atomically determined (i.e. in
the same "transaction" as the upload itself), this method may return
`None` as etag. This special case only exists because of DAV. Avoid
this situation whenever possible.
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` if there is
already an item with that href.
:returns: (href, etag)
"""
raise NotImplementedError()
async def update(self, href, item, etag):
"""Update an item.
The etag may be none in some cases, see `upload`.
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` if the etag on
the server doesn't match the given etag or if the item doesn't
exist.
:returns: etag
"""
raise NotImplementedError()
async def delete(self, href, etag):
"""Delete an item by href.
:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` when item has
a different etag or doesn't exist.
"""
raise NotImplementedError()
@contextlib.asynccontextmanager
async def at_once(self):
"""A contextmanager that buffers all writes.
Essentially, this::
s.upload(...)
s.update(...)
becomes this::
with s.at_once():
s.upload(...)
s.update(...)
Note that this removes guarantees about which exceptions are returned
when.
"""
yield
async def get_meta(self, key: str) -> Optional[str]:
"""Get metadata value for collection/storage.
See the vdir specification for the keys that *have* to be accepted.
:param key: The metadata key.
:return: The metadata or None, if metadata is missing.
"""
raise NotImplementedError("This storage does not support metadata.")
async def set_meta(self, key: str, value: Optional[str]):
"""Get metadata value for collection/storage.
:param key: The metadata key.
:param value: The value. Use None to delete the data.
"""
raise NotImplementedError("This storage does not support metadata.")
def normalize_meta_value(value) -> Optional[str]:
# `None` is returned by iCloud for empty properties.
if value is None or value == "None":
return
return value.strip() if value else ""