fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
c14ceee verified
Raw
History Blame Contribute Delete
5.52 kB
"""Harness core β€” the platform contract that turns 'a dashboard' into 'a product'.
Three concepts:
Connector a source adapter (Odoo / QuickBooks / Amazon / …). Declares its capabilities and
answers canonical Queries. Adding a source = subclass Connector.
Tenant one client (Royal Imports, …). Bundles the connectors they've hooked up + their scope
config (business units, excluded accounts, channel map, branding). Multi-tenant lives here.
Skill a portable dashboard/metric written ONCE against the canonical model; it declares the
capabilities it needs so a tenant only ever sees skills its sources can serve.
The UI (app.py) becomes a thin renderer over (tenant β†’ available skills). Nothing above the
connector layer knows what Odoo is.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Callable
from .canonical import Capability, Query, Entity
class Connector(ABC):
"""A data source. Implement `capabilities()` + `aggregate()` (and optionally `records()`), map the
canonical model to the source, and the source is usable by every compatible skill."""
key: str = 'connector'
name: str = 'Connector'
@abstractmethod
def capabilities(self) -> set[Capability]:
"""The canonical capabilities this source can serve for the given account."""
@abstractmethod
def aggregate(self, q: Query) -> list[dict]:
"""Answer an aggregate Query β†’ list of canonical dicts (keys = measures/group_by, + 'count')."""
def records(self, q: Query) -> list[dict]:
"""Optional row-level fetch (canonical dicts). Default: not supported."""
raise NotImplementedError(f'{self.key}: row-level records() not implemented')
def distinct_count(self, q: Query, fieldname: str) -> int:
"""Number of distinct values of a canonical field over the query's scope (e.g. active
customers). Default: derive from an aggregate group_by; connectors may override efficiently."""
from dataclasses import replace
rows = self.aggregate(replace(q, measures=(), aggregate='count', group_by=(fieldname,)))
return sum(1 for r in rows if r.get(fieldname) not in (None, False))
def supports(self, cap: Capability) -> bool:
return cap in self.capabilities()
def entities(self) -> set[Entity]:
"""Canonical entities this connector can materialize (advertised by subclasses)."""
return set()
@dataclass
class Tenant:
"""One client of the platform. `sources` maps a connector key β†’ Connector instance. `config` holds
the client's scope semantics (which is what used to be hard-coded in core/odoo.py):
business_units {label: source_scope_value} e.g. {'Fisch': 5, 'Royal': 6}
confirmed_statuses [...] which order states count as 'real'
excluded_customers [ids] house / inter-co accounts to drop
channels {label: match} e.g. {'Amazon': <partner>} (marketplace split)
branding {...} palette, name, logo
A skill reads tenant.config for scope and asks tenant.source(capability) for the right connector."""
key: str
name: str
sources: dict[str, Connector] = field(default_factory=dict)
config: dict = field(default_factory=dict)
def add(self, conn: Connector) -> 'Tenant':
self.sources[conn.key] = conn
return self
def capabilities(self) -> set[Capability]:
caps: set[Capability] = set()
for c in self.sources.values():
caps |= c.capabilities()
return caps
def source(self, cap: Capability, prefer: str | None = None) -> Connector:
"""The connector that serves `cap` (a tenant may hook several sources; `prefer` picks one,
else first that supports it). Raises if no connected source provides the capability."""
if prefer and prefer in self.sources and self.sources[prefer].supports(cap):
return self.sources[prefer]
for c in self.sources.values():
if c.supports(cap):
return c
raise LookupError(f"tenant {self.key!r} has no source for capability {cap.value!r}")
def has(self, *caps: Capability) -> bool:
mine = self.capabilities()
return all(c in mine for c in caps)
# --------------------------------------------------------------------------- skill registry
@dataclass(frozen=True)
class Skill:
"""A portable dashboard. `requires` are the capabilities it needs; the harness only offers it to a
tenant whose connected sources cover them. `render` is the UI entrypoint (given tenant + context)."""
key: str
label: str
requires: tuple[Capability, ...]
render: Callable | None = None
hq: bool = False # consolidated / company-level (ignores the business-unit filter)
def available_for(self, tenant: Tenant) -> bool:
return tenant.has(*self.requires)
_SKILLS: dict[str, Skill] = {}
def register_skill(skill: Skill) -> Skill:
_SKILLS[skill.key] = skill
return skill
def skills_for(tenant: Tenant) -> list[Skill]:
"""The skills a tenant can actually run, given its connected sources β€” this is the dynamic nav."""
return [s for s in _SKILLS.values() if s.available_for(tenant)]
def all_skills() -> list[Skill]:
return list(_SKILLS.values())