| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- """Bounded read-only business context for scheduling agents."""
- from app.core.orchestration.spec import (
- validate_schedule_plan as normalize_schedule_plan,
- )
- from app.core.orchestration.spec import (
- validate_workflow_spec as normalize_workflow_spec,
- )
- from app.core.orchestration.spec import workflow_spec_hash
- def _untrusted(value):
- if value is None:
- return None
- return {"trust": "untrusted", "value": str(value)[:2000]}
- def _page(items, maximum):
- bounded = list(items)[:maximum]
- return {
- "items": bounded,
- "count": len(bounded),
- "truncated": len(items) > maximum,
- }
- class ContextService:
- def __init__(self, repository):
- self.repository = repository
- def list_dataflows(self, identity, *, limit=50):
- if limit < 1 or limit > 50:
- raise ValueError("limit must be between 1 and 50")
- allowed = []
- for row in self.repository.list_dataflows():
- domain = row.get("business_domain")
- if (
- "*" not in identity.business_domains
- and domain not in identity.business_domains
- ):
- continue
- allowed.append(
- {
- "uid": row.get("uid"),
- "name": row.get("name"),
- "business_domain": domain,
- "description": _untrusted(row.get("description")),
- }
- )
- return _page(allowed, limit)
- def describe_dataflow(self, identity, dataflow_uid, *, business_domain):
- identity.require_domain(business_domain)
- row = self.repository.describe_dataflow(dataflow_uid)
- if row.get("business_domain") != business_domain:
- raise PermissionError("dataflow is outside identity scope")
- return {
- "uid": row.get("uid"),
- "name": row.get("name"),
- "business_domain": row.get("business_domain"),
- "status": row.get("status"),
- "description": _untrusted(row.get("description")),
- }
- def get_dataflow_dependencies(self, identity, dataflow_uid, *, business_domain):
- identity.require_domain(business_domain)
- safe = []
- for row in self.repository.get_dataflow_dependencies(dataflow_uid):
- if row.get("business_domain") != business_domain:
- continue
- safe.append(
- {
- "uid": row.get("uid"),
- "name": row.get("name"),
- "relation": row.get("relation"),
- "business_domain": row.get("business_domain"),
- }
- )
- return _page(safe, 100)
- def get_data_lineage(
- self,
- identity,
- dataflow_uid,
- *,
- business_domain,
- depth=3,
- ):
- identity.require_domain(business_domain)
- if (
- isinstance(depth, bool)
- or not isinstance(depth, int)
- or depth < 1
- or depth > 5
- ):
- raise ValueError("lineage depth must be between 1 and 5")
- safe = []
- for row in self.repository.get_data_lineage(dataflow_uid, depth):
- safe.append(
- {
- "from_uid": row.get("from_uid"),
- "to_uid": row.get("to_uid"),
- "relation": row.get("relation"),
- "description": _untrusted(row.get("description")),
- }
- )
- return _page(safe, 100)
- def list_datasource_capabilities(self, identity, business_domain):
- identity.require_domain(business_domain)
- safe = []
- for row in self.repository.list_datasource_capabilities(business_domain):
- safe.append(
- {
- "uid": row.get("uid"),
- "type": row.get("type"),
- "purposes": list(row.get("purposes") or [])[:10],
- "health": row.get("health"),
- "capacity": {
- key: value
- for key, value in dict(row.get("capacity") or {}).items()
- if key
- in {
- "available",
- "pool_size",
- "checked_out",
- "max_concurrency",
- }
- },
- }
- )
- return _page(safe, 50)
- def get_datasource_pool_health(
- self,
- identity,
- data_source_uid,
- *,
- business_domain,
- ):
- identity.require_domain(business_domain)
- row = self.repository.get_datasource_pool_health(data_source_uid)
- return {
- "uid": row.get("uid"),
- "health": row.get("health"),
- "capacity": {
- key: value
- for key, value in dict(row.get("capacity") or {}).items()
- if key
- in {
- "available",
- "pool_size",
- "checked_out",
- "max_concurrency",
- }
- },
- }
- def get_execution_history(
- self,
- identity,
- dataflow_uid,
- *,
- business_domain,
- limit=20,
- days=7,
- ):
- identity.require_domain(business_domain)
- if limit < 1 or limit > 100:
- raise ValueError("limit must be between 1 and 100")
- if days < 1 or days > 30:
- raise ValueError("days must be between 1 and 30")
- safe = []
- for row in self.repository.get_execution_history(
- dataflow_uid,
- limit,
- days,
- ):
- safe.append(
- {
- "status": row.get("status"),
- "correlation_id": row.get("correlation_id"),
- "started_at": row.get("started_at"),
- "finished_at": row.get("finished_at"),
- "safe_error": _untrusted(row.get("safe_error")),
- }
- )
- return _page(safe, limit)
- def get_sla_constraints(self, identity, dataflow_uid, *, business_domain):
- identity.require_domain(business_domain)
- row = self.repository.get_sla_constraints(dataflow_uid)
- return {
- key: row.get(key)
- for key in (
- "timezone",
- "max_duration_seconds",
- "deadline",
- "priority",
- )
- if key in row
- }
- def estimate_schedule_capacity(
- self,
- identity,
- *,
- business_domain,
- schedule_plan,
- ):
- identity.require_domain(business_domain)
- plan = normalize_schedule_plan(schedule_plan)
- row = self.repository.estimate_schedule_capacity(business_domain, plan)
- return {
- "feasible": bool(row.get("feasible", False)),
- "required_slots": row.get("required_slots"),
- "available_slots": row.get("available_slots"),
- "warnings": [
- _untrusted(value) for value in list(row.get("warnings") or [])[:20]
- ],
- }
- def validate_workflow_spec(self, identity, workflow_spec):
- del identity
- normalized = normalize_workflow_spec(workflow_spec)
- return {
- "valid": True,
- "workflow_spec": normalized,
- "workflow_hash": workflow_spec_hash(normalized),
- }
- def validate_schedule_plan(self, identity, schedule_plan):
- del identity
- return {
- "valid": True,
- "schedule_plan": normalize_schedule_plan(schedule_plan),
- }
- def simulate_schedule(
- self,
- identity,
- *,
- business_domain,
- schedule_plan,
- ):
- identity.require_domain(business_domain)
- plan = normalize_schedule_plan(schedule_plan)
- row = self.repository.simulate_schedule(business_domain, plan)
- return {
- "next_runs": list(row.get("next_runs") or [])[:20],
- "warnings": [
- _untrusted(value) for value in list(row.get("warnings") or [])[:20]
- ],
- }
- def compare_execution_results(
- self,
- identity,
- *,
- business_domain,
- baseline_execution_id,
- candidate_execution_id,
- ):
- identity.require_domain(business_domain)
- row = self.repository.compare_execution_results(
- baseline_execution_id,
- candidate_execution_id,
- )
- metrics = {
- key: value
- for key, value in dict(row.get("metrics") or {}).items()
- if key
- in {
- "row_count_delta",
- "duration_delta_seconds",
- "error_count_delta",
- "output_hash_match",
- }
- and (value is None or isinstance(value, (bool, int, float)))
- }
- return {
- "equivalent": bool(row.get("equivalent", False)),
- "metrics": metrics,
- "differences": [
- _untrusted(value) for value in list(row.get("differences") or [])[:50]
- ],
- }
|