feat: add selective vCard batch workflows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import json
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
@@ -56,6 +57,23 @@ from govoplan_addresses.backend.imports import (
|
||||
rollback_address_import,
|
||||
update_import_profile,
|
||||
)
|
||||
from govoplan_addresses.backend.vcard_batch_schemas import (
|
||||
VCardBatchCancelRequest,
|
||||
VCardBatchCommitRequest,
|
||||
VCardBatchPreviewRequest,
|
||||
VCardBatchRunResponse,
|
||||
VCardExportRequest,
|
||||
VCardExportResponse,
|
||||
)
|
||||
from govoplan_addresses.backend.vcard_batches import (
|
||||
apply_vcard_batch,
|
||||
cancel_vcard_batch,
|
||||
export_vcards,
|
||||
get_vcard_batch_run,
|
||||
preview_vcard_batch,
|
||||
vcard_batch_payload,
|
||||
vcard_diagnostics_payload,
|
||||
)
|
||||
from govoplan_addresses.backend.capabilities import (
|
||||
AddressesContactPointResolutionCapability,
|
||||
AddressesContactWriterCapability,
|
||||
@@ -238,9 +256,7 @@ def _contact_response(
|
||||
decision = quality.get((channel, point_id)) or quality.get((channel, None))
|
||||
return {
|
||||
"quality_state": decision.state if decision is not None else "valid",
|
||||
"quality_reason_code": (
|
||||
decision.reason_code if decision is not None else None
|
||||
),
|
||||
"quality_reason_code": (decision.reason_code if decision is not None else None),
|
||||
}
|
||||
|
||||
return ContactResponse.model_validate(
|
||||
@@ -323,9 +339,7 @@ def _contact_point_audit_details(
|
||||
"postal": [item.id for item in contact.postal_addresses],
|
||||
}
|
||||
return {
|
||||
f"{key_prefix}contact_point_counts": {
|
||||
channel: len(ids) for channel, ids in point_ids.items()
|
||||
},
|
||||
f"{key_prefix}contact_point_counts": {channel: len(ids) for channel, ids in point_ids.items()},
|
||||
f"{key_prefix}contact_point_ids": point_ids,
|
||||
}
|
||||
|
||||
@@ -454,7 +468,9 @@ def _sync_source_response(sync_source: AddressSyncSource) -> AddressSyncSourceRe
|
||||
)
|
||||
|
||||
|
||||
def _sync_diagnostic_response(diagnostic: AddressSyncDiagnostic) -> AddressSyncDiagnosticResponse:
|
||||
def _sync_diagnostic_response(
|
||||
diagnostic: AddressSyncDiagnostic,
|
||||
) -> AddressSyncDiagnosticResponse:
|
||||
return AddressSyncDiagnosticResponse.model_validate(
|
||||
{
|
||||
"id": diagnostic.id,
|
||||
@@ -470,7 +486,9 @@ def _sync_diagnostic_response(diagnostic: AddressSyncDiagnostic) -> AddressSyncD
|
||||
)
|
||||
|
||||
|
||||
def _sync_tombstone_response(tombstone: AddressSyncTombstone) -> AddressSyncTombstoneResponse:
|
||||
def _sync_tombstone_response(
|
||||
tombstone: AddressSyncTombstone,
|
||||
) -> AddressSyncTombstoneResponse:
|
||||
return AddressSyncTombstoneResponse.model_validate(
|
||||
{
|
||||
"id": tombstone.id,
|
||||
@@ -490,7 +508,9 @@ def _sync_tombstone_response(tombstone: AddressSyncTombstone) -> AddressSyncTomb
|
||||
)
|
||||
|
||||
|
||||
def _sync_conflict_response(conflict: AddressSyncConflict) -> AddressSyncConflictResponse:
|
||||
def _sync_conflict_response(
|
||||
conflict: AddressSyncConflict,
|
||||
) -> AddressSyncConflictResponse:
|
||||
return AddressSyncConflictResponse.model_validate(
|
||||
{
|
||||
"id": conflict.id,
|
||||
@@ -583,7 +603,11 @@ def api_list_address_books(
|
||||
return AddressBookListResponse(address_books=[_book_response(book, contact_count=counts.get(book.id, 0)) for book in books])
|
||||
|
||||
|
||||
@router.post("/address-books", response_model=AddressBookResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-books",
|
||||
response_model=AddressBookResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_address_book(
|
||||
payload: AddressBookCreateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
@@ -591,7 +615,12 @@ def api_create_address_book(
|
||||
):
|
||||
_require_scope(principal, "addresses:address_book:write")
|
||||
try:
|
||||
book = create_address_book(session, principal, payload, allow_system=has_scope(principal, "addresses:address_book:admin"))
|
||||
book = create_address_book(
|
||||
session,
|
||||
principal,
|
||||
payload,
|
||||
allow_system=has_scope(principal, "addresses:address_book:admin"),
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(book)
|
||||
return _book_response(book)
|
||||
@@ -738,10 +767,7 @@ def api_suggest_duplicate_contacts(
|
||||
right=_contact_response(item.right),
|
||||
score=item.score,
|
||||
confidence=item.confidence,
|
||||
features=[
|
||||
ContactDuplicateFeatureResponse(**asdict(feature))
|
||||
for feature in item.features
|
||||
],
|
||||
features=[ContactDuplicateFeatureResponse(**asdict(feature)) for feature in item.features],
|
||||
)
|
||||
for item in scan.suggestions
|
||||
],
|
||||
@@ -778,10 +804,7 @@ def api_address_quality_summary(
|
||||
quality_counts=summary.quality_counts,
|
||||
duplicate_suggestion_count=summary.duplicate_suggestion_count,
|
||||
correction_count=summary.correction_count,
|
||||
corrections=[
|
||||
AddressQualityCorrectionResponse(**asdict(item))
|
||||
for item in summary.corrections
|
||||
],
|
||||
corrections=[AddressQualityCorrectionResponse(**asdict(item)) for item in summary.corrections],
|
||||
truncated=summary.truncated,
|
||||
)
|
||||
except AddressBookError as exc:
|
||||
@@ -895,9 +918,7 @@ def api_resolve_contact_redirect(
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
try:
|
||||
return ContactRedirectResponse.model_validate(
|
||||
asdict(resolve_contact_redirect(session, principal, contact_id))
|
||||
)
|
||||
return ContactRedirectResponse.model_validate(asdict(resolve_contact_redirect(session, principal, contact_id)))
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
@@ -1167,16 +1188,23 @@ def api_list_address_lists(
|
||||
):
|
||||
_require_scope(principal, "addresses:address_list:read")
|
||||
try:
|
||||
address_lists = list_address_lists(session, principal, address_book_id=address_book_id, include_deleted=include_deleted)
|
||||
counts = address_list_entry_counts(session, [address_list.id for address_list in address_lists])
|
||||
return AddressListListResponse(
|
||||
address_lists=[_address_list_response(address_list, entry_count=counts.get(address_list.id, 0)) for address_list in address_lists]
|
||||
address_lists = list_address_lists(
|
||||
session,
|
||||
principal,
|
||||
address_book_id=address_book_id,
|
||||
include_deleted=include_deleted,
|
||||
)
|
||||
counts = address_list_entry_counts(session, [address_list.id for address_list in address_lists])
|
||||
return AddressListListResponse(address_lists=[_address_list_response(address_list, entry_count=counts.get(address_list.id, 0)) for address_list in address_lists])
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/address-books/{book_id}/address-lists", response_model=AddressListResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-books/{book_id}/address-lists",
|
||||
response_model=AddressListResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_address_list(
|
||||
book_id: str,
|
||||
payload: AddressListCreateRequest,
|
||||
@@ -1247,7 +1275,10 @@ def api_restore_address_list(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/address-lists/{address_list_id}/entries", response_model=AddressListEntryListResponse)
|
||||
@router.get(
|
||||
"/address-lists/{address_list_id}/entries",
|
||||
response_model=AddressListEntryListResponse,
|
||||
)
|
||||
def api_list_address_list_entries(
|
||||
address_list_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
@@ -1261,7 +1292,11 @@ def api_list_address_list_entries(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/address-lists/{address_list_id}/entries", response_model=AddressListEntryResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-lists/{address_list_id}/entries",
|
||||
response_model=AddressListEntryResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_address_list_entry(
|
||||
address_list_id: str,
|
||||
payload: AddressListEntryCreateRequest,
|
||||
@@ -1332,7 +1367,10 @@ def api_list_write_targets(
|
||||
return AddressBookWriteTargetsResponse(targets=[_write_decision_response(decision) for decision in decisions])
|
||||
|
||||
|
||||
@router.get("/address-books/{book_id}/write-decision", response_model=AddressBookWriteDecisionResponse)
|
||||
@router.get(
|
||||
"/address-books/{book_id}/write-decision",
|
||||
response_model=AddressBookWriteDecisionResponse,
|
||||
)
|
||||
def api_get_address_book_write_decision(
|
||||
book_id: str,
|
||||
operation: str = Query(default="create_contact"),
|
||||
@@ -1378,9 +1416,7 @@ def api_discover_ldap_base_dns(
|
||||
):
|
||||
_require_scope(principal, "addresses:sync:write")
|
||||
try:
|
||||
return AddressLdapDiscoveryResponse(
|
||||
base_dns=list(discover_ldap_base_dns(session, principal, payload))
|
||||
)
|
||||
return AddressLdapDiscoveryResponse(base_dns=list(discover_ldap_base_dns(session, principal, payload)))
|
||||
except (AddressBookError, AddressLdapError) as exc:
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
@@ -1438,7 +1474,11 @@ def api_list_address_credentials(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/address-books/{book_id}/carddav/sources", response_model=AddressSyncSourceResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-books/{book_id}/carddav/sources",
|
||||
response_model=AddressSyncSourceResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_carddav_sync_source(
|
||||
book_id: str,
|
||||
payload: AddressCardDavSourceCreateRequest,
|
||||
@@ -1454,7 +1494,11 @@ def api_create_carddav_sync_source(
|
||||
action="addresses.sync_source_created",
|
||||
object_type="address_sync_source",
|
||||
object_id=sync_source.id,
|
||||
details={"address_book_id": book_id, "connector_type": "carddav", "sync_direction": sync_source.sync_direction},
|
||||
details={
|
||||
"address_book_id": book_id,
|
||||
"connector_type": "carddav",
|
||||
"sync_direction": sync_source.sync_direction,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(sync_source)
|
||||
@@ -1473,7 +1517,12 @@ def api_list_sync_sources(
|
||||
):
|
||||
_require_scope(principal, "addresses:sync:read")
|
||||
try:
|
||||
sync_sources = list_sync_sources(session, principal, address_book_id=address_book_id, include_disabled=include_disabled)
|
||||
sync_sources = list_sync_sources(
|
||||
session,
|
||||
principal,
|
||||
address_book_id=address_book_id,
|
||||
include_disabled=include_disabled,
|
||||
)
|
||||
return AddressSyncSourceListResponse(sync_sources=[_sync_source_response(sync_source) for sync_source in sync_sources])
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
@@ -1575,7 +1624,11 @@ def api_run_sync_source(
|
||||
raise _error(AddressBookError(message)) from exc
|
||||
|
||||
|
||||
@router.post("/address-books/{book_id}/sync-sources", response_model=AddressSyncSourceResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-books/{book_id}/sync-sources",
|
||||
response_model=AddressSyncSourceResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_sync_source(
|
||||
book_id: str,
|
||||
payload: AddressSyncSourceCreateRequest,
|
||||
@@ -1591,7 +1644,11 @@ def api_create_sync_source(
|
||||
action="addresses.sync_source_created",
|
||||
object_type="address_sync_source",
|
||||
object_id=sync_source.id,
|
||||
details={"address_book_id": book_id, "connector_type": sync_source.connector_type, "sync_direction": sync_source.sync_direction},
|
||||
details={
|
||||
"address_book_id": book_id,
|
||||
"connector_type": sync_source.connector_type,
|
||||
"sync_direction": sync_source.sync_direction,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(sync_source)
|
||||
@@ -1617,7 +1674,11 @@ def api_update_sync_source(
|
||||
action="addresses.sync_source_updated",
|
||||
object_type="address_sync_source",
|
||||
object_id=sync_source.id,
|
||||
details={"connector_type": sync_source.connector_type, "sync_direction": sync_source.sync_direction, "enabled": sync_source.enabled},
|
||||
details={
|
||||
"connector_type": sync_source.connector_type,
|
||||
"sync_direction": sync_source.sync_direction,
|
||||
"enabled": sync_source.enabled,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(sync_source)
|
||||
@@ -1651,7 +1712,10 @@ def api_delete_sync_source(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/sync-sources/{sync_source_id}/attempts/start", response_model=AddressSyncSourceResponse)
|
||||
@router.post(
|
||||
"/sync-sources/{sync_source_id}/attempts/start",
|
||||
response_model=AddressSyncSourceResponse,
|
||||
)
|
||||
def api_start_sync_attempt(
|
||||
sync_source_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
@@ -1676,7 +1740,10 @@ def api_start_sync_attempt(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/sync-sources/{sync_source_id}/attempts/finish", response_model=AddressSyncSourceResponse)
|
||||
@router.post(
|
||||
"/sync-sources/{sync_source_id}/attempts/finish",
|
||||
response_model=AddressSyncSourceResponse,
|
||||
)
|
||||
def api_finish_sync_attempt(
|
||||
sync_source_id: str,
|
||||
payload: AddressSyncAttemptFinishRequest,
|
||||
@@ -1692,7 +1759,11 @@ def api_finish_sync_attempt(
|
||||
action="addresses.sync_finished",
|
||||
object_type="address_sync_source",
|
||||
object_id=sync_source.id,
|
||||
details={"connector_type": sync_source.connector_type, "status": sync_source.status, "error": sync_source.last_error},
|
||||
details={
|
||||
"connector_type": sync_source.connector_type,
|
||||
"status": sync_source.status,
|
||||
"error": sync_source.last_error,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(sync_source)
|
||||
@@ -1702,7 +1773,10 @@ def api_finish_sync_attempt(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/sync-sources/{sync_source_id}/diagnostics", response_model=AddressSyncDiagnosticListResponse)
|
||||
@router.get(
|
||||
"/sync-sources/{sync_source_id}/diagnostics",
|
||||
response_model=AddressSyncDiagnosticListResponse,
|
||||
)
|
||||
def api_list_sync_diagnostics(
|
||||
sync_source_id: str,
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
@@ -1717,7 +1791,11 @@ def api_list_sync_diagnostics(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/sync-sources/{sync_source_id}/diagnostics", response_model=AddressSyncDiagnosticResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/sync-sources/{sync_source_id}/diagnostics",
|
||||
response_model=AddressSyncDiagnosticResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_record_sync_diagnostic(
|
||||
sync_source_id: str,
|
||||
payload: AddressSyncDiagnosticCreateRequest,
|
||||
@@ -1735,7 +1813,10 @@ def api_record_sync_diagnostic(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/sync-sources/{sync_source_id}/tombstones", response_model=AddressSyncTombstoneListResponse)
|
||||
@router.get(
|
||||
"/sync-sources/{sync_source_id}/tombstones",
|
||||
response_model=AddressSyncTombstoneListResponse,
|
||||
)
|
||||
def api_list_sync_tombstones(
|
||||
sync_source_id: str,
|
||||
limit: int = Query(default=200, ge=1, le=1000),
|
||||
@@ -1750,7 +1831,11 @@ def api_list_sync_tombstones(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/sync-sources/{sync_source_id}/tombstones", response_model=AddressSyncTombstoneResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/sync-sources/{sync_source_id}/tombstones",
|
||||
response_model=AddressSyncTombstoneResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_record_sync_tombstone(
|
||||
sync_source_id: str,
|
||||
payload: AddressSyncTombstoneCreateRequest,
|
||||
@@ -1768,7 +1853,10 @@ def api_record_sync_tombstone(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/sync-sources/{sync_source_id}/conflicts", response_model=AddressSyncConflictListResponse)
|
||||
@router.get(
|
||||
"/sync-sources/{sync_source_id}/conflicts",
|
||||
response_model=AddressSyncConflictListResponse,
|
||||
)
|
||||
def api_list_sync_conflicts(
|
||||
sync_source_id: str,
|
||||
status_filter: str | None = Query(default="open", alias="status"),
|
||||
@@ -1784,7 +1872,11 @@ def api_list_sync_conflicts(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/sync-sources/{sync_source_id}/conflicts", response_model=AddressSyncConflictResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/sync-sources/{sync_source_id}/conflicts",
|
||||
response_model=AddressSyncConflictResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_record_sync_conflict(
|
||||
sync_source_id: str,
|
||||
payload: AddressSyncConflictCreateRequest,
|
||||
@@ -1818,7 +1910,11 @@ def api_resolve_sync_conflict(
|
||||
action="addresses.sync_conflict_resolved",
|
||||
object_type="address_sync_conflict",
|
||||
object_id=conflict.id,
|
||||
details={"sync_source_id": conflict.sync_source_id, "status": conflict.status, "resolution": conflict.resolution},
|
||||
details={
|
||||
"sync_source_id": conflict.sync_source_id,
|
||||
"status": conflict.status,
|
||||
"resolution": conflict.resolution,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(conflict)
|
||||
@@ -1828,7 +1924,11 @@ def api_resolve_sync_conflict(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/address-books/{book_id}/contacts", response_model=ContactResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-books/{book_id}/contacts",
|
||||
response_model=ContactResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_contact(
|
||||
book_id: str,
|
||||
payload: ContactCreateRequest,
|
||||
@@ -1869,11 +1969,7 @@ def api_update_contact(
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
previous_contact = session.get(Contact, contact_id)
|
||||
previous_point_details = (
|
||||
_contact_point_audit_details(previous_contact, prefix="previous")
|
||||
if previous_contact is not None
|
||||
else {}
|
||||
)
|
||||
previous_point_details = _contact_point_audit_details(previous_contact, prefix="previous") if previous_contact is not None else {}
|
||||
contact = update_contact(session, principal, contact_id, payload)
|
||||
audit_from_principal(
|
||||
session,
|
||||
@@ -1897,7 +1993,10 @@ def api_update_contact(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/contacts/{contact_id}/channel-rules", response_model=ContactChannelRuleListResponse)
|
||||
@router.get(
|
||||
"/contacts/{contact_id}/channel-rules",
|
||||
response_model=ContactChannelRuleListResponse,
|
||||
)
|
||||
def api_list_contact_channel_rules(
|
||||
contact_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
@@ -1905,12 +2004,7 @@ def api_list_contact_channel_rules(
|
||||
):
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
return ContactChannelRuleListResponse(
|
||||
rules=[
|
||||
_channel_rule_response(rule)
|
||||
for rule in list_contact_channel_rules(session, principal, contact_id)
|
||||
]
|
||||
)
|
||||
return ContactChannelRuleListResponse(rules=[_channel_rule_response(rule) for rule in list_contact_channel_rules(session, principal, contact_id)])
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
@@ -2044,7 +2138,11 @@ def api_restore_contact(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/address-books/{book_id}/vcards/import", response_model=VCardImportResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/address-books/{book_id}/vcards/import",
|
||||
response_model=VCardImportResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_import_address_book_vcards(
|
||||
book_id: str,
|
||||
payload: VCardImportRequest,
|
||||
@@ -2091,6 +2189,136 @@ def api_import_address_book_vcards(
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/address-books/{book_id}/vcard-batches/preview",
|
||||
response_model=VCardBatchRunResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_preview_vcard_batch(
|
||||
book_id: str,
|
||||
payload: VCardBatchPreviewRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
run = preview_vcard_batch(session, principal, book_id, payload)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.vcard_batch_previewed",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={
|
||||
"address_book_id": book_id,
|
||||
"input_hash": run.input_hash,
|
||||
"plan_hash": run.plan_hash,
|
||||
"statistics": run.statistics,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
return VCardBatchRunResponse.model_validate(vcard_batch_payload(run))
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/vcard-batches/{run_id}", response_model=VCardBatchRunResponse)
|
||||
def api_get_vcard_batch(
|
||||
run_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
try:
|
||||
return VCardBatchRunResponse.model_validate(vcard_batch_payload(get_vcard_batch_run(session, principal, run_id)))
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/vcard-batches/{run_id}/apply", response_model=VCardBatchRunResponse)
|
||||
def api_apply_vcard_batch(
|
||||
run_id: str,
|
||||
payload: VCardBatchCommitRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
run = apply_vcard_batch(session, principal, run_id, payload)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.vcard_batch_applied",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={
|
||||
"address_book_id": run.address_book_id,
|
||||
"input_hash": run.input_hash,
|
||||
"plan_hash": run.plan_hash,
|
||||
"statistics": run.statistics,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
return VCardBatchRunResponse.model_validate(vcard_batch_payload(run))
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/vcard-batches/{run_id}/cancel", response_model=VCardBatchRunResponse)
|
||||
def api_cancel_vcard_batch(
|
||||
run_id: str,
|
||||
payload: VCardBatchCancelRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
run = cancel_vcard_batch(session, principal, run_id, payload)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.vcard_batch_cancelled",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={"address_book_id": run.address_book_id, "reason": payload.reason},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
return VCardBatchRunResponse.model_validate(vcard_batch_payload(run))
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/vcard-batches/{run_id}/diagnostics")
|
||||
def api_export_vcard_batch_diagnostics(
|
||||
run_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
try:
|
||||
run = get_vcard_batch_run(session, principal, run_id)
|
||||
content = json.dumps(
|
||||
vcard_diagnostics_payload(run),
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": f'attachment; filename="vcard-batch-{run.id}.json"'},
|
||||
)
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/import-profiles", response_model=AddressImportProfileListResponse)
|
||||
def api_list_address_import_profiles(
|
||||
include_history: bool = Query(default=False),
|
||||
@@ -2098,15 +2326,14 @@ def api_list_address_import_profiles(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
return AddressImportProfileListResponse(
|
||||
profiles=[
|
||||
AddressImportProfileResponse.model_validate(item)
|
||||
for item in list_import_profiles(session, principal, include_history=include_history)
|
||||
]
|
||||
)
|
||||
return AddressImportProfileListResponse(profiles=[AddressImportProfileResponse.model_validate(item) for item in list_import_profiles(session, principal, include_history=include_history)])
|
||||
|
||||
|
||||
@router.post("/import-profiles", response_model=AddressImportProfileResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/import-profiles",
|
||||
response_model=AddressImportProfileResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_address_import_profile(
|
||||
payload: AddressImportProfileCreateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
@@ -2122,7 +2349,11 @@ def api_create_address_import_profile(
|
||||
action="addresses.import_profile_created",
|
||||
object_type="address_import_profile",
|
||||
object_id=profile.profile_key,
|
||||
details={"version": profile.version, "source_format": profile.source_format, "scope_type": profile.scope_type},
|
||||
details={
|
||||
"version": profile.version,
|
||||
"source_format": profile.source_format,
|
||||
"scope_type": profile.scope_type,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(profile)
|
||||
@@ -2149,7 +2380,10 @@ def api_update_address_import_profile(
|
||||
action="addresses.import_profile_versioned",
|
||||
object_type="address_import_profile",
|
||||
object_id=profile.profile_key,
|
||||
details={"version": profile.version, "source_format": profile.source_format},
|
||||
details={
|
||||
"version": profile.version,
|
||||
"source_format": profile.source_format,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(profile)
|
||||
@@ -2202,7 +2436,11 @@ def api_preview_address_import(
|
||||
action="addresses.import_previewed",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={"input_hash": run.input_hash, "plan_hash": run.plan_hash, "statistics": run.statistics},
|
||||
details={
|
||||
"input_hash": run.input_hash,
|
||||
"plan_hash": run.plan_hash,
|
||||
"statistics": run.statistics,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
@@ -2220,9 +2458,7 @@ def api_get_address_import_run(
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
try:
|
||||
return AddressImportRunResponse.model_validate(
|
||||
import_run_payload(get_import_run(session, principal, run_id))
|
||||
)
|
||||
return AddressImportRunResponse.model_validate(import_run_payload(get_import_run(session, principal, run_id)))
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
@@ -2244,7 +2480,11 @@ def api_apply_address_import(
|
||||
action="addresses.import_applied",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={"input_hash": run.input_hash, "plan_hash": run.plan_hash, "statistics": run.statistics},
|
||||
details={
|
||||
"input_hash": run.input_hash,
|
||||
"plan_hash": run.plan_hash,
|
||||
"statistics": run.statistics,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
@@ -2300,6 +2540,39 @@ def api_export_address_book_vcards(
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/address-books/{book_id}/vcards/export",
|
||||
response_model=VCardExportResponse,
|
||||
)
|
||||
def api_export_selected_vcards(
|
||||
book_id: str,
|
||||
payload: VCardExportRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
try:
|
||||
result = export_vcards(session, principal, book_id, payload)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.vcards_exported",
|
||||
object_type="address_book",
|
||||
object_id=book_id,
|
||||
details={
|
||||
"scope": result["scope"],
|
||||
"version": result["version"],
|
||||
"contact_count": result["contact_count"],
|
||||
"content_hash": result["content_hash"],
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return VCardExportResponse.model_validate(result)
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/contacts/{contact_id}/vcard")
|
||||
def api_export_contact_vcard(
|
||||
contact_id: str,
|
||||
|
||||
Reference in New Issue
Block a user