gen3-dataops-toolkit 2.1.2__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/PKG-INFO +71 -3
  2. gen3_dataops_toolkit-2.2.0/README.md +159 -0
  3. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/pyproject.toml +1 -1
  4. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/config_cmds.py +20 -2
  5. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/dict_cmds.py +46 -19
  6. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/main.py +13 -0
  7. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/synth.py +119 -8
  8. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/config.py +106 -3
  9. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/dictionary/deploy_dd.sh +6 -3
  10. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/dictionary/pull_dict.sh +5 -1
  11. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +7 -7
  12. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/utils/athena_utils.py +67 -25
  13. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/utils/release_writer.py +32 -3
  14. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/validate/validate.py +162 -50
  15. gen3_dataops_toolkit-2.1.2/README.md +0 -91
  16. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/__init__.py +0 -0
  17. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/__init__.py +0 -0
  18. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/_internal/__init__.py +0 -0
  19. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/_internal/dispatch.py +0 -0
  20. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/_internal/registry.py +0 -0
  21. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/_internal/resolve.py +0 -0
  22. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/_internal/runner.py +0 -0
  23. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/_internal/safety.py +0 -0
  24. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/delete_cmds.py +0 -0
  25. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/ec2_cmds.py +0 -0
  26. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/indexd_cmds.py +0 -0
  27. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/jobs.py +0 -0
  28. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/k8s.py +0 -0
  29. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/metadata.py +0 -0
  30. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/pipeline_cmds.py +0 -0
  31. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/cli/release_cmds.py +0 -0
  32. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/indexd/__init__.py +0 -0
  33. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/indexd/indexd_registrar.py +0 -0
  34. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/ingest/ingest.py +0 -0
  35. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/resolver.py +0 -0
  36. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/delete/delete_all_metadata_for_project.py +0 -0
  37. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/delete/delete_metadata.sh +0 -0
  38. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/delete/delete_metadata_by_guid.py +0 -0
  39. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/dictionary/upload_dictionary.py +0 -0
  40. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/indexd/register_indexd.py +0 -0
  41. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/k8s_ops/argocd_restart_etl.sh +0 -0
  42. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/k8s_ops/argocd_restart_ms.sh +0 -0
  43. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/k8s_ops/argocd_restart_schema.sh +0 -0
  44. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/k8s_ops/login_to_pod.sh +0 -0
  45. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/k8s_ops/restart_etl_and_ms.sh +0 -0
  46. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +0 -0
  47. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/synthetic_data/generate_synth_metadata.sh +0 -0
  48. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +0 -0
  49. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/upload/metadata/upload_all_studies.sh +0 -0
  50. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/services/upload/metadata/upload_metadata.py +0 -0
  51. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/upload/__init__.py +0 -0
  52. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/upload/metadata_deleter.py +0 -0
  53. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/upload/metadata_submitter.py +0 -0
  54. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/upload/upload_synthdata_s3.py +0 -0
  55. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.2.0}/src/g3dt/utils/dbt_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gen3-dataops-toolkit
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: Gen3 DataOps toolkit (g3dt): operate SSM-published Gen3 data pipeline environments
5
5
  License: Apache-2.0
6
6
  Author: JoshuaHarris391
@@ -95,8 +95,8 @@ There are exactly two kinds of configuration:
95
95
 
96
96
  - **INPUTS** — human-authored values, committed as
97
97
  `config/<projectId>.<env>.json` in the CDK repo and read only by
98
- `cdk deploy`. To change a deployed setting (e.g. the dictionary version),
99
- edit that file and redeploy — the value flows to SSM.
98
+ `cdk deploy`. To change what an environment *declares*, edit that file and
99
+ redeploy — the value flows to SSM.
100
100
  - **OUTPUTS** — every resource name the CDK creates plus the mirrored Gen3
101
101
  app facts, published to SSM under `/{project}/{env}/...` on deploy. `g3dt`
102
102
  reads these live (cached one round-trip per invocation) and never stores
@@ -107,6 +107,74 @@ disagree — and because each environment has its own tree (including its own
107
107
  `ec2/instanceId`), running a job against the wrong environment's resources is
108
108
  structurally impossible.
109
109
 
110
+
111
+ ## CI isolation and the release contract
112
+
113
+ **Only the dbt template's `ci` target is prefixed.** `g3dt config dbt-env`
114
+ emits, alongside the real names, the CI-isolation variants the template's
115
+ `ci` target consumes: `G3DT_DB_RAW_SILVER_CI` / `G3DT_DB_RAW_GOLD_CI`
116
+ (`ci_` + the real database name) and `G3DT_S3_SILVER_DATA_DIR_CI` /
117
+ `G3DT_S3_GOLD_DATA_DIR_CI` (`dbt_ci/` under the same buckets). Commit-
118
+ triggered CI builds land there; every other target (default, local) and the
119
+ release build keep the real, unprefixed names — so CI can never advance the
120
+ warehouse's Iceberg snapshots that releases pin. The library enforces the
121
+ other half: `find_db_for_model` always skips `ci_`-prefixed databases, so
122
+ `g3dt release write` can never pin a release to a CI-build snapshot.
123
+
124
+ **Snapshot pinning.** `AthenaValidationWriter.construct_json` /
125
+ `AthenaGoldWriter.construct_json` honour a pre-set `snapshot_id` (reading the
126
+ table `FOR VERSION AS OF` that snapshot) and only fetch the latest snapshot
127
+ when unpinned — the contract the release-JSON export relies on for
128
+ reproducible releases.
129
+
130
+ **Concurrency.** `release_writer.run` processes models with a bounded thread
131
+ pool (`max_workers`, default 8) and fails at the end naming every failed
132
+ model (inserts are idempotent — re-run to fill the remainder). The S3
133
+ writers (`write_release_jsons_to_s3`, `write_validation_json_to_s3`) accept
134
+ `s3_client=` (pass one per worker thread) and `key_prefix=` (write a
135
+ verification tree without touching real artifacts).
136
+
137
+ **The validation gate.** `g3dt.validate.run_validation_gate(glue_database,
138
+ athena_s3_output, aws_region, workgroup)` queries the latest
139
+ `validation_id` in `full_validation_results` for REAL failures — the
140
+ known-noise patterns in `VALIDATION_GATE_IGNORED_ERRORS` and synthetic
141
+ studies are excluded. The validator Glue job fails when rows come back, so a
142
+ green validation Step Function means schema-clean data; the operator loop is
143
+ gate fails -> inspect the results table -> fix data -> re-run until green.
144
+ `validate_pipeline` also accepts pre-computed loop-invariants
145
+ (`schema=`/`resolver=`/`metadata_table=`) and `write_iceberg=False` so a
146
+ multi-study caller resolves the schema once, lists the validation prefix
147
+ once, and batches all studies into a single Iceberg INSERT.
148
+
149
+ ### Where the data dictionary comes from
150
+
151
+ Composed from the env's inputs as
152
+ `{dictionary_base_url}/{schema_repo}/refs/tags/{dictionary_version}/{dictionary_path}`.
153
+ Only `schema_repo` and `dictionary_version` are required; `app/dictionary_base_url`
154
+ and `app/dictionary_path` are optional and default to raw GitHub and the schema
155
+ repo's conventional layout, so environments deployed before they existed keep
156
+ working. `g3dt config show --env <env>` prints the composed URL.
157
+
158
+ ### Promoting a dictionary across environments
159
+
160
+ A dictionary version is *content*, not infrastructure: it changes far more often
161
+ than buckets or clusters do. Rather than a `cdk deploy` per environment per
162
+ version, `dict pull`, `dict upload` and `dict deploy` all accept `--version`:
163
+
164
+ ```bash
165
+ g3dt dict deploy --env test --version v1.1.7
166
+ g3dt dict deploy --env staging --version v1.1.7 # same tag, no cdk deploy
167
+ ```
168
+
169
+ An override does not persist, so `config show` keeps reporting the declared
170
+ version until the CDK config catches up — `g3dt config diff --env <env>` reports
171
+ exactly that gap and exits 1, so it can gate CI.
172
+
173
+ Synthetic data is only schema-valid against the dictionary that generated it, so
174
+ `synth generate` records the dictionary version in each batch and `synth upload`
175
+ refuses a batch that doesn't match the version being uploaded (override with
176
+ `--allow-version-mismatch`).
177
+
110
178
  ## Development
111
179
 
112
180
  ```bash
@@ -0,0 +1,159 @@
1
+ # gen3-dataops-toolkit (`g3dt`)
2
+
3
+ Operate Gen3 AWS data-pipeline environments from one pip-installable CLI.
4
+
5
+ `g3dt` is the tooling half of the Gen3 DataOps platform: the
6
+ [gen3-aws-data-pipeline](https://github.com/AustralianBioCommons/gen3-aws-data-pipeline)
7
+ CDK app deploys a complete pipeline per project/environment and publishes every
8
+ resource name to AWS SSM Parameter Store; `g3dt` resolves those names at
9
+ runtime and gives operators one command surface for dictionary deploys,
10
+ metadata upload/delete, indexd registration, EC2 job dispatch, and Kubernetes
11
+ restarts. The dbt half of the platform lives in
12
+ [gen3-dbt-template](https://github.com/AustralianBioCommons/gen3-dbt-template).
13
+
14
+ **No AWS resource name is compiled into this package.** The same wheel
15
+ operates any project: it is targeted purely by `--env`, the project's SSM tree
16
+ (`/{project}/{env}/...`), and a tiny local bootstrap marker.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install gen3-dataops-toolkit
22
+ ```
23
+
24
+ ## Bootstrap (the only local configuration)
25
+
26
+ `g3dt` needs to know just the project and region — everything else comes from
27
+ SSM. Create `~/.g3dt/g3dt.yaml`:
28
+
29
+ ```yaml
30
+ project: etl # your projectId
31
+ region: ap-southeast-2
32
+ default_env: test
33
+ profiles: # optional: AWS named profile per env
34
+ test: etl_test # (omit entirely on EC2/CodeBuild — ambient
35
+ staging: etl_staging # role credentials are used)
36
+ studies: # optional: the project's study registry;
37
+ mystudy_test: # alternatively upload it once per env to
38
+ project_id: MyStudy # s3://<metadata-bucket>/config/studies.yaml
39
+ program_id: program1
40
+ s3_metadata_path: s3://my-bucket/metadata/mystudy/
41
+ ```
42
+
43
+ Search order: `./g3dt.yaml` → `~/.g3dt/g3dt.yaml` → `/etc/g3dt/g3dt.yaml`
44
+ (the EC2 job box's copy, written by CDK user-data). Env vars override:
45
+ `G3DT_PROJECT`, `AWS_REGION`, `G3DT_DEFAULT_ENV`.
46
+
47
+ ## Quick start
48
+
49
+ ```bash
50
+ g3dt config envs # environments with a deployed SSM tree
51
+ g3dt config show --env test # every resolved name — the safety check
52
+ g3dt ec2 up --env test # start the env's job box (SSM-managed)
53
+ g3dt metadata upload --study mystudy --env test --on ec2
54
+ g3dt jobs logs <run-id> --follow # live logs; laptop can sleep, job keeps going
55
+ g3dt ec2 down --env test # or let the auto-stop alarm handle it
56
+ g3dt docs # the full operations overview
57
+ ```
58
+
59
+ ## How configuration works
60
+
61
+ There are exactly two kinds of configuration:
62
+
63
+ - **INPUTS** — human-authored values, committed as
64
+ `config/<projectId>.<env>.json` in the CDK repo and read only by
65
+ `cdk deploy`. To change what an environment *declares*, edit that file and
66
+ redeploy — the value flows to SSM.
67
+ - **OUTPUTS** — every resource name the CDK creates plus the mirrored Gen3
68
+ app facts, published to SSM under `/{project}/{env}/...` on deploy. `g3dt`
69
+ reads these live (cached one round-trip per invocation) and never stores
70
+ them locally.
71
+
72
+ Because the CLI and the infrastructure read the same parameters, they cannot
73
+ disagree — and because each environment has its own tree (including its own
74
+ `ec2/instanceId`), running a job against the wrong environment's resources is
75
+ structurally impossible.
76
+
77
+
78
+ ## CI isolation and the release contract
79
+
80
+ **Only the dbt template's `ci` target is prefixed.** `g3dt config dbt-env`
81
+ emits, alongside the real names, the CI-isolation variants the template's
82
+ `ci` target consumes: `G3DT_DB_RAW_SILVER_CI` / `G3DT_DB_RAW_GOLD_CI`
83
+ (`ci_` + the real database name) and `G3DT_S3_SILVER_DATA_DIR_CI` /
84
+ `G3DT_S3_GOLD_DATA_DIR_CI` (`dbt_ci/` under the same buckets). Commit-
85
+ triggered CI builds land there; every other target (default, local) and the
86
+ release build keep the real, unprefixed names — so CI can never advance the
87
+ warehouse's Iceberg snapshots that releases pin. The library enforces the
88
+ other half: `find_db_for_model` always skips `ci_`-prefixed databases, so
89
+ `g3dt release write` can never pin a release to a CI-build snapshot.
90
+
91
+ **Snapshot pinning.** `AthenaValidationWriter.construct_json` /
92
+ `AthenaGoldWriter.construct_json` honour a pre-set `snapshot_id` (reading the
93
+ table `FOR VERSION AS OF` that snapshot) and only fetch the latest snapshot
94
+ when unpinned — the contract the release-JSON export relies on for
95
+ reproducible releases.
96
+
97
+ **Concurrency.** `release_writer.run` processes models with a bounded thread
98
+ pool (`max_workers`, default 8) and fails at the end naming every failed
99
+ model (inserts are idempotent — re-run to fill the remainder). The S3
100
+ writers (`write_release_jsons_to_s3`, `write_validation_json_to_s3`) accept
101
+ `s3_client=` (pass one per worker thread) and `key_prefix=` (write a
102
+ verification tree without touching real artifacts).
103
+
104
+ **The validation gate.** `g3dt.validate.run_validation_gate(glue_database,
105
+ athena_s3_output, aws_region, workgroup)` queries the latest
106
+ `validation_id` in `full_validation_results` for REAL failures — the
107
+ known-noise patterns in `VALIDATION_GATE_IGNORED_ERRORS` and synthetic
108
+ studies are excluded. The validator Glue job fails when rows come back, so a
109
+ green validation Step Function means schema-clean data; the operator loop is
110
+ gate fails -> inspect the results table -> fix data -> re-run until green.
111
+ `validate_pipeline` also accepts pre-computed loop-invariants
112
+ (`schema=`/`resolver=`/`metadata_table=`) and `write_iceberg=False` so a
113
+ multi-study caller resolves the schema once, lists the validation prefix
114
+ once, and batches all studies into a single Iceberg INSERT.
115
+
116
+ ### Where the data dictionary comes from
117
+
118
+ Composed from the env's inputs as
119
+ `{dictionary_base_url}/{schema_repo}/refs/tags/{dictionary_version}/{dictionary_path}`.
120
+ Only `schema_repo` and `dictionary_version` are required; `app/dictionary_base_url`
121
+ and `app/dictionary_path` are optional and default to raw GitHub and the schema
122
+ repo's conventional layout, so environments deployed before they existed keep
123
+ working. `g3dt config show --env <env>` prints the composed URL.
124
+
125
+ ### Promoting a dictionary across environments
126
+
127
+ A dictionary version is *content*, not infrastructure: it changes far more often
128
+ than buckets or clusters do. Rather than a `cdk deploy` per environment per
129
+ version, `dict pull`, `dict upload` and `dict deploy` all accept `--version`:
130
+
131
+ ```bash
132
+ g3dt dict deploy --env test --version v1.1.7
133
+ g3dt dict deploy --env staging --version v1.1.7 # same tag, no cdk deploy
134
+ ```
135
+
136
+ An override does not persist, so `config show` keeps reporting the declared
137
+ version until the CDK config catches up — `g3dt config diff --env <env>` reports
138
+ exactly that gap and exits 1, so it can gate CI.
139
+
140
+ Synthetic data is only schema-valid against the dictionary that generated it, so
141
+ `synth generate` records the dictionary version in each batch and `synth upload`
142
+ refuses a batch that doesn't match the version being uploaded (override with
143
+ `--allow-version-mismatch`).
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ poetry install
149
+ poetry run python3 -m pytest
150
+ ```
151
+
152
+ ## Provenance
153
+
154
+ This toolkit was ported (working tree only) from
155
+ [AustralianBioCommons/acdc-aws-etl-pipeline](https://github.com/AustralianBioCommons/acdc-aws-etl-pipeline),
156
+ the ACDC ETL monolith, as part of the Gen3 DataOps platform refactor (2026).
157
+ It starts at version **2.0.0**; versions ≤ 1.2.0 on PyPI are the legacy
158
+ `acdc_aws_etl_pipeline` package, which continues to operate the legacy ACDC
159
+ pipeline unchanged.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "gen3-dataops-toolkit"
3
- version = "2.1.2"
3
+ version = "2.2.0"
4
4
  description = "Gen3 DataOps toolkit (g3dt): operate SSM-published Gen3 data pipeline environments"
5
5
  authors = ["JoshuaHarris391 <harjo391@gmail.com>"]
6
6
  readme = "README.md"
@@ -86,6 +86,10 @@ def show(
86
86
  typer.echo(f" aws_profile : {e.aws_profile or '(ambient credentials)'}")
87
87
  typer.echo(f" aws_secret_name : {e.aws_secret_name}")
88
88
  typer.echo(f" dictionary_version : {e.dictionary_version}")
89
+ # The composed URL, not the three parts: "which dictionary will this env
90
+ # actually fetch?" now spans schema_repo plus two optional inputs, so showing
91
+ # the resolved result is what makes the answer checkable before a deploy.
92
+ typer.echo(f" dictionary_url : {config.dictionary_url(e)}")
89
93
  typer.echo(f" schema_s3_uri : {e.schema_s3_uri}")
90
94
  typer.echo(f" schema_repo : {e.schema_repo}")
91
95
  typer.echo(f" domain : {e.domain}")
@@ -208,15 +212,24 @@ def dbt_env(
208
212
  typer.secho(str(exc), fg=typer.colors.RED, err=True)
209
213
  raise typer.Exit(1)
210
214
 
215
+ raw_silver_db = rc.get("glue/db/rawSilver")
216
+ raw_gold_db = rc.get("glue/db/rawGold")
211
217
  values = {
212
218
  "G3DT_REGION": rc.region,
213
219
  "G3DT_ATHENA_WORKGROUP": rc.athena_workgroup,
214
220
  "G3DT_ATHENA_OUTPUT": rc.athena_output_location,
215
221
  "G3DT_DB_RAW_BRONZE": rc.get("glue/db/rawBronze"),
216
- "G3DT_DB_RAW_SILVER": rc.get("glue/db/rawSilver"),
217
- "G3DT_DB_RAW_GOLD": rc.get("glue/db/rawGold"),
222
+ "G3DT_DB_RAW_SILVER": raw_silver_db,
223
+ "G3DT_DB_RAW_GOLD": raw_gold_db,
218
224
  "G3DT_S3_SILVER_DATA_DIR": f"s3://{rc.get('buckets/rawSilver')}/dbt/",
219
225
  "G3DT_S3_GOLD_DATA_DIR": f"s3://{rc.get('buckets/rawGold')}/dbt/",
226
+ # CI isolation: the dbt template's `ci` target builds into these
227
+ # instead — same grammar as the CDK's ci_ databases, same buckets
228
+ # under a dbt_ci/ prefix. Real names above are never prefixed.
229
+ "G3DT_DB_RAW_SILVER_CI": f"ci_{raw_silver_db}" if raw_silver_db else None,
230
+ "G3DT_DB_RAW_GOLD_CI": f"ci_{raw_gold_db}" if raw_gold_db else None,
231
+ "G3DT_S3_SILVER_DATA_DIR_CI": f"s3://{rc.get('buckets/rawSilver')}/dbt_ci/",
232
+ "G3DT_S3_GOLD_DATA_DIR_CI": f"s3://{rc.get('buckets/rawGold')}/dbt_ci/",
220
233
  }
221
234
  if profile:
222
235
  # A named profile means a laptop run: select the dbt target that
@@ -239,6 +252,11 @@ def set_value(
239
252
  settings — dictionary_version, domain, buckets, ... — are CDK INPUTS: edit
240
253
  config/<project>.<env>.json in gen3-aws-data-pipeline and `cdk deploy`;
241
254
  the values flow to SSM, which is what every consumer reads.
255
+
256
+ The one exception is per-invocation: `dict pull/upload/deploy --version`
257
+ fetches a different dictionary tag without a redeploy (for promoting one
258
+ dictionary across environments). That does not persist — `config diff`
259
+ reports the gap until the CDK config is updated to match.
242
260
  """
243
261
  try:
244
262
  old, new, path = config.set_marker_value(key, value)
@@ -10,22 +10,16 @@ can point at its own dictionary repo. Downloads land in ``~/.g3dt/schemas/``
10
10
  from __future__ import annotations
11
11
 
12
12
  from pathlib import Path
13
+ from typing import Optional
13
14
 
14
15
  import typer
15
16
 
16
- from g3dt.config import script_env
17
+ from g3dt.config import dictionary_filename, dictionary_url, script_env
17
18
  from g3dt.cli._internal import runner
18
19
  from g3dt.cli._internal.resolve import env_of
19
20
 
20
21
  app = typer.Typer(no_args_is_help=True, help="Data dictionary operations (local).")
21
22
 
22
- #: Raw-GitHub URL template; the trailing path is the schema repo's layout
23
- #: convention (see AustralianBioCommons/acdc-schema-json), not a project name.
24
- _DICT_URL_TMPL = (
25
- "https://raw.githubusercontent.com/{repo}/"
26
- "refs/tags/{version}/dictionary/prod_dict/acdc_schema.json"
27
- )
28
-
29
23
  SCHEMA_DIR = Path("~/.g3dt/schemas").expanduser()
30
24
 
31
25
 
@@ -33,8 +27,23 @@ def _version(env_cfg, override):
33
27
  return override or env_cfg.dictionary_version
34
28
 
35
29
 
36
- def dict_url(env_cfg, version: str) -> str:
37
- return _DICT_URL_TMPL.format(repo=env_cfg.schema_repo, version=version)
30
+ def warn_if_overridden(env_cfg, version: Optional[str]) -> None:
31
+ """Say so, loudly, when the deployed version isn't the one SSM declares.
32
+
33
+ An override is legitimate -- promoting one dictionary through environments
34
+ shouldn't need a `cdk deploy` per env -- but it leaves SSM (and so
35
+ `g3dt config show`) describing a different version than the bucket holds.
36
+ Naming both keeps that discoverable instead of silent; `g3dt config diff`
37
+ is what reconciles it once the CDK config catches up.
38
+ """
39
+ if version and version != env_cfg.dictionary_version:
40
+ typer.secho(
41
+ f"Overriding the declared version: SSM says "
42
+ f"{env_cfg.dictionary_version}, using {version}. `config show` will "
43
+ f"keep reporting {env_cfg.dictionary_version} until "
44
+ f"config/<project>.{env_cfg.name}.json is updated and redeployed.",
45
+ fg=typer.colors.YELLOW,
46
+ )
38
47
 
39
48
 
40
49
  @app.command()
@@ -46,15 +55,23 @@ def pull(
46
55
  ) -> None:
47
56
  """Download the dictionary JSON from the env's schema repo.
48
57
 
58
+ Where it comes from is config: `app/schema_repo` plus the optional
59
+ `app/dictionary_base_url` and `app/dictionary_path`.
60
+
49
61
  Examples:
50
62
  g3dt dict pull --env test
51
63
  g3dt dict pull --env staging --version v1.1.5
52
64
  """
53
65
  e = env_of(env)
54
- url = dict_url(e, _version(e, version))
66
+ warn_if_overridden(e, version)
67
+ v = _version(e, version)
55
68
  runner.run(
56
- runner.bash_script("services/dictionary/pull_dict.sh", url),
57
- env=script_env(e),
69
+ runner.bash_script(
70
+ "services/dictionary/pull_dict.sh",
71
+ dictionary_url(e, v),
72
+ dictionary_filename(e, v),
73
+ ),
74
+ env=script_env(e, v),
58
75
  )
59
76
 
60
77
 
@@ -67,36 +84,46 @@ def upload(
67
84
  ) -> None:
68
85
  """Upload the (already pulled) dictionary JSON to the env's S3 location."""
69
86
  e = env_of(env)
87
+ warn_if_overridden(e, version)
70
88
  v = _version(e, version)
71
- local_file = str(SCHEMA_DIR / f"acdc_schema_{v}.json")
89
+ local_file = str(SCHEMA_DIR / dictionary_filename(e, v))
72
90
  s3_uri = f"s3://{e.schema_s3_uri}"
73
91
  args = [local_file, s3_uri]
74
92
  if e.aws_profile:
75
93
  args.append(e.aws_profile)
76
94
  runner.run(
77
95
  runner.python_script("services/dictionary/upload_dictionary.py", *args),
78
- env=script_env(e),
96
+ env=script_env(e, v),
79
97
  )
80
98
 
81
99
 
82
100
  @app.command()
83
101
  def deploy(
84
102
  env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
103
+ version: str = typer.Option(
104
+ None, "--version", help="Dictionary git tag (default: the env's version)."
105
+ ),
85
106
  ) -> None:
86
107
  """Pull + upload the dictionary and restart Gen3 schema microservices.
87
108
 
88
109
  Wraps services/dictionary/deploy_dd.sh. Requires an interactive ArgoCD SSO
89
110
  login, so it runs locally only.
90
111
 
91
- The deployed version is the env's `dictionary_version` a CDK INPUT. To
92
- change it, edit config/<project>.<env>.json in gen3-aws-data-pipeline and
93
- `cdk deploy` (the value flows to SSM), then re-run this command.
112
+ The version defaults to the env's `dictionary_version`, a CDK INPUT: edit
113
+ config/<project>.<env>.json in gen3-aws-data-pipeline and `cdk deploy` to
114
+ change what an env declares. Pass --version to deploy a different tag now
115
+ without that round trip — which is how one dictionary gets promoted across
116
+ environments. `g3dt config diff` reports the resulting drift until the CDK
117
+ config catches up.
94
118
 
95
119
  Examples:
96
120
  g3dt dict deploy --env test
121
+ g3dt dict deploy --env test --version v1.1.7
122
+ g3dt dict deploy --env staging --version v1.1.7 # promote the same tag
97
123
  """
98
124
  e = env_of(env)
125
+ warn_if_overridden(e, version)
99
126
  runner.run(
100
127
  runner.bash_script("services/dictionary/deploy_dd.sh", env),
101
- env=script_env(e),
128
+ env=script_env(e, _version(e, version)),
102
129
  )
@@ -73,6 +73,16 @@ Typical release runbook (staging shown; repeat for prod with care)
73
73
  3. g3dt jobs logs <run-id> --follow
74
74
  4. g3dt k8s restart-etl --env staging
75
75
 
76
+ Promoting one dictionary across environments
77
+ The source repo/path are env inputs; usually only the tag changes, and it
78
+ changes far more often than infrastructure does. So --version deploys a tag
79
+ without a `cdk deploy` per env:
80
+ g3dt dict deploy --env test --version v1.1.7
81
+ g3dt dict deploy --env staging --version v1.1.7
82
+ The override does not persist: `g3dt config show` keeps reporting the declared
83
+ version, and `g3dt config diff --env <env>` reports the gap (exit 1) until
84
+ config/<project>.<env>.json is updated to match.
85
+
76
86
  Data releases (the dbt pipeline; see the project's dbt repo)
77
87
  git tag data-v1.4.0 && git push origin data-v1.4.0
78
88
  g3dt pipeline status --env staging which stage is running/failed
@@ -81,6 +91,9 @@ Data releases (the dbt pipeline; see the project's dbt repo)
81
91
 
82
92
  Synthetic data (test only, all local)
83
93
  g3dt synth deploy --env test
94
+ Batches are only schema-valid against the dictionary that generated them, so
95
+ each one records its dictionary version and `g3dt synth upload` refuses a
96
+ batch that does not match the version being uploaded.
84
97
 
85
98
  EC2 / SSM prerequisites
86
99
  - The env's job box is created by the CDK (ec2-job-runner stack): SSM-managed,
@@ -15,14 +15,21 @@ working directory (``LLM_PROVIDER`` / ``LLM_MODEL`` / ``LLM_API_KEY_FILE``).
15
15
  """
16
16
  from __future__ import annotations
17
17
 
18
+ import json
18
19
  from pathlib import Path
20
+ from typing import Optional
19
21
 
20
22
  import typer
21
23
 
22
- from g3dt.config import script_env
24
+ from g3dt.config import (
25
+ dictionary_filename,
26
+ dictionary_url,
27
+ dictionary_version_of,
28
+ script_env,
29
+ )
23
30
  from g3dt.cli._internal import runner, safety
24
31
  from g3dt.cli._internal.resolve import env_of
25
- from g3dt.cli.dict_cmds import SCHEMA_DIR, dict_url
32
+ from g3dt.cli.dict_cmds import SCHEMA_DIR, warn_if_overridden
26
33
 
27
34
  app = typer.Typer(
28
35
  no_args_is_help=True,
@@ -31,6 +38,72 @@ app = typer.Typer(
31
38
 
32
39
  SYNTH_DIR = Path("~/.g3dt/synth_metadata").expanduser()
33
40
 
41
+ #: Written into each generated batch so `synth upload` can tell which dictionary
42
+ #: produced it. A batch is only valid against that dictionary, and the directory
43
+ #: name alone cannot be trusted: --schema and --version are separate options, so
44
+ #: the label is exactly the thing that can be wrong.
45
+ PROVENANCE_FILE = ".g3dt-provenance.json"
46
+
47
+
48
+ def _write_provenance(batch_dir: Path, e, ver: str, schema_path: str) -> None:
49
+ """Record which dictionary produced this batch."""
50
+ batch_dir.mkdir(parents=True, exist_ok=True)
51
+ (batch_dir / PROVENANCE_FILE).write_text(
52
+ json.dumps(
53
+ {
54
+ "dictionary_version": ver,
55
+ "dictionary_url": dictionary_url(e, ver),
56
+ "schema_file": Path(schema_path).name,
57
+ "generated_for_env": e.name,
58
+ },
59
+ indent=2,
60
+ )
61
+ + "\n"
62
+ )
63
+
64
+
65
+ def _check_batch_matches_env(batch_dir: Path, e, ver: str, allow_mismatch: bool) -> None:
66
+ """Refuse to upload a batch generated against a different dictionary.
67
+
68
+ Synthetic records are only schema-valid against the dictionary version used
69
+ to generate them, so pushing a v1.0.0 batch into an environment running
70
+ v1.1.0 produces data Gen3 may reject or, worse, silently accept as wrong.
71
+ The environment's own version is the authority here.
72
+
73
+ A batch with no provenance file predates this check and cannot be attributed
74
+ after the fact, so it warns rather than blocks.
75
+ """
76
+ marker = batch_dir / PROVENANCE_FILE
77
+ if not marker.is_file():
78
+ typer.secho(
79
+ f"No provenance in {batch_dir} — cannot confirm which dictionary "
80
+ f"generated it. Proceeding; regenerate the batch to record it.",
81
+ fg=typer.colors.YELLOW,
82
+ )
83
+ return
84
+ batch_version = json.loads(marker.read_text()).get("dictionary_version")
85
+ if batch_version == ver:
86
+ return
87
+ if allow_mismatch:
88
+ typer.secho(
89
+ f"Version mismatch allowed by flag: batch was generated against "
90
+ f"{batch_version}, uploading to {e.name} which runs {ver}.",
91
+ fg=typer.colors.YELLOW,
92
+ )
93
+ return
94
+ typer.secho(
95
+ f"Refusing to upload: this batch was generated against dictionary "
96
+ f"{batch_version}, but '{e.name}' runs {ver}. Synthetic data is only "
97
+ f"valid against the dictionary that produced it.\n"
98
+ f"Either regenerate for {ver} (g3dt synth generate ... --version {ver}), "
99
+ f"deploy {batch_version} to '{e.name}' first "
100
+ f"(g3dt dict deploy --env {e.name} --version {batch_version}), "
101
+ f"or override with --allow-version-mismatch.",
102
+ fg=typer.colors.RED,
103
+ err=True,
104
+ )
105
+ raise typer.Exit(1)
106
+
34
107
 
35
108
  @app.command()
36
109
  def deploy(
@@ -113,14 +186,34 @@ def generate(
113
186
  raise typer.Exit(1)
114
187
 
115
188
  ver = version or e.dictionary_version
116
- schema_path = schema or str(SCHEMA_DIR / f"acdc_schema_{ver}.json")
189
+ schema_path = schema or str(SCHEMA_DIR / dictionary_filename(e, ver))
190
+
191
+ # A batch is only valid against the dictionary that generated it, and the
192
+ # output directory is named for `ver` -- so an explicit --schema carrying a
193
+ # different version stamp would silently mislabel the whole batch.
194
+ if schema:
195
+ schema_version = dictionary_version_of(Path(schema).name)
196
+ if schema_version and schema_version != ver:
197
+ typer.secho(
198
+ f"--schema is {Path(schema).name} (dictionary {schema_version}) "
199
+ f"but the batch would be labelled {ver}. Synthetic data is only "
200
+ f"valid against the dictionary that produced it — pass "
201
+ f"--version {schema_version}, or drop --schema to use {ver}.",
202
+ fg=typer.colors.RED,
203
+ err=True,
204
+ )
205
+ raise typer.Exit(1)
117
206
 
118
207
  # Ensure the schema is available locally; pull it if missing.
119
208
  if not Path(schema_path).exists():
120
209
  typer.secho(f"Schema not found locally; pulling {ver}...", fg=typer.colors.YELLOW)
121
210
  runner.run(
122
- runner.bash_script("services/dictionary/pull_dict.sh", dict_url(e, ver)),
123
- env=script_env(e),
211
+ runner.bash_script(
212
+ "services/dictionary/pull_dict.sh",
213
+ dictionary_url(e, ver),
214
+ dictionary_filename(e, ver),
215
+ ),
216
+ env=script_env(e, ver),
124
217
  )
125
218
 
126
219
  effective_provider = "llm" if llm else provider
@@ -138,8 +231,11 @@ def generate(
138
231
  runner.bash_script(
139
232
  "services/synthetic_data/generate_synth_metadata.sh", *args
140
233
  ),
141
- env=script_env(e),
234
+ env=script_env(e, ver),
142
235
  )
236
+ # Only after a successful generate: runner.run raises on failure, so a
237
+ # half-written batch is never stamped as valid.
238
+ _write_provenance(SYNTH_DIR / ver, e, ver, schema_path)
143
239
 
144
240
 
145
241
  @app.command()
@@ -150,11 +246,26 @@ def upload(
150
246
  version: str = typer.Option(
151
247
  None, "--version", help="Dictionary version dir (default: the env's version)."
152
248
  ),
249
+ allow_version_mismatch: bool = typer.Option(
250
+ False,
251
+ "--allow-version-mismatch",
252
+ help="Upload even if the batch was generated against another dictionary.",
253
+ ),
153
254
  ) -> None:
154
- """Upload generated synthetic metadata to Gen3 (reads local files)."""
255
+ """Upload generated synthetic metadata to Gen3 (reads local files).
256
+
257
+ The batch is checked against the dictionary version being uploaded for:
258
+ synthetic records are only schema-valid against the dictionary that produced
259
+ them. Defaults to the env's declared version, so the common case verifies
260
+ against what the environment actually runs.
261
+ """
155
262
  e = env_of(env)
156
263
  safety.confirm_prod_strict("synthetic metadata upload", env)
264
+ # A promoted dictionary (dict deploy --version) leaves SSM behind, so an
265
+ # explicit --version here is how you say "this env really runs that tag".
266
+ warn_if_overridden(e, version)
157
267
  v = version or e.dictionary_version
268
+ _check_batch_matches_env(SYNTH_DIR / v, e, v, allow_version_mismatch)
158
269
  base_dir = str(SYNTH_DIR / v) + "/"
159
270
  args = ["--base-dir", base_dir, "--aws-secret-name", e.aws_secret_name]
160
271
  if e.aws_profile:
@@ -163,7 +274,7 @@ def upload(
163
274
  runner.python_script(
164
275
  "services/synthetic_data/upload_synth_metadata_sheepdog.py", *args
165
276
  ),
166
- env=script_env(e),
277
+ env=script_env(e, v),
167
278
  )
168
279
 
169
280