6.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Temporal.io worker for industrial time-series data ingestion (scouter). Pulls data from MongoDB / PI Web API, runs quality-gate filtering + aggregation, writes to PostgreSQL, caches state in Redis. Heavy dependency on the private sientia_do library (aka sientia-dataops-library) for base activity classes (Postgres/Redis/Mongo repos), logging, notifications, metrics, retry policy, and connector config builders — read that lib's source when a base method/behavior isn't in this repo.
Setup
python3.11 -m venv venv
source ./venv/bin/activate
pip install -r requirements-local.txt # uses git+ssh to sientia-dataops-library (needs SSH access)
pip install -r requirements-dev.txt # lint/test/e2e tooling
cp .env.example .env # fill in connection details
requirements.txt uses the plain package name sientia_do (resolved via index/private registry in CI); requirements-local.txt pins it via git+ssh://...sientia-dataops-library.git@<tag> for local dev — keep both in sync when bumping the library version (see git-requirements-mapping.txt).
Commands
./run_local.sh # activate venv, load .env, run worker
python -m scouter.worker.worker # run worker manually
pytest # unit tests (tests/), Docker-free
pytest tests/activities/test_redis.py # single file
pytest tests/workflow/test_scouter.py::TestClassName::test_name # single test
pytest --cov=scouter --cov-report=html # coverage (or ./run_coverage.sh, opens browser)
# e2e tests are excluded from default `pytest` runs (testpaths=tests) — invoke explicitly:
pytest e2e/ --override-ini testpaths=e2e -m e2e -v
pytest e2e/ --override-ini testpaths=e2e -m e2e -x # stop on first failure
ruff check . # lint
ruff format . # format
mypy scouter # type check
bandit -r scouter # security scan
CI quality gate (.github/workflows/quality-gate.yml) calls a shared reusable workflow (Aignosi/github_workflow_templates) with requirements_file: requirements-local.txt; releases (release.yml) fire on merged PRs to main via another shared workflow. Both are effectively opaque here — the actual lint/test/coverage steps live in the github_workflow_templates repo.
Architecture
Parent/child Temporal workflow pattern, two independent worker task queues sharing the same CoreScouter sub-workflow:
Scouter (scouter/workflow/scouter.py) PIWebAPIScouter (scouter/workflow/pi_web_api_scouter.py)
reads raw_{schedule_name} from MongoDB queries PI Web API directly (get_tag_values)
tracks last-processed timestamp in Redis normalizes all timestamps to the batch max
│ │
└───────────────► CoreScouter (scouter/workflow/sub_workflows/core_scouter.py) ◄───────┘
1. data_quality_gate (NULL_VALUES_FILTER / OUT_OF_BOUNDS_FILTER, DISCARD policy)
2. aggregate_data (per-tag: lts/avg/mdn/max/min)
3. group_and_hold_data (Redis, TTL = retention_time)
4. export_data_to_postgres
5. write_metrics (Prometheus)
6. store_data_package (optional, MongoDB, if debug_data_package=true)
Activities(scouter/activities/activities.py) is a multiple-inheritance god-class combiningPostgres(fromsientia_do),Redis,Gates,MongoDB,API(this repo'sscouter/activities/*.py). Each mixin owns its own__init__/close— the worker calls all of them explicitly. When adding a new activity, decide which mixin it belongs to; don't bypass the split.- Temporal sandboxing: every module doing non-workflow-safe imports (pandas, sientia_do, os, etc.) wraps them in
with workflow.unsafe.imports_passed_through():. Follow this pattern in any new workflow/activity file — Temporal's sandbox will fail otherwise. - Two task queues, one worker process:
worker.pybuilds twoprepare_worker(...)instances (main workflowScoutervsPIWebAPIScouter, both withCoreScouteras a registered child) and runs them concurrently viaasyncio.gather. If either crashes, the whole process exits non-zero (see thefinallyblock) — that's deliberate, for k8s restart semantics. - Aggregation functions:
lts(latest),avg,mdn(median),max,min— configured per-tag inmodel_tags[tag].aggr_function. - Quality filters:
NULL_VALUES_FILTER,OUT_OF_BOUNDS_FILTER(scouter/utils/quality/filters.py), each with a configurablepolicy(currentlyDISCARD). Register new filters in thequality_gate_filtersdict inscouter/activities/gates.py. - Config builders (
build_postgres_config,build_redis_config,build_mongodb_config,build_api_config) come fromsientia_do.utils.connectors_config, not this repo —scouter/utils/connectors_config.pyonly hasbuild_kafka_configlocally. - Metrics (
scouter/metrics.py):APP_UPgauge,LABORIOUS_DATA_WRITTEN_COUNTcounter,TAG_CHANGES_MONITORgauge, all labeled bypod_id/model_name/workflow_name(+tag_namefor the last one).sonar-project.propertiesexcludesscouter/worker/worker.pyfrom coverage — it's the process entrypoint, exercised by e2e, not unit tests.
Testing structure
tests/— Docker-free unit tests, mirrorsscouter/package layout (tests/activities/,tests/workflow/,tests/utils/). Defaultpytestinvocation only runs this dir (testpaths=testsinpyproject.toml).e2e/— production-faithful tests against real testcontainers (Postgres/Mongo/Redis) +WorkflowEnvironment.start_local()+ an in-process PI Web API test server (e2e/pi_web_api_test_server.py). OnlyLoggerand an optional notification-insert spy are mocked; everything else is realActivitieswiring. Must be invoked explicitly (see Commands) — not picked up by defaultpytest.- Scenario catalog lives in
e2e/scenarios.md, numbered and mapped totest_scenario_*functions. Section## 0= harness smoke tests (e2e/test_harness_smoke.py) that check Docker/container/worker wiring liveness before the real scenarios run. - A scenario that fails due to a genuine production defect gets marked
xfail(strict=True)rather than deleted/skipped silently.
- Scenario catalog lives in
- Markers:
asyncio,e2e,integration,unit(declared inpyproject.toml).
Root-level scripts
get_data_pims.py, pi_web_api_fetch_data.py, init_port_forward.sh are standalone helper/debug scripts (PI/PIMS data fetch, k8s port-forwarding for local dev against a real cluster) — not part of the worker's runtime path.