SIENTIAPDE-1974 Write OPC tags through the tag database and drop the PI Web API path
- predictions_batch write_tags are now [{tag_id}] only: resolve_write_tags builds
the opc_output_config from connector_tags + connectors (server = connector_id,
Write / Prediction Confidence -> prediction_tags / confidence_tags, address and
data_type from the tag, float by default). The legacy server_id/type/addr write
tags are no longer read and are notified as invalid.
- A pipeline's effective updated_at is the latest of its own and of the
connector_tags it references, so renaming, moving or deactivating a tag
redeploys it (removes the rename limitation).
- The connector_tags query loads the referenced tags of every category and
status; the code filters them. aggregate_documents_in_mongodb formats only
datetime timestamp fields.
- Remove the PI Web API tag database path (connectors-pi-web-api patch, PI loads,
resolution, connection and held schedules): PI pipelines behave as on main.
- tag_id is canonical text and the workflow-side id collection never raises;
a malformed pipeline no longer fails the whole OPC slot.
- Replay test against histories recorded on the e2e stack (main and this
version), e2e scenarios for write tags and tag changes, and the runbook
script scripts/sientiapde_1974_backfill_write_tags.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
SIENTIA DataOps Orchestrator — a Temporal.io workflow application for pipeline orchestration, real-time alerting, and scheduled reporting on the SIENTIA platform. Python 3.11+, package name orchestrator.
Commands
Install deps:
pip install -r requirements.txt
pip install -r requirements-dev.txt # ruff, mypy, bandit, pytest, pytest-cov, pytest-asyncio
Run the worker locally (activates venv, loads .env, runs python -m orchestrator.worker.worker):
./run_local.sh
Tests:
pytest tests/ --cov=orchestrator --cov-report=html # full suite with coverage
pytest tests/orchestrator/activities/test_mongo_db.py # single file
pytest tests/orchestrator/activities/test_mongo_db.py::TestClass::test_name # single test
./run_coverage.sh # runs pytest w/ coverage, opens htmlcov/index.html
Lint/format/type/security (run individually — validate.sh referenced by README no longer exists in the repo, so don't rely on it):
ruff format --check orchestrator/ tests/
ruff check orchestrator/ tests/
mypy orchestrator/
bandit -r orchestrator/ -ll
# autofix
ruff format orchestrator/ tests/
ruff check --fix orchestrator/ tests/
CI quality gate (.github/workflows/quality-gate.yml) calls the shared reusable workflow Aignosi/github_workflow_templates/.github/workflows/dataops-module-quality-gate.yml on PRs to main — it's the source of truth for what must pass, not a local script.
Architecture
Layering
orchestrator/worker/worker.py— entrypoint (python -m orchestrator.worker.worker). Builds a singleActivitiesinstance, connects to Temporal, starts Prometheus (HTTP_METRICS_PORT) and Temporal SDK metrics (HTTP_SDK_METRICS_PORT) servers, then runs threeWorkers concurrently viaasyncio.gather, each on its own task queue:orchestrator-queue,alerts-queue,reports-queue.prepare_worker.pyderives the queue name from the workflow class name (camel_to_snake) and reads per-queue concurrency/poller tuning from env vars prefixed with the workflow name (e.g.ORCHESTRATOR_MAX_CONCURRENT_ACTIVITIES).orchestrator/workflows/— Temporal workflow definitions:orchestrator.py(pipeline/slot/schedule orchestration),alerts.py(real-time ERROR notifications),reports.py(scheduled all-level reports).alerts.pyandreports.pyboth delegate to the same two subworkflows inworkflows/subworkflows/:load_notification_package.py(load notifications + receiver configs from Mongo, incremental via Redis timestamp) andprocess_notifications.py(build HTML email, send, log to Postgres). The difference between alerts and reports is which filter activity they call (filter_notification_alertsvsfilter_notification_reports) and their base data filter (level: ERRORby default vs all levels) —load_notification_package.py's_compute_query_time_filtercan widen either beyond that default to the union of active receiver groups' ownlevels(SIENTIAPDE-2011); the ERROR-only default only holds when no active group narrows further. Receiver groups also support amodel_idsallow-list (SIENTIAPDE-2149), independent of the pre-existingignore_models_iddeny-list — both apply together (AND) when a group configures both, andmodel_idsfollows the same union-at-query-time / precise-per-group-slice split aslevels.load_latest_data's Mongo query is capped atNOTIFICATION_QUEUE_BATCH_SIZEdocuments per run (default0, meaning unlimited — unchanged from pre-SIENTIAPDE-2013 behavior unless a deployment opts in), sorted bytimestampascending so a capped run always returns the oldest pending batch first (SIENTIAPDE-2013) — a hard cap with no drain loop, since the incremental Redis cursor picks up any remainder on the next run.orchestrator/activities/activities.py—Activitiesis a single class combining every activity mixin via multiple inheritance:TemporalManager, SlotManager, Formatters, MongoDB, Email, Postgres(Postgres comes fromsientia_do.temporal.activities.postgres, the externalsientia-dataops-librarydependency). One instance is constructed inworker.pyand its bound methods are handed to eachWorkeras the activity list — activities are plain methods on this shared object, not separate classes. Each mixin's__init__takes its own config dict plus sharedlogger,notification_handler,metrics_controller.Activities.shutdown()closes every mixin's connections.orchestrator/utils/—connectors_config.pybuilds each config dict from env vars (Temporal/Redis/MongoDB/Postgres/Email);email_builder.pyrenders Jinja2 templates (utils/templates/email_template.html,general_template.html);pipeline_configs.pytransforms pipeline documents into pipeline-type-specific configs (scouter,pi_web_api_scouter,predictions_batch,minimal_retrain,drift,simple_metrics);tag_database.pyindexes the tag database (connectors+connector_tags) and resolves tags bytag_id(scouter read tag names and thepredictions_batchwrite tags →opc_output_configkeyed byconnector_id, viaresolve_write_tags; a pipeline's effectiveupdated_atalso follows its tags'updated_at, so tag changes redeploy it);opc_slots.pybuilds the OPC slot v2 the gateway reads (gather_read_tags/build_tag_config, contract C1: one entry per address with anames[]list; see the README section "OPC slots").resolve_tag_names(intag_database.py) is the single source of tag names for both the scouter'smodel_tagsand the slot'snames(SIENTIAPDE-1974 SC1);converters.pyhandles Temporal schedule frequency parsing.
Temporal-specific conventions (all workflow/activity files follow this)
- Every workflow and activity-consuming module wraps non-deterministic/side-effecting imports in
with workflow.unsafe.imports_passed_through(): ...right afterfrom temporalio import workflow, because Temporal's sandbox restricts what workflow code can import directly. When adding a new workflow file, follow the existing pattern (seeorchestrator/workflows/orchestrator.py,orchestrator/activities/activities.py) rather than importing at module top level. - Workflows are registered with
@workflow.defn(name='...'); therunmethod is@workflow.run. Activity calls from within a workflow go throughworkflow.start_local_activity_method/workflow.execute_activity_methodreferencingActivities.<method>(the class method, not an instance) with aretry_policyfromsientia_do.temporal.policies. - Task queue isolation is deliberate: don't move activities between the orchestrator/alerts/reports worker activity lists without checking which queue actually needs them — each list in
worker.pyis scoped to what that workflow (+ its subworkflows) calls. - Namespaces: Temporal schedules span two namespaces,
scouterandlaborious(TEMPORAL_SCOUTER_NAMESPACE/TEMPORAL_LABORIOUS_NAMESPACE), handled insideTemporalManager.
External dependency
sientia-dataops-library (pinned via requirements.txt as a git dependency, currently @1.8.2) supplies sientia_do.observability.logger, sientia_do.observability.metrics_controller, sientia_do.notifications.handlers, sientia_do.temporal.policies, and sientia_do.temporal.activities.postgres.Postgres. It is a separate Aignosi repo — its version bump is a deliberate, explicit change, not incidental.
Tests
tests/ mirrors orchestrator/ (tests/orchestrator/activities/, tests/orchestrator/utils/, tests/orchestrator/workflows/...). Async tests use pytest-asyncio; coverage targets the orchestrator package. When testing Temporal workflows, follow the existing pattern in tests/orchestrator/workflows/test_orchestrator.py / test_alerts.py / test_reports.py (using Temporal's test env / activity mocking) rather than hitting real Temporal/Mongo/Redis/Postgres.
Ruff config notes (pyproject.toml)
Single quotes enforced by formatter. Notably ignored lint rules with reasons already documented inline: B023 (closures over loop vars needed for schedule-action assembly), BLE001 (blind except needed so activities can always notify on any error), N802/N806 (Temporal decorators/variables don't follow standard naming), S105/S106 (false-positive hardcoded-password matches).