Standalone WTISEN runner that executes determine -> extract -> transform -> load without Kubeflow and without data-lake dependencies.
- Runs WTISEN extraction with Playwright using PHO credentials.
- Stores landing/processed/archive/curated artifacts on local filesystem paths.
- Keeps schema validation and merge-key dedupe/upsert behavior from the WTISEN pipeline design.
- Supports full run (
run-all) and stage-by-stage commands. - Resolves the default extraction end date using UTC.
- Required env vars:
PHO_USERNAMEPHO_PASSWORD
- Required mounted config file:
/config/wtisen.yaml
- Required writable data mount:
/data(or matchstorage.local.root_dirin config)
- Runtime prerequisites:
- outbound network access to WTISEN URLs
- container user write permissions to data mount
- system clock/timezone should be accurate (date windows are date-based)
cd /path/to/workflow-WTISEN
python -m venv .venv
. .venv/bin/activate
pip install -e .
pip install pytest
python -m playwright install --with-deps firefox
cp config/wtisen.example.yaml /tmp/wtisen.yaml
export PHO_USERNAME='your-user'
export PHO_PASSWORD='your-password'
wtisen-runner run-all --config /tmp/wtisen.yamlNotes:
- The example config uses
storage.local.root_dir: "./data", which is appropriate for local Python runs from the repo root. - If you run from another working directory, change
storage.local.root_dirto an absolute path. - On Linux,
python -m playwright install --with-deps firefoxis the preferred one-time setup because it installs the browser plus required system packages.
If you prefer invoking via Python directly (instead of console script):
cd /path/to/workflow-WTISEN
python -m wtisen_runner.cli run-all --config /tmp/wtisen.yamlcd /path/to/workflow-WTISEN
docker build -t wtisen-runner:local .docker run --rm \
-e PHO_USERNAME='your-user' \
-e PHO_PASSWORD='your-password' \
-v /absolute/path/wtisen.yaml:/config/wtisen.yaml:ro \
-v /absolute/path/data:/data \
wtisen-runner:local run-all --config /config/wtisen.yamlExample nightly run at 01:30 UTC:
CRON_TZ=UTC
30 1 * * * /usr/bin/docker run --rm -e PHO_USERNAME="$PHO_USERNAME" -e PHO_PASSWORD="$PHO_PASSWORD" -v /opt/wtisen/config/wtisen.yaml:/config/wtisen.yaml:ro -v /opt/wtisen/data:/data wtisen-runner:local run-all --config /config/wtisen.yaml >> /var/log/wtisen-runner.log 2>&1Notes:
- Ensure
PHO_USERNAMEandPHO_PASSWORDare available to cron (for example via/etc/environmentor wrapper script). - Ensure the config mounted at
/config/wtisen.yamlsetsstorage.local.root_dir: "/data"so output is written to the mounted volume. - Set
CRON_TZ=UTCin the crontab or run the host in UTC so the schedule time matches the documented UTC window. - Use absolute paths for all mounts and logs.
- Prefer a small wrapper script if you need additional setup such as
PATH, environment loading, or log rotation. - Exit code
3means partial success; alerting/monitoring should treat this as non-healthy.
Example nightly run at 01:30 UTC with a project virtualenv:
CRON_TZ=UTC
30 1 * * * cd /opt/workflow-WTISEN && PHO_USERNAME="$PHO_USERNAME" PHO_PASSWORD="$PHO_PASSWORD" /opt/workflow-WTISEN/.venv/bin/python -m wtisen_runner.cli run-all --config /opt/wtisen/config/wtisen.yaml >> /var/log/wtisen-runner.log 2>&1Notes:
- Install dependencies and browser binaries once in that environment:
pip install -e .,pip install pytest, andpython -m playwright install --with-deps firefox. - If you cannot use
--with-depson the host, install the required Linux system packages separately before runningpython -m playwright install firefox. - Set
CRON_TZ=UTCin the crontab or run the host in UTC so the schedule time matches the documented UTC window. - Use absolute paths for the config file and
storage.local.root_dirbecause cron should not rely on an implicit working directory. - If you do not
cdinto the repo, the config should not use./data; setstorage.local.root_dirto an absolute path such as/opt/wtisen/data.
The image entrypoint is already wtisen-runner, so args can directly provide subcommands.
Example resources:
apiVersion: v1
kind: Secret
metadata:
name: wtisen-credentials
type: Opaque
stringData:
PHO_USERNAME: "your-user"
PHO_PASSWORD: "your-password"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: wtisen-config
data:
wtisen.yaml: |
config_version: 1
source:
url: "<INSERT_SITE_URL>"
report_id: "<INSERT_REPORT_ID>"
phu_code: "<INSERT_PHU_CODE>"
landing_file_prefix: "wtisen"
auth:
username_env: "PHO_USERNAME"
password_env: "PHO_PASSWORD"
run:
default_start_date: "2008-01-01"
lookback_days: 3
archive_enabled: true
json_logs: true
debug_sensitive_logging_enabled: false
ignore_https_errors: false
login_timeout_ms: 30000
post_login_timeout_ms: 30000
report_viewer_timeout_ms: 120000
download_timeout_ms: 60000
download_retries: 2
storage:
local:
root_dir: "/data"
landing: "landing/wtisen"
processed: "processed/wtisen"
archive_landing: "archive/landing/wtisen"
archive_processed: "archive/processed/wtisen"
curated: "curated/wtisen/wtisen_curated_v2.0.parquet"
transform:
file_pattern: "*wtisen*.csv"
load:
merge_keys:
- barcode
- date_collected
- date_received
- date_released
- date_reported
file_pattern: "*_v2.0.parquet"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: wtisen-runner
spec:
schedule: "30 1 * * *"
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 1800
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: wtisen-runner
image: wtisen-runner:local
args: ["run-all", "--config", "/config/wtisen.yaml"]
envFrom:
- secretRef:
name: wtisen-credentials
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: data
mountPath: /data
volumes:
- name: config
configMap:
name: wtisen-config
- name: data
persistentVolumeClaim:
claimName: wtisen-data-pvcSave the manifest above as wtisen-cronjob.yaml, then apply it:
kubectl apply -f wtisen-cronjob.yamlNotes:
- Keep
storage.local.root_dir: "/data"in the mounted config so the job writes to the PVC. timeZone: "Etc/UTC"makes the schedule match the documented UTC run time explicitly.- Replace
wtisen-runner:localwith a registry-qualified image for shared environments, or import the built image onto each k3s node if you are running it locally. - Publishing the image is deployment-specific and is typically handled by the team operating the cluster, whether that image comes from GitHub Actions or a local build.
startingDeadlineSecondslimits how long Kubernetes will try to catch up a missed schedule after controller downtime.
wtisen-runner determine --config /config/wtisen.yaml
wtisen-runner extract --config /config/wtisen.yaml --start 2026-01-01 --end 2026-01-31
wtisen-runner transform --config /config/wtisen.yaml
wtisen-runner load --config /config/wtisen.yamlrun-all:- Optional
--start/--endoverrides are accepted only when both are provided. - If neither is provided, date window is derived by
determine.
- Optional
extract:- Optional
--start/--endoverrides are accepted only when both are provided. - If neither is provided, date window is derived by
determine.
- Optional
determine:- Prints JSON with computed date window:
{"stage":"determine","start":"YYYY-MM-DD","end":"YYYY-MM-DD"}
- Prints JSON with computed date window:
transform/load:- Return stage stats as JSON:
{"stage":"transform","records":N,"files_total":X,"files_loaded":Y,"files_skipped":Z}
- Return stage stats as JSON:
See config/wtisen.example.yaml.
Required groups:
source: WTISEN URL/report/phu/prefixauth: env key names for username/passwordrun: incremental controls and logging flags- includes extract resiliency knobs:
login_timeout_mspost_login_timeout_msreport_viewer_timeout_msdownload_timeout_msdownload_retries
- includes extract resiliency knobs:
storage.local: root + landing/processed/archive/curated pathstransform: landing file patternload: merge keys and processed file pattern
All storage paths are relative to storage.local.root_dir.
Using the container-oriented config values shown above:
root_dir:/datalanding:landing/wtisenprocessed:processed/wtisenarchive_landing:archive/landing/wtisenarchive_processed:archive/processed/wtisencurated:curated/wtisen/wtisen_curated_v2.0.parquet
Effective locations are:
- landing input/output folder:
/data/landing/wtisen - processed output folder:
/data/processed/wtisen - landing archive folder:
/data/archive/landing/wtisen - processed archive folder:
/data/archive/processed/wtisen - curated parquet target:
/data/curated/wtisen/wtisen_curated_v2.0.parquet
The shipped local example in config/wtisen.example.yaml instead uses storage.local.root_dir: "./data".
Stage behavior:
extract:- Writes WTISEN CSV files into
landing. - Empty downloads (no data rows) are not written.
- Writes WTISEN CSV files into
transform:- Reads matching CSV files from
landing. - Writes processed parquet files to
processed. - If
run.archive_enabled: true, successfully transformed landing files are moved fromlandingtoarchive_landing. - If
run.archive_enabled: false, landing files remain inlanding.
- Reads matching CSV files from
load:- Reads matching parquet files from
processed. - Merges into one local curated parquet at
curatedusingload.merge_keys(keep="last"on duplicates). - If
run.archive_enabled: true, successfully loaded processed files are moved fromprocessedtoarchive_processed. - If
run.archive_enabled: false, processed files remain inprocessed.
- Reads matching parquet files from
Operational notes:
- Archiving applies only to files that were successfully transformed/loaded.
- Failed files are left in place and counted as skipped.
- With archiving disabled, reruns can reprocess the same inputs unless you clean up or narrow file patterns.
- Default logs are plain text; set
run.json_logs: truefor JSON lines. - Keep
run.debug_sensitive_logging_enabled: falsein shared environments. - Authentication/exception messages are sanitized before logging where possible.
- Default is strict TLS verification (
run.ignore_https_errors: false). - Set
run.ignore_https_errors: trueonly if your organization explicitly accepts the risk (for example, known internal TLS interception or non-public CA chains).
- Missing config fields: startup validation failure.
- Missing credentials: extraction fails before browser automation.
- No matching landing/processed files: transform/load safely skip with zero records.
- Stage-level file/schema errors are logged per file and processing continues.
- Transform/load fail the stage only if all candidate files fail.
0: success (including no-op when no matching files are found).2: usage/config error (bad args or config validation failure).3: partial success (command completed but one or more files were skipped/failed).4: stage failure (stage execution failed before usable completion).
Scheduler handling recommendation:
- Treat
0as healthy. - Treat
2and4as failures with retry/escalation. - Treat
3as warning/partial failure (run completed but needs review).
run-allprints a summary JSON payload:start,end,extracted_records,transformed_records,loaded_records.- per-stage stats:
extract,transform,loadwith:records,files_total,files_loaded,files_skipped
partial_failure(boolean)
- Local load behavior writes/updates a curated parquet file using merge-key dedupe (
keep="last"). - This is not a transactional Delta merge; it is local filesystem parquet consolidation.
cd /path/to/workflow-WTISEN
. .venv/bin/activate
pytest -q- v1 storage backend is local filesystem only.
- Existing Kubeflow WTISEN pipeline is not modified by this package.