"""Backend facade that composes all service modules.""" from __future__ import annotations from pathlib import Path import threading from typing import Any from sqlalchemy import event as sa_event from sqlmodel import SQLModel, create_engine from backend import catalog as _catalog_mod from backend import content_types as _content_types_mod from backend import db as _db_mod from backend import enrichment as _enrichment_mod from backend import fetlife_data as _fetlife_data_mod from backend import fetlife_queue as _fetlife_queue_mod from backend import ingestion as _ingestion_mod from backend import partners as _partners_mod from backend import recommendations as _recommendations_mod from backend import scenarios as _scenarios_mod from backend import search as _search_mod from backend import shared_play as _shared_play_mod from backend import similarity as _similarity_mod from backend import users as _users_mod from backend.models import RecsysSettings from backend.recsys_env_overrides import load_recsys_settings class Backend: def __init__(self, path: Path, *, recsys_settings: RecsysSettings | None = None) -> None: self.path = path self.recsys_settings = load_recsys_settings(path, recsys_settings) self.path.parent.mkdir(parents=True, exist_ok=True) from sqlalchemy.pool import NullPool self.engine = create_engine( f"sqlite:///{self.path}", echo=False, connect_args={"timeout": 30, "check_same_thread": False}, poolclass=NullPool, ) @sa_event.listens_for(self.engine, "connect") def _set_sqlite_pragmas(dbapi_connection, _connection_record) -> None: cursor = dbapi_connection.cursor() cursor.execute("PRAGMA journal_mode=WAL") cursor.execute("PRAGMA synchronous=NORMAL") cursor.execute("PRAGMA busy_timeout=30000") cursor.execute("PRAGMA foreign_keys=ON") cursor.close() SQLModel.metadata.create_all(self.engine) self._migrate_user_table() self._migrate_legacy_ratings() self._migrate_partner_group_member() self._migrate_duplicate_pair_groups() self._seed_type_overrides() self._ensure_runtime_indexes() self._catalog_cache: dict[str, Any] | None = None self._catalog_token: tuple[Any, ...] | None = None self._catalog_build_lock = threading.Lock() self._last_cache_invalidation_at = 0.0 self._ppr_graph_cache: Any = None self._ppr_transition_cache: Any = None self._top_edges_by_source_cache: dict[str, list[dict[str, Any]]] = {} self._ensure_fts_schema() def invalidate_recommendation_cache(self) -> None: """Compatibility hook for play mutations. Recommendations are recomputed against fresh user state on every call; the old per-process HTTP cache was removed because it could serve stale decks in multi-process deployments. """ return def peek_catalog_kink_id(self) -> str | None: """Return any stored kink id (single-row probe for deploy smoke tests).""" from sqlmodel import Session, select from models import Kink with Session(self.engine) as session: row = session.exec(select(Kink.id).limit(1)).first() return str(row) if row is not None else None # --- db --- _sqlite = _db_mod._sqlite _now_iso = _db_mod._now_iso _ensure_fts_schema = _db_mod._ensure_fts_schema _ensure_runtime_indexes = _db_mod._ensure_runtime_indexes _normalize_interest_state = _db_mod._normalize_interest_state _migrate_user_table = _db_mod._migrate_user_table _migrate_partner_group_member = _db_mod._migrate_partner_group_member _migrate_duplicate_pair_groups = _db_mod._migrate_duplicate_pair_groups _migrate_legacy_ratings = _db_mod._migrate_legacy_ratings # --- similarity --- _rebuild_similarity_edges = _similarity_mod._rebuild_similarity_edges rebuild_similarity_edges = _similarity_mod.rebuild_similarity_edges _edges_for_kink = _similarity_mod._edges_for_kink add_similarity_edge = _similarity_mod.add_similarity_edge # --- content_types --- _seed_type_overrides = _content_types_mod._seed_type_overrides _derive_content_kind = _content_types_mod._derive_content_kind _sync_content_types = _content_types_mod._sync_content_types _content_type_map = _content_types_mod._content_type_map _content_kind = _content_types_mod._content_kind # --- enrichment --- _is_role_query = _enrichment_mod._is_role_query _discoverability_penalty = _enrichment_mod._discoverability_penalty _looks_role_like = _enrichment_mod._looks_role_like _looks_play_like = _enrichment_mod._looks_play_like _display_summary = _enrichment_mod._display_summary _normalized_play_name = _enrichment_mod._normalized_play_name _canonical_preference_tuple = _enrichment_mod._canonical_preference_tuple _normalize_directions = _enrichment_mod._normalize_directions _serialize_directions = _enrichment_mod._serialize_directions _deserialize_directions = _enrichment_mod._deserialize_directions _direction_mode_for_kink = _enrichment_mod._direction_mode_for_kink _default_directions_for_kink = _enrichment_mod._default_directions_for_kink _parse_count_value = _enrichment_mod._parse_count_value _starter_score = _enrichment_mod._starter_score _starter_reason_for_kink = _enrichment_mod._starter_reason_for_kink _starter_tier_for_kink = _enrichment_mod._starter_tier_for_kink _starter_tier_rank = _enrichment_mod._starter_tier_rank _detail_summary_text = _enrichment_mod._detail_summary_text _image_trust_state = _enrichment_mod._image_trust_state _image_relevance = _enrichment_mod._image_relevance _derived_product_flags = _enrichment_mod._derived_product_flags _starter_candidates = _enrichment_mod._starter_candidates _extract_popularity_from_notes = _enrichment_mod._extract_popularity_from_notes slugify = _enrichment_mod.slugify stable_fragment = _enrichment_mod.stable_fragment _make_memorable_id = _enrichment_mod._make_memorable_id _make_memorable_pass = _enrichment_mod._make_memorable_pass # --- catalog --- _invalidate_catalog_cache = _catalog_mod._invalidate_catalog_cache _build_catalog_cache = _catalog_mod._build_catalog_cache _mark_runtime_duplicates = _catalog_mod._mark_runtime_duplicates _rebuild_fts_index = _catalog_mod._rebuild_fts_index _maybe_rebuild_fts_index = _catalog_mod._maybe_rebuild_fts_index _catalog = _catalog_mod._catalog catalog_is_ready = _catalog_mod.catalog_is_ready warm_up_catalog = _catalog_mod.warm_up_catalog refresh_cache = _catalog_mod.refresh_cache # --- search --- _query_tokens = _search_mod._query_tokens _fts_query = _search_mod._fts_query _fallback_doc_search = _search_mod._fallback_doc_search search_kinks = _search_mod.search_kinks search_roles = _search_mod.search_roles list_kinks = _search_mod.list_kinks list_kink_summaries = _search_mod.list_kink_summaries get_kink = _search_mod.get_kink get_kinks_batch = _search_mod.get_kinks_batch similar_kinks = _search_mod.similar_kinks _card_for_kink = _search_mod._card_for_kink # --- ingestion --- upsert_source = _ingestion_mod.upsert_source match_kink_id = _ingestion_mod.match_kink_id ensure_kink = _ingestion_mod.ensure_kink add_alias = _ingestion_mod.add_alias add_definition = _ingestion_mod.add_definition add_example = _ingestion_mod.add_example add_asset = _ingestion_mod.add_asset upsert_fetlife_picture_ref = _ingestion_mod.upsert_fetlife_picture_ref catalog_stats = _ingestion_mod.catalog_stats # --- users --- create_user = _users_mod.create_user _load_play_preferences = _users_mod._load_play_preferences _load_role_preferences = _users_mod._load_role_preferences get_user = _users_mod.get_user authenticate_user = _users_mod.authenticate_user all_users = _users_mod.all_users delete_user = _users_mod.delete_user export_user_data = _users_mod.export_user_data partner_activity_hash = _users_mod.partner_activity_hash save_preferences = _users_mod.save_preferences remove_play_preference = _users_mod.remove_play_preference save_play_preference = _users_mod.save_play_preference save_role_preference = _users_mod.save_role_preference role_cards = _users_mod.role_cards link_users = _users_mod.link_users request_partner_link = _users_mod.request_partner_link accept_partner_request = _users_mod.accept_partner_request decline_partner_request = _users_mod.decline_partner_request cancel_partner_request = _users_mod.cancel_partner_request dismiss_prompt = _users_mod.dismiss_prompt play_board = _users_mod.play_board # --- partners --- _linked_partner_ids = _partners_mod._linked_partner_ids list_partner_groups = _partners_mod.list_partner_groups ensure_pair_partner_groups = _partners_mod.ensure_pair_partner_groups create_partner_group = _partners_mod.create_partner_group add_partner_group_members = _partners_mod.add_partner_group_members remove_partner_group_member = _partners_mod.remove_partner_group_member _group_participants = _partners_mod._group_participants can_access_partner_group = _partners_mod.can_access_partner_group toggle_share_full_list = _partners_mod.toggle_share_full_list partner_full_board = _partners_mod.partner_full_board # --- scenarios --- _load_scenario_preferences = _scenarios_mod._load_scenario_preferences scenario_parent_links = _scenarios_mod.scenario_parent_links list_scenarios_for_parent = _scenarios_mod.list_scenarios_for_parent scenario_parents_for_user = _scenarios_mod.scenario_parents_for_user upsert_scenario_parent = _scenarios_mod.upsert_scenario_parent candidate_scenario_parent_links = _scenarios_mod.candidate_scenario_parent_links save_scenario_preference = _scenarios_mod.save_scenario_preference remove_scenario_preference = _scenarios_mod.remove_scenario_preference # --- recommendations --- _recommendation_item = _recommendations_mod._recommendation_item _finalize_reasons = _recommendations_mod._finalize_reasons _strip_rec_sampler_meta = _recommendations_mod._strip_rec_sampler_meta _stochastic_select_recommendations = _recommendations_mod._stochastic_select_recommendations _starter_recommendations = _recommendations_mod._starter_recommendations _main_recommendation_target = _recommendations_mod._main_recommendation_target _legacy_edge_sum_scores = _recommendations_mod._legacy_edge_sum_scores _negative_edge_adjustments = _recommendations_mod._negative_edge_adjustments _positive_seed_reasons_only = _recommendations_mod._positive_seed_reasons_only _personalized_recommendations = _recommendations_mod._personalized_recommendations _dedupe_recommendations = _recommendations_mod._dedupe_recommendations _take_unique_kink_items = _recommendations_mod._take_unique_kink_items _explore_backfill = _recommendations_mod._explore_backfill _blended_recommendations = _recommendations_mod._blended_recommendations _partner_influenced_recommendations = _recommendations_mod._partner_influenced_recommendations _load_ots_candidate_cache = _recommendations_mod._load_ots_candidate_cache _ots_recommendations = _recommendations_mod._ots_recommendations _graph_recommend = _recommendations_mod._graph_recommend recommend = _recommendations_mod.recommend recommendation_debug = _recommendations_mod.recommendation_debug _group_recommendations = _recommendations_mod._group_recommendations # --- shared_play --- _directions_compatible = _shared_play_mod._directions_compatible _directions_compatible_group = _shared_play_mod._directions_compatible_group _safe_prompt_candidate = _shared_play_mod._safe_prompt_candidate _direct_shared_ids = _shared_play_mod._direct_shared_ids _similarity_edges_by_source = _shared_play_mod._similarity_edges_by_source _positive_scenario_states = _shared_play_mod._positive_scenario_states _related_direction_compatible = _shared_play_mod._related_direction_compatible _related_match_entry = _shared_play_mod._related_match_entry _related_positive_matches_for_pair = _shared_play_mod._related_positive_matches_for_pair _related_positive_matches_for_users = _shared_play_mod._related_positive_matches_for_users _scenario_direct_matches_for_users = _shared_play_mod._scenario_direct_matches_for_users _scenario_related_matches_for_pair = _shared_play_mod._scenario_related_matches_for_pair _scenario_related_matches_for_users = _shared_play_mod._scenario_related_matches_for_users _pair_safe_similarity_floor = _shared_play_mod._pair_safe_similarity_floor _direct_shared_ids_for_users = _shared_play_mod._direct_shared_ids_for_users _shared_play_list_for_users = _shared_play_mod._shared_play_list_for_users shared_play_list = _shared_play_mod.shared_play_list shared_play_list_for_group = _shared_play_mod.shared_play_list_for_group prompt_candidates = _shared_play_mod.prompt_candidates group_prompt_candidates = _shared_play_mod.group_prompt_candidates # --- fetlife_queue --- queue_fetlife_job = _fetlife_queue_mod.queue_fetlife_job start_fetlife_supervisor_run = _fetlife_queue_mod.start_fetlife_supervisor_run heartbeat_fetlife_supervisor = _fetlife_queue_mod.heartbeat_fetlife_supervisor finish_fetlife_supervisor_run = _fetlife_queue_mod.finish_fetlife_supervisor_run _process_is_live = _fetlife_queue_mod._process_is_live fetlife_supervisor_status = _fetlife_queue_mod.fetlife_supervisor_status requeue_stale_fetlife_jobs = _fetlife_queue_mod.requeue_stale_fetlife_jobs claim_fetlife_jobs = _fetlife_queue_mod.claim_fetlife_jobs complete_fetlife_job = _fetlife_queue_mod.complete_fetlife_job fetlife_job_stats = _fetlife_queue_mod.fetlife_job_stats fetlife_jobs = _fetlife_queue_mod.fetlife_jobs fetlife_queue_preview = _fetlife_queue_mod.fetlife_queue_preview queue_fetlife_priority_debt = _fetlife_queue_mod.queue_fetlife_priority_debt # --- fetlife_data --- upsert_fetlife_kink_meta = _fetlife_data_mod.upsert_fetlife_kink_meta fetlife_source_coverage = _fetlife_data_mod.fetlife_source_coverage fetlife_coverage_debt = _fetlife_data_mod.fetlife_coverage_debt fetlife_data_health = _fetlife_data_mod.fetlife_data_health content_type_coverage = _fetlife_data_mod.content_type_coverage upsert_fetlife_user_sample = _fetlife_data_mod.upsert_fetlife_user_sample replace_fetlife_user_fetishes = _fetlife_data_mod.replace_fetlife_user_fetishes rebuild_fetlife_collaborative_edges = _fetlife_data_mod.rebuild_fetlife_collaborative_edges fetlife_sample_coverage = _fetlife_data_mod.fetlife_sample_coverage