Review feedback (Vitor): gating the batch-max normalization behind max_count == 1 just hid the same collapsing behavior for the single-point case, where it's redundant anyway (aggregation/export don't need it) while still being the wrong fix conceptually. Removed the normalization block entirely so every record always keeps its own real timestamp from the PI Web API, regardless of max_count. Updated unit tests accordingly and confirmed no e2e scenario needs adjustment: aggregate_data groups by (tag, name) and always collapses to one output row per tag per execution, independent of per-record timestamp values, so pi_web_api_scouter_multiple_tags' aggregated-value assertions are unaffected. SIENTIAPDE-1993 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 preserves each record's real timestamp
│ │
└───────────────► 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.