Skip to content

Commit b8475e4

Browse files
Add required_attendee
1 parent adc9e5f commit b8475e4

File tree

3 files changed

+63
-35
lines changed

3 files changed

+63
-35
lines changed

vdirsyncer/cli/tasks.py

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def error_callback(e):
8080
error_callback=error_callback,
8181
partial_sync=pair.partial_sync,
8282
remove_details=pair.remove_details,
83+
required_attendee=pair.required_attendee,
8384
)
8485

8586
if sync_failed:

vdirsyncer/sync/__init__.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __init__(self, storage: Storage, status: SubStatus):
4242
self.status = status
4343
self._item_cache = {} # type: ignore[var-annotated]
4444

45-
async def prepare_new_status(self, remove_details: bool = False) -> bool:
45+
async def prepare_new_status(self, remove_details: bool = False, required_attendee: str | None = None) -> bool:
4646
storage_nonempty = False
4747
prefetch = []
4848

@@ -67,6 +67,8 @@ def _store_props(ident: str, props: ItemMetadata) -> None:
6767
# Prefetch items
6868
if prefetch:
6969
async for href, item, etag in self.storage.get_multi(prefetch):
70+
if required_attendee and not item.has_confirmed_attendee(required_attendee):
71+
continue
7072
if remove_details:
7173
item = item.without_details()
7274
_store_props(
@@ -107,7 +109,8 @@ async def sync(
107109
force_delete=False,
108110
error_callback=None,
109111
partial_sync="revert",
110-
remove_details: bool=False,
112+
remove_details: bool = False,
113+
required_attendee: str | None = None,
111114
) -> None:
112115
"""Synchronizes two storages.
113116
@@ -149,8 +152,14 @@ async def sync(
149152
a_info = _StorageInfo(storage_a, SubStatus(status, "a"))
150153
b_info = _StorageInfo(storage_b, SubStatus(status, "b"))
151154

152-
a_nonempty = await a_info.prepare_new_status(remove_details=remove_details)
153-
b_nonempty = await b_info.prepare_new_status(remove_details=remove_details)
155+
a_nonempty = await a_info.prepare_new_status(
156+
remove_details=remove_details,
157+
required_attendee=required_attendee
158+
)
159+
b_nonempty = await b_info.prepare_new_status(
160+
remove_details=remove_details,
161+
required_attendee=required_attendee
162+
)
154163

155164
if status_nonempty and not force_delete:
156165
if a_nonempty and not b_nonempty:

vdirsyncer/vobject.py

+49-31
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ def with_uid(self, new_uid):
5656
component["UID"] = new_uid
5757

5858
return Item("\r\n".join(parsed.dump_lines()))
59+
60+
def has_confirmed_attendee(self, email: str) -> bool:
61+
"""Returns True if the given attendee has accepted an invite to this event"""
62+
parsed = _Component.parse(self.raw)
63+
stack = [parsed]
64+
while stack:
65+
component = stack.pop()
66+
for attendee_line in component.get_all("ATTENDEE"):
67+
sections = attendee_line.split(";")
68+
if f"CN={email}" in sections and "PARTSTAT=ACCEPTED" in sections:
69+
return True
70+
stack.extend(component.subcomponents)
71+
72+
return False
5973

6074
def without_details(self):
6175
"""Returns a minimal version of this item.
@@ -78,8 +92,7 @@ def without_details(self):
7892
if subcomp.name != "VTIMEZONE"
7993
]
8094
for field in ["DESCRIPTION", "ORGANIZER", "ATTENDEE", "LOCATION"]:
81-
# Repeatedly delete because some fields can appear multiple times
82-
while field in component:
95+
if field in component:
8396
del component[field]
8497

8598
stack.extend(component.subcomponents)
@@ -264,6 +277,17 @@ def _get_item_type(components, wrappers):
264277
raise ValueError("Not sure how to join components.")
265278

266279

280+
def _extract_prop_value(line, key):
281+
if line.startswith(key):
282+
prefix_without_params = f"{key}:"
283+
prefix_with_params = f"{key};"
284+
if line.startswith(prefix_without_params):
285+
return line[len(prefix_without_params) :]
286+
elif line.startswith(prefix_with_params):
287+
return line[len(prefix_with_params) :].split(":", 1)[-1]
288+
289+
return None
290+
267291
class _Component:
268292
"""
269293
Raw outline of the components.
@@ -347,20 +371,15 @@ def dump_lines(self):
347371
def __delitem__(self, key):
348372
prefix = (f"{key}:", f"{key};")
349373
new_lines = []
350-
lineiter = iter(self.props)
351-
while True:
352-
for line in lineiter:
374+
in_prop = False
375+
for line in iter(self.props):
376+
if not in_prop:
353377
if line.startswith(prefix):
354-
break
378+
in_prop = True
355379
else:
356380
new_lines.append(line)
357-
else:
358-
break
359-
360-
for line in lineiter:
361-
if not line.startswith((" ", "\t")):
362-
new_lines.append(line)
363-
break
381+
elif not line.startswith((" ", "\t")):
382+
in_prop = False
364383

365384
self.props = new_lines
366385

@@ -382,26 +401,25 @@ def __contains__(self, obj):
382401
raise ValueError(obj)
383402

384403
def __getitem__(self, key):
385-
prefix_without_params = f"{key}:"
386-
prefix_with_params = f"{key};"
387-
iterlines = iter(self.props)
388-
for line in iterlines:
389-
if line.startswith(prefix_without_params):
390-
rv = line[len(prefix_without_params) :]
391-
break
392-
elif line.startswith(prefix_with_params):
393-
rv = line[len(prefix_with_params) :].split(":", 1)[-1]
394-
break
395-
else:
404+
try:
405+
return next(self.get_all(key))
406+
except StopIteration:
396407
raise KeyError
397-
398-
for line in iterlines:
399-
if line.startswith((" ", "\t")):
400-
rv += line[1:]
408+
409+
def get_all(self, key: str):
410+
rv = None
411+
for line in iter(self.props):
412+
if rv is None:
413+
rv = _extract_prop_value(line, key)
401414
else:
402-
break
403-
404-
return rv
415+
if line.startswith((" ", "\t")):
416+
rv += line[1:]
417+
else:
418+
yield rv
419+
rv = _extract_prop_value(line, key)
420+
421+
if rv is not None:
422+
yield rv
405423

406424
def get(self, key, default=None):
407425
try:

0 commit comments

Comments
 (0)