monish563/NU-KIOSK-API
0
1"""Utility class that coordinates blueprint execution across data tables."""2 3from __future__ import annotations4 5from datetime import datetime6from typing import Any, Callable, Dict, Iterable, List, Optional7 8from ..data.catalog import DataCatalog9from .base import AnalysisContext, Blueprint, BlueprintResult, normalize_datetime10 11 12class AnalysisEngine:13 """14 Lightweight executor that keeps Satyrn-inspired components wired together.15 16 - Maintains a registry of blueprints.17 - Provides normalized event data to blueprints that need it.18 - Offers a fetch hook to refresh PlanIt Purple feeds.19 """20 21 def __init__(self, catalog: DataCatalog, blueprints: Optional[Iterable[Blueprint]] = None) -> None:22 self.catalog = catalog23 self._blueprints: Dict[str, Blueprint] = {}24 self._events: List[Dict[str, Any]] = []25 if blueprints:26 for blueprint in blueprints:27 self.register_blueprint(blueprint)28 29 def register_blueprint(self, blueprint: Blueprint) -> None:30 self._blueprints[blueprint.name] = blueprint31 32 def run(self, blueprint_name: str, **params: Any) -> BlueprintResult:33 if blueprint_name not in self._blueprints:34 raise KeyError(f"Blueprint '{blueprint_name}' not registered.")35 context = AnalysisContext(catalog=self.catalog, events=self._events)36 return self._blueprints[blueprint_name].run(context, **params)37 38 # ------------------------------------------------------------------ #39 # Event management40 # ------------------------------------------------------------------ #41 def refresh_events(self, fetch_fn: Optional[Callable[[List[str]], List[Dict[str, Any]]]] = None) -> int:42 """43 Update the in-memory event cache by downloading PlanIt Purple feeds.44 45 A custom fetch function can be provided for testing or offline use.46 Returns the number of events in the cache.47 """48 feed_config = self.catalog.metadata.get("event_feeds")49 urls = feed_config.get("urls") if isinstance(feed_config, dict) else None50 if not urls:51 self._events = []52 return 053 54 fetcher = fetch_fn or self._default_fetch55 events = fetcher(urls)56 events.sort(key=lambda evt: evt.get("start") or datetime.max)57 self._events = events58 return len(self._events)59 60 # Internal helpers -------------------------------------------------- #61 def _default_fetch(self, urls: List[str]) -> List[Dict[str, Any]]:62 try:63 import requests # type: ignore64 except ImportError:65 return []66 67 events: List[Dict[str, Any]] = []68 for url in urls:69 try:70 response = requests.get(url, timeout=10)71 response.raise_for_status()72 payload = response.json()73 except Exception:74 continue75 events.extend(self._normalize_events(payload, source=url))76 return events77 78 def _normalize_events(self, payload: Any, *, source: str) -> List[Dict[str, Any]]:79 raw_events: List[Dict[str, Any]] = []80 if isinstance(payload, dict):81 if isinstance(payload.get("events"), list):82 raw_events = payload["events"]83 elif isinstance(payload.get("items"), list):84 raw_events = payload["items"]85 else:86 raw_events = [payload]87 elif isinstance(payload, list):88 raw_events = payload89 else:90 return []91 92 normalized: List[Dict[str, Any]] = []93 for item in raw_events:94 if not isinstance(item, dict):95 continue96 start_value = (97 item.get("start")98 or item.get("startDate")99 or item.get("start_date")100 or item.get("start_datetime")101 )102 end_value = item.get("end") or item.get("endDate") or item.get("end_date") or item.get("end_datetime")103 normalized.append(104 {105 "title": item.get("title") or item.get("summary") or item.get("name") or "Untitled Event",106 "start": normalize_datetime(start_value),107 "end": normalize_datetime(end_value),108 "location": item.get("location") or item.get("location_name"),109 "description": item.get("description") or item.get("summary"),110 "url": item.get("url") or item.get("permalink") or source,111 "tags": item.get("tags") or item.get("keywords"),112 "source": source,113 "raw": item,114 }115 )116 return normalized117 