Skip to content

Commit 7ffcf58

Browse files
Add required_attendee
1 parent 2e6a213 commit 7ffcf58

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
@@ -41,7 +41,7 @@ def __init__(self, storage: Storage, status: SubStatus):
4141
self.status = status
4242
self._item_cache = {} # type: ignore[var-annotated]
4343

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

@@ -66,6 +66,8 @@ def _store_props(ident: str, props: ItemMetadata) -> None:
6666
# Prefetch items
6767
if prefetch:
6868
async for href, item, etag in self.storage.get_multi(prefetch):
69+
if required_attendee and not item.has_confirmed_attendee(required_attendee):
70+
continue
6971
if remove_details:
7072
item = item.without_details()
7173
_store_props(
@@ -106,7 +108,8 @@ async def sync(
106108
force_delete=False,
107109
error_callback=None,
108110
partial_sync="revert",
109-
remove_details: bool=False,
111+
remove_details: bool = False,
112+
required_attendee: str | None = None,
110113
) -> None:
111114
"""Synchronizes two storages.
112115
@@ -148,8 +151,14 @@ async def sync(
148151
a_info = _StorageInfo(storage_a, SubStatus(status, "a"))
149152
b_info = _StorageInfo(storage_b, SubStatus(status, "b"))
150153

151-
a_nonempty = await a_info.prepare_new_status(remove_details=remove_details)
152-
b_nonempty = await b_info.prepare_new_status(remove_details=remove_details)
154+
a_nonempty = await a_info.prepare_new_status(
155+
remove_details=remove_details,
156+
required_attendee=required_attendee
157+
)
158+
b_nonempty = await b_info.prepare_new_status(
159+
remove_details=remove_details,
160+
required_attendee=required_attendee
161+
)
153162

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

vdirsyncer/vobject.py

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

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

6175
def without_details(self):
6276
"""Returns a minimal version of this item.
@@ -79,8 +93,7 @@ def without_details(self):
7993
if subcomp.name != "VTIMEZONE"
8094
]
8195
for field in ["DESCRIPTION", "ORGANIZER", "ATTENDEE", "LOCATION"]:
82-
# Repeatedly delete because some fields can appear multiple times
83-
while field in component:
96+
if field in component:
8497
del component[field]
8598

8699
stack.extend(component.subcomponents)
@@ -265,6 +278,17 @@ def _get_item_type(components, wrappers):
265278
raise ValueError("Not sure how to join components.")
266279

267280

281+
def _extract_prop_value(line, key):
282+
if line.startswith(key):
283+
prefix_without_params = f"{key}:"
284+
prefix_with_params = f"{key};"
285+
if line.startswith(prefix_without_params):
286+
return line[len(prefix_without_params) :]
287+
elif line.startswith(prefix_with_params):
288+
return line[len(prefix_with_params) :].split(":", 1)[-1]
289+
290+
return None
291+
268292
class _Component:
269293
"""
270294
Raw outline of the components.
@@ -337,20 +361,15 @@ def dump_lines(self):
337361
def __delitem__(self, key):
338362
prefix = (f"{key}:", f"{key};")
339363
new_lines = []
340-
lineiter = iter(self.props)
341-
while True:
342-
for line in lineiter:
364+
in_prop = False
365+
for line in iter(self.props):
366+
if not in_prop:
343367
if line.startswith(prefix):
344-
break
368+
in_prop = True
345369
else:
346370
new_lines.append(line)
347-
else:
348-
break
349-
350-
for line in lineiter:
351-
if not line.startswith((" ", "\t")):
352-
new_lines.append(line)
353-
break
371+
elif not line.startswith((" ", "\t")):
372+
in_prop = False
354373

355374
self.props = new_lines
356375

@@ -372,26 +391,25 @@ def __contains__(self, obj):
372391
raise ValueError(obj)
373392

374393
def __getitem__(self, key):
375-
prefix_without_params = f"{key}:"
376-
prefix_with_params = f"{key};"
377-
iterlines = iter(self.props)
378-
for line in iterlines:
379-
if line.startswith(prefix_without_params):
380-
rv = line[len(prefix_without_params) :]
381-
break
382-
elif line.startswith(prefix_with_params):
383-
rv = line[len(prefix_with_params) :].split(":", 1)[-1]
384-
break
385-
else:
394+
try:
395+
return next(self.get_all(key))
396+
except StopIteration:
386397
raise KeyError
387-
388-
for line in iterlines:
389-
if line.startswith((" ", "\t")):
390-
rv += line[1:]
398+
399+
def get_all(self, key: str):
400+
rv = None
401+
for line in iter(self.props):
402+
if rv is None:
403+
rv = _extract_prop_value(line, key)
391404
else:
392-
break
393-
394-
return rv
405+
if line.startswith((" ", "\t")):
406+
rv += line[1:]
407+
else:
408+
yield rv
409+
rv = _extract_prop_value(line, key)
410+
411+
if rv is not None:
412+
yield rv
395413

396414
def get(self, key, default=None):
397415
try:

0 commit comments

Comments
 (0)