Files
Eduardo Rios f79d51336f fix: remove timestamp normalization instead of gating it by max_count
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>
2026-09-11 16:05:37 -03:00

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 combining Postgres (from sientia_do), Redis, Gates, MongoDB, API (this repo's scouter/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.py builds two prepare_worker(...) instances (main workflow Scouter vs PIWebAPIScouter, both with CoreScouter as a registered child) and runs them concurrently via asyncio.gather. If either crashes, the whole process exits non-zero (see the finally block) — that's deliberate, for k8s restart semantics.
  • Aggregation functions: lts (latest), avg, mdn (median), max, min — configured per-tag in model_tags[tag].aggr_function.
  • Quality filters: NULL_VALUES_FILTER, OUT_OF_BOUNDS_FILTER (scouter/utils/quality/filters.py), each with a configurable policy (currently DISCARD). Register new filters in the quality_gate_filters dict in scouter/activities/gates.py.
  • Config builders (build_postgres_config, build_redis_config, build_mongodb_config, build_api_config) come from sientia_do.utils.connectors_config, not this repo — scouter/utils/connectors_config.py only has build_kafka_config locally.
  • Metrics (scouter/metrics.py): APP_UP gauge, LABORIOUS_DATA_WRITTEN_COUNT counter, TAG_CHANGES_MONITOR gauge, all labeled by pod_id/model_name/workflow_name (+tag_name for the last one). sonar-project.properties excludes scouter/worker/worker.py from coverage — it's the process entrypoint, exercised by e2e, not unit tests.

Testing structure

  • tests/ — Docker-free unit tests, mirrors scouter/ package layout (tests/activities/, tests/workflow/, tests/utils/). Default pytest invocation only runs this dir (testpaths=tests in pyproject.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). Only Logger and an optional notification-insert spy are mocked; everything else is real Activities wiring. Must be invoked explicitly (see Commands) — not picked up by default pytest.
    • Scenario catalog lives in e2e/scenarios.md, numbered and mapped to test_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.
  • Markers: asyncio, e2e, integration, unit (declared in pyproject.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.