2026-03-26 21:57:56 +00:00
|
|
|
"""Temporal activity definitions for activity-core.
|
|
|
|
|
|
|
|
|
|
Activities run inside a Worker bound to 'orchestrator-tq'.
|
|
|
|
|
Each function is decorated with @activity.defn and executed by
|
|
|
|
|
RunActivityWorkflow via workflow.execute_activity().
|
|
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
DB access pattern: worker.py calls init_session_factory(url) once before
|
|
|
|
|
starting workers, which sets the module-level _session_factory used by
|
|
|
|
|
activities that need DB access.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
2026-03-26 21:57:56 +00:00
|
|
|
from temporalio import activity
|
2026-03-26 22:02:15 +00:00
|
|
|
from temporalio.exceptions import ApplicationError
|
|
|
|
|
|
|
|
|
|
from activity_core.db import make_engine
|
|
|
|
|
from activity_core.orm import ActivityDefinition as ActivityDefinitionRow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_session_factory: async_sessionmaker[AsyncSession] | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_session_factory(url: str) -> None:
|
|
|
|
|
"""Initialise the shared DB session factory.
|
|
|
|
|
|
|
|
|
|
Must be called once from worker.py before workers are started.
|
|
|
|
|
"""
|
|
|
|
|
global _session_factory
|
|
|
|
|
_session_factory = async_sessionmaker(make_engine(url), expire_on_commit=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_session_factory() -> async_sessionmaker[AsyncSession]:
|
|
|
|
|
if _session_factory is None:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"DB session factory not initialised — call init_session_factory() first"
|
|
|
|
|
)
|
|
|
|
|
return _session_factory
|
|
|
|
|
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
# ── Activities ─────────────────────────────────────────────────────────────────
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def load_activity_definition(activity_id: str) -> dict:
|
2026-03-26 22:02:15 +00:00
|
|
|
"""Load an ActivityDefinition row from Postgres by ID.
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
Returns a JSON-serialisable dict suitable for passing between
|
|
|
|
|
Temporal workflow steps.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ApplicationError (non-retryable): if no row exists for activity_id.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
2026-03-26 22:02:15 +00:00
|
|
|
Session = _get_session_factory()
|
|
|
|
|
async with Session() as session:
|
|
|
|
|
row = await session.scalar(
|
|
|
|
|
select(ActivityDefinitionRow).where(
|
|
|
|
|
ActivityDefinitionRow.id == uuid.UUID(activity_id)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if row is None:
|
|
|
|
|
raise ApplicationError(
|
|
|
|
|
f"ActivityDefinition {activity_id!r} not found",
|
|
|
|
|
non_retryable=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"id": str(row.id),
|
|
|
|
|
"name": row.name,
|
|
|
|
|
"enabled": row.enabled,
|
|
|
|
|
"trigger_type": row.trigger_type,
|
|
|
|
|
"trigger_config": row.trigger_config,
|
|
|
|
|
"context_sources": row.context_sources,
|
|
|
|
|
"task_templates": row.task_templates,
|
|
|
|
|
"dedupe_key_strategy": row.dedupe_key_strategy,
|
|
|
|
|
"version": row.version,
|
|
|
|
|
}
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def resolve_context(context_sources: list[dict]) -> dict:
|
2026-03-26 22:06:09 +00:00
|
|
|
"""Resolve each context source and merge into a snapshot dict.
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:06:09 +00:00
|
|
|
Returns: {source.name: resolved_value, ...}
|
|
|
|
|
|
|
|
|
|
Supported source types:
|
|
|
|
|
static — returns config["value"] directly
|
|
|
|
|
http_get — not yet implemented
|
|
|
|
|
db_query — not yet implemented
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
2026-03-26 22:06:09 +00:00
|
|
|
snapshot: dict = {}
|
|
|
|
|
for source in context_sources:
|
|
|
|
|
name = source["name"]
|
|
|
|
|
source_type = source["type"]
|
|
|
|
|
config = source.get("config", {})
|
|
|
|
|
|
|
|
|
|
if source_type == "static":
|
|
|
|
|
snapshot[name] = config.get("value")
|
|
|
|
|
elif source_type in ("http_get", "db_query"):
|
|
|
|
|
raise ApplicationError(
|
|
|
|
|
f"Context source type {source_type!r} is not yet implemented",
|
|
|
|
|
non_retryable=True,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise ApplicationError(
|
|
|
|
|
f"Unknown context source type {source_type!r}",
|
|
|
|
|
non_retryable=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return snapshot
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def log_run(run_payload: dict) -> str:
|
|
|
|
|
"""Persist an ActivityRun record and return its run_id.
|
|
|
|
|
|
|
|
|
|
Implemented in T17.
|
|
|
|
|
"""
|
|
|
|
|
raise NotImplementedError("T17")
|