frimeet-api-nlp / app /modules /events /infrastructure /main_api_event_source.py
AlleksDev's picture
Fix: Sync endpoints
304d46b unverified
Raw
History Blame Contribute Delete
4.25 kB
from datetime import UTC, datetime
from typing import Any, AsyncIterator
from app.modules.events.infrastructure.semantic_document import (
EVENT_SEARCH_DOCUMENT_VERSION,
build_event_search_document,
)
from app.shared.config.settings import Settings
from app.shared.search.documents import as_text_list, first_present, make_search_record
from app.shared.search.source import PagedMainApiSearchClient, SearchSourceRecord
class MainApiEventsClient(PagedMainApiSearchClient):
def __init__(self, settings: Settings) -> None:
super().__init__(
settings=settings,
path=settings.main_api_events_snapshot_path,
collection_keys=("events",),
mapper=event_to_source_record,
page_limit=settings.main_api_search_page_limit,
pagination_mode=settings.main_api_search_pagination_mode,
)
async def iter_events(self, **kwargs: Any) -> AsyncIterator[SearchSourceRecord]:
async for record in self.iter_records(**kwargs):
yield record
def event_to_source_record(event: dict[str, Any]) -> SearchSourceRecord | None:
event_id = first_present(event, "id", "_id", "event_id", "uuid")
if event_id is None:
return None
title = str(first_present(event, "title", "name", default="")).strip()
description = str(first_present(event, "description", "summary", default="")).strip()
tag_names = as_text_list(first_present(event, "tags", default=[]))
tag_ids = as_text_list(first_present(event, "tag_ids", "tagIds", default=[]))
# Only textual tag names are semantic. UUID/numeric IDs stay as filters/metadata.
tag_names = [tag for tag in tag_names if not _looks_like_identifier(tag)]
start_time, duration_minutes = _normalized_temporal_metadata(event)
temporal_metadata_valid = start_time is not None and duration_minutes is not None
is_active = (
bool(first_present(event, "is_active", "isActive", default=True))
and temporal_metadata_valid
)
is_public = bool(first_present(event, "is_public", "isPublic", default=True))
document = build_event_search_document(title, tag_names, description)
return make_search_record(
resource_id=event_id,
document=document,
document_version=EVENT_SEARCH_DOCUMENT_VERSION,
is_active=is_active,
metadata={
"search_title": title,
"title": title,
"description": description[:300],
"club_id": first_present(event, "club_id", "clubId"),
"place_id": first_present(event, "place_id", "placeId"),
"start_time": start_time,
"duration_minutes": duration_minutes,
"max_attendees": first_present(event, "max_attendees", "maxAttendees"),
"is_public": is_public,
"cover_image_url": first_present(event, "cover_image_url", "coverImageUrl"),
"tags": tag_names,
"tag_ids": tag_ids,
"is_active": is_active,
"temporal_metadata_valid": temporal_metadata_valid,
"document_version": EVENT_SEARCH_DOCUMENT_VERSION,
},
)
def _looks_like_identifier(value: str) -> bool:
compact = value.replace("-", "")
return compact.isdigit() or (len(compact) == 32 and all(c in "0123456789abcdefABCDEF" for c in compact))
def _normalized_temporal_metadata(
event: dict[str, Any],
) -> tuple[str | None, int | None]:
raw_start = first_present(event, "start_time", "startTime")
if not isinstance(raw_start, str) or not raw_start.strip():
return None, None
try:
parsed_start = datetime.fromisoformat(
raw_start.strip().replace("Z", "+00:00")
)
except ValueError:
return None, None
if parsed_start.utcoffset() is None:
return None, None
raw_duration = first_present(event, "duration_minutes", "durationMinutes")
if isinstance(raw_duration, bool):
return None, None
try:
parsed_duration = int(raw_duration)
except (TypeError, ValueError):
return None, None
if parsed_duration <= 0 or parsed_duration > 2_147_483_647:
return None, None
return parsed_start.astimezone(UTC).isoformat(), parsed_duration