Refactor CardDAV report planning branches
This commit is contained in:
@@ -19,6 +19,7 @@ from govoplan_addresses.backend.carddav import (
|
||||
AddressCardDAVAddressBook,
|
||||
AddressCardDAVClient,
|
||||
AddressCardDAVError,
|
||||
AddressCardDAVObject,
|
||||
AddressCardDAVPreconditionFailed,
|
||||
AddressCardDAVReportResult,
|
||||
AddressCardDAVSyncUnsupported,
|
||||
@@ -59,7 +60,7 @@ from govoplan_addresses.backend.schemas import (
|
||||
ContactPostalAddressPayload,
|
||||
ContactUpdateRequest,
|
||||
)
|
||||
from govoplan_addresses.backend.vcard import ParsedVCardIssue, contacts_to_vcard, parse_vcards_with_issues
|
||||
from govoplan_addresses.backend.vcard import ParsedVCard, ParsedVCardIssue, contacts_to_vcard, parse_vcards_with_issues
|
||||
|
||||
|
||||
class AddressBookError(ValueError):
|
||||
@@ -772,15 +773,85 @@ def _plan_carddav_report(
|
||||
for item in report.objects:
|
||||
href = item.href
|
||||
seen_hrefs.add(href)
|
||||
local = local_by_href.get(href)
|
||||
_plan_carddav_report_object(
|
||||
sync_source=sync_source,
|
||||
client=client,
|
||||
item=item,
|
||||
local=local_by_href.get(href),
|
||||
reads_remote=reads_remote,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
if plan.stats.full_sync:
|
||||
_plan_carddav_full_sync_absences(
|
||||
sync_source=sync_source,
|
||||
local_by_href=local_by_href,
|
||||
seen_hrefs=seen_hrefs,
|
||||
reads_remote=reads_remote,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
_plan_carddav_outbound_local_changes(session, sync_source=sync_source, plan=plan)
|
||||
|
||||
|
||||
def _plan_carddav_report_object(
|
||||
*,
|
||||
sync_source: AddressSyncSource,
|
||||
client: AddressCardDAVClient,
|
||||
item: AddressCardDAVObject,
|
||||
local: Contact | None,
|
||||
reads_remote: bool,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
if item.deleted:
|
||||
if local is not None and local.deleted_at is None:
|
||||
_plan_carddav_remote_deletion(
|
||||
sync_source=sync_source,
|
||||
item=item,
|
||||
local=local,
|
||||
reads_remote=reads_remote,
|
||||
plan=plan,
|
||||
)
|
||||
return
|
||||
loaded = _load_carddav_report_vcard(client=client, item=item, plan=plan)
|
||||
if loaded is None:
|
||||
return
|
||||
raw_vcard, parsed = loaded
|
||||
_plan_carddav_live_object(
|
||||
sync_source=sync_source,
|
||||
item=item,
|
||||
local=local,
|
||||
reads_remote=reads_remote,
|
||||
raw_vcard=raw_vcard,
|
||||
parsed=parsed,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
|
||||
def _plan_carddav_remote_deletion(
|
||||
*,
|
||||
sync_source: AddressSyncSource,
|
||||
item: AddressCardDAVObject,
|
||||
local: Contact | None,
|
||||
reads_remote: bool,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
if local is None or local.deleted_at is not None:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="unchanged",
|
||||
href=item.href,
|
||||
etag=item.etag,
|
||||
message="Remote delete already reflected locally.",
|
||||
),
|
||||
)
|
||||
return
|
||||
if _sync_source_writes_remote(sync_source) and _local_contact_changed_after_last_sync(local, sync_source):
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="conflict",
|
||||
href=href,
|
||||
href=item.href,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
etag=item.etag,
|
||||
@@ -788,49 +859,75 @@ def _plan_carddav_report(
|
||||
),
|
||||
)
|
||||
elif reads_remote:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="delete", href=href, contact_id=local.id, display_name=local.display_name, etag=item.etag))
|
||||
else:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="unchanged", href=href, etag=item.etag, message="Remote delete already reflected locally."))
|
||||
continue
|
||||
|
||||
raw_vcard = item.address_data
|
||||
if not raw_vcard:
|
||||
try:
|
||||
raw_vcard = client.fetch_object(href)
|
||||
except AddressCardDAVError as exc:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="error", href=href, etag=item.etag, message=str(exc)))
|
||||
continue
|
||||
parsed, error_message = _parse_single_remote_vcard(raw_vcard)
|
||||
if parsed is None:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="error", href=href, etag=item.etag, message=error_message or "Remote vCard could not be parsed."))
|
||||
continue
|
||||
|
||||
if local is None:
|
||||
if reads_remote:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="create",
|
||||
href=href,
|
||||
remote_uid=parsed.source_ref,
|
||||
display_name=parsed.payload.display_name,
|
||||
action="delete",
|
||||
href=item.href,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
etag=item.etag,
|
||||
raw_vcard=raw_vcard,
|
||||
parsed_payload=parsed.payload,
|
||||
source_revision=item.etag or parsed.source_revision,
|
||||
),
|
||||
)
|
||||
else:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="unchanged", href=href, etag=item.etag, message="Remote object ignored by export-only sync source."))
|
||||
continue
|
||||
|
||||
|
||||
def _load_carddav_report_vcard(
|
||||
*,
|
||||
client: AddressCardDAVClient,
|
||||
item: AddressCardDAVObject,
|
||||
plan: AddressSyncPlan,
|
||||
) -> tuple[str, ParsedVCard] | None:
|
||||
raw_vcard = item.address_data
|
||||
if not raw_vcard:
|
||||
try:
|
||||
raw_vcard = client.fetch_object(item.href)
|
||||
except AddressCardDAVError as exc:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(action="error", href=item.href, etag=item.etag, message=str(exc)),
|
||||
)
|
||||
return None
|
||||
parsed, error_message = _parse_single_remote_vcard(raw_vcard)
|
||||
if parsed is None:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="error",
|
||||
href=item.href,
|
||||
etag=item.etag,
|
||||
message=error_message or "Remote vCard could not be parsed.",
|
||||
),
|
||||
)
|
||||
return None
|
||||
return raw_vcard, parsed
|
||||
|
||||
|
||||
def _plan_carddav_live_object(
|
||||
*,
|
||||
sync_source: AddressSyncSource,
|
||||
item: AddressCardDAVObject,
|
||||
local: Contact | None,
|
||||
reads_remote: bool,
|
||||
raw_vcard: str,
|
||||
parsed: ParsedVCard,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
if local is None:
|
||||
_plan_new_carddav_remote_object(
|
||||
item=item,
|
||||
reads_remote=reads_remote,
|
||||
raw_vcard=raw_vcard,
|
||||
parsed=parsed,
|
||||
plan=plan,
|
||||
)
|
||||
return
|
||||
remote_revision = item.etag or parsed.source_revision
|
||||
if local.deleted_at is not None and _sync_source_writes_remote(sync_source) and _local_contact_changed_after_last_sync(local, sync_source):
|
||||
if _carddav_local_delete_needs_push(local, sync_source):
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="remote_delete",
|
||||
href=href,
|
||||
href=item.href,
|
||||
remote_uid=parsed.source_ref,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
@@ -838,37 +935,22 @@ def _plan_carddav_report(
|
||||
message="Local delete will be pushed to CardDAV.",
|
||||
),
|
||||
)
|
||||
continue
|
||||
|
||||
return
|
||||
if local.deleted_at is None and local.source_revision == remote_revision:
|
||||
if _sync_source_writes_remote(sync_source) and _local_contact_changed_after_last_sync(local, sync_source):
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="remote_update",
|
||||
href=href,
|
||||
remote_uid=parsed.source_ref,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
etag=item.etag or local.source_revision,
|
||||
raw_vcard=_carddav_contact_vcard(local, href=href),
|
||||
source_revision=item.etag or local.source_revision,
|
||||
message="Local update will be pushed to CardDAV.",
|
||||
),
|
||||
_plan_matching_carddav_revision(
|
||||
sync_source=sync_source,
|
||||
item=item,
|
||||
local=local,
|
||||
parsed=parsed,
|
||||
plan=plan,
|
||||
)
|
||||
continue
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(action="unchanged", href=href, remote_uid=parsed.source_ref, contact_id=local.id, display_name=local.display_name, etag=item.etag),
|
||||
)
|
||||
continue
|
||||
|
||||
return
|
||||
if _local_contact_changed_after_last_sync(local, sync_source):
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="conflict",
|
||||
href=href,
|
||||
href=item.href,
|
||||
remote_uid=parsed.source_ref,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
@@ -879,14 +961,113 @@ def _plan_carddav_report(
|
||||
message="Local contact changed since the last successful sync.",
|
||||
),
|
||||
)
|
||||
continue
|
||||
return
|
||||
_plan_carddav_remote_update(
|
||||
item=item,
|
||||
local=local,
|
||||
reads_remote=reads_remote,
|
||||
raw_vcard=raw_vcard,
|
||||
parsed=parsed,
|
||||
remote_revision=remote_revision,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
|
||||
def _plan_new_carddav_remote_object(
|
||||
*,
|
||||
item: AddressCardDAVObject,
|
||||
reads_remote: bool,
|
||||
raw_vcard: str,
|
||||
parsed: ParsedVCard,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
if reads_remote:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="create",
|
||||
href=item.href,
|
||||
remote_uid=parsed.source_ref,
|
||||
display_name=parsed.payload.display_name,
|
||||
etag=item.etag,
|
||||
raw_vcard=raw_vcard,
|
||||
parsed_payload=parsed.payload,
|
||||
source_revision=item.etag or parsed.source_revision,
|
||||
),
|
||||
)
|
||||
else:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="unchanged",
|
||||
href=item.href,
|
||||
etag=item.etag,
|
||||
message="Remote object ignored by export-only sync source.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _carddav_local_delete_needs_push(local: Contact, sync_source: AddressSyncSource) -> bool:
|
||||
return bool(
|
||||
local.deleted_at is not None
|
||||
and _sync_source_writes_remote(sync_source)
|
||||
and _local_contact_changed_after_last_sync(local, sync_source)
|
||||
)
|
||||
|
||||
|
||||
def _plan_matching_carddav_revision(
|
||||
*,
|
||||
sync_source: AddressSyncSource,
|
||||
item: AddressCardDAVObject,
|
||||
local: Contact,
|
||||
parsed: ParsedVCard,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
if _sync_source_writes_remote(sync_source) and _local_contact_changed_after_last_sync(local, sync_source):
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="remote_update",
|
||||
href=item.href,
|
||||
remote_uid=parsed.source_ref,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
etag=item.etag or local.source_revision,
|
||||
raw_vcard=_carddav_contact_vcard(local, href=item.href),
|
||||
source_revision=item.etag or local.source_revision,
|
||||
message="Local update will be pushed to CardDAV.",
|
||||
),
|
||||
)
|
||||
return
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="unchanged",
|
||||
href=item.href,
|
||||
remote_uid=parsed.source_ref,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
etag=item.etag,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _plan_carddav_remote_update(
|
||||
*,
|
||||
item: AddressCardDAVObject,
|
||||
local: Contact,
|
||||
reads_remote: bool,
|
||||
raw_vcard: str,
|
||||
parsed: ParsedVCard,
|
||||
remote_revision: str | None,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
if reads_remote:
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="update",
|
||||
href=href,
|
||||
href=item.href,
|
||||
remote_uid=parsed.source_ref,
|
||||
contact_id=local.id,
|
||||
display_name=parsed.payload.display_name or local.display_name,
|
||||
@@ -897,11 +1078,30 @@ def _plan_carddav_report(
|
||||
),
|
||||
)
|
||||
else:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="unchanged", href=href, contact_id=local.id, display_name=local.display_name, etag=item.etag, message="Remote update ignored by export-only sync source."))
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="unchanged",
|
||||
href=item.href,
|
||||
contact_id=local.id,
|
||||
display_name=local.display_name,
|
||||
etag=item.etag,
|
||||
message="Remote update ignored by export-only sync source.",
|
||||
),
|
||||
)
|
||||
|
||||
if plan.stats.full_sync:
|
||||
|
||||
def _plan_carddav_full_sync_absences(
|
||||
*,
|
||||
sync_source: AddressSyncSource,
|
||||
local_by_href: dict[str, Contact],
|
||||
seen_hrefs: set[str],
|
||||
reads_remote: bool,
|
||||
plan: AddressSyncPlan,
|
||||
) -> None:
|
||||
for href, contact in local_by_href.items():
|
||||
if href not in seen_hrefs and contact.deleted_at is None:
|
||||
if href in seen_hrefs or contact.deleted_at is not None:
|
||||
continue
|
||||
if _sync_source_writes_remote(sync_source) and _local_contact_changed_after_last_sync(contact, sync_source):
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
@@ -915,9 +1115,16 @@ def _plan_carddav_report(
|
||||
),
|
||||
)
|
||||
elif reads_remote:
|
||||
_add_sync_plan_item(plan, AddressSyncPlanItem(action="delete", href=href, contact_id=contact.id, display_name=contact.display_name, message="Remote object is absent from full sync."))
|
||||
|
||||
_plan_carddav_outbound_local_changes(session, sync_source=sync_source, plan=plan)
|
||||
_add_sync_plan_item(
|
||||
plan,
|
||||
AddressSyncPlanItem(
|
||||
action="delete",
|
||||
href=href,
|
||||
contact_id=contact.id,
|
||||
display_name=contact.display_name,
|
||||
message="Remote object is absent from full sync.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _plan_carddav_outbound_local_changes(
|
||||
|
||||
Reference in New Issue
Block a user