gen3-dataops-toolkit 2.1.2__tar.gz → 2.3.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 (61) hide show
  1. gen3_dataops_toolkit-2.3.0/PKG-INFO +234 -0
  2. gen3_dataops_toolkit-2.3.0/README.md +199 -0
  3. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/pyproject.toml +2 -1
  4. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/_internal/safety.py +0 -12
  5. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/config_cmds.py +20 -2
  6. gen3_dataops_toolkit-2.3.0/src/g3dt/cli/delete_cmds.py +212 -0
  7. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/dict_cmds.py +46 -19
  8. gen3_dataops_toolkit-2.3.0/src/g3dt/cli/indexd_cmds.py +126 -0
  9. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/main.py +13 -0
  10. gen3_dataops_toolkit-2.3.0/src/g3dt/cli/metadata.py +133 -0
  11. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/synth.py +119 -8
  12. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/config.py +106 -3
  13. gen3_dataops_toolkit-2.3.0/src/g3dt/indexd/file_access.py +274 -0
  14. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/indexd/indexd_registrar.py +95 -4
  15. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/ingest/ingest.py +117 -33
  16. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/delete/delete_metadata.sh +52 -18
  17. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/dictionary/deploy_dd.sh +6 -3
  18. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/dictionary/pull_dict.sh +5 -1
  19. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/indexd/register_indexd.py +38 -0
  20. gen3_dataops_toolkit-2.3.0/src/g3dt/services/indexd/verify_file_access.py +126 -0
  21. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +7 -7
  22. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/upload/metadata/upload_all_studies.sh +29 -8
  23. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/upload/metadata/upload_metadata.py +34 -0
  24. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/upload/metadata_submitter.py +81 -8
  25. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/utils/athena_utils.py +146 -25
  26. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/utils/release_writer.py +32 -3
  27. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/validate/validate.py +162 -50
  28. gen3_dataops_toolkit-2.1.2/PKG-INFO +0 -125
  29. gen3_dataops_toolkit-2.1.2/README.md +0 -91
  30. gen3_dataops_toolkit-2.1.2/src/g3dt/cli/delete_cmds.py +0 -101
  31. gen3_dataops_toolkit-2.1.2/src/g3dt/cli/indexd_cmds.py +0 -57
  32. gen3_dataops_toolkit-2.1.2/src/g3dt/cli/metadata.py +0 -76
  33. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/__init__.py +0 -0
  34. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/__init__.py +0 -0
  35. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/_internal/__init__.py +0 -0
  36. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/_internal/dispatch.py +0 -0
  37. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/_internal/registry.py +0 -0
  38. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/_internal/resolve.py +0 -0
  39. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/_internal/runner.py +0 -0
  40. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/ec2_cmds.py +0 -0
  41. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/jobs.py +0 -0
  42. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/k8s.py +0 -0
  43. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/pipeline_cmds.py +0 -0
  44. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/cli/release_cmds.py +0 -0
  45. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/indexd/__init__.py +0 -0
  46. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/resolver.py +0 -0
  47. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/delete/delete_all_metadata_for_project.py +0 -0
  48. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/delete/delete_metadata_by_guid.py +0 -0
  49. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/dictionary/upload_dictionary.py +0 -0
  50. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/k8s_ops/argocd_restart_etl.sh +0 -0
  51. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/k8s_ops/argocd_restart_ms.sh +0 -0
  52. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/k8s_ops/argocd_restart_schema.sh +0 -0
  53. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/k8s_ops/login_to_pod.sh +0 -0
  54. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/k8s_ops/restart_etl_and_ms.sh +0 -0
  55. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +0 -0
  56. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/synthetic_data/generate_synth_metadata.sh +0 -0
  57. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +0 -0
  58. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/upload/__init__.py +0 -0
  59. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/upload/metadata_deleter.py +0 -0
  60. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/upload/upload_synthdata_s3.py +0 -0
  61. {gen3_dataops_toolkit-2.1.2 → gen3_dataops_toolkit-2.3.0}/src/g3dt/utils/dbt_utils.py +0 -0
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: gen3-dataops-toolkit
3
+ Version: 2.3.0
4
+ Summary: Gen3 DataOps toolkit (g3dt): operate SSM-published Gen3 data pipeline environments
5
+ License: Apache-2.0
6
+ Author: JoshuaHarris391
7
+ Author-email: harjo391@gmail.com
8
+ Requires-Python: >=3.9.5,<4.0.0
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: awswrangler (>=3.14.0,<4.0.0)
17
+ Requires-Dist: boto3
18
+ Requires-Dist: gen3 (>=4.27.4,<5.0.0)
19
+ Requires-Dist: gen3-metadata (>=1.4.0,<2.0.0)
20
+ Requires-Dist: gen3_validator (>=2.0.0,<3.0.0)
21
+ Requires-Dist: numpy (<2.0.0)
22
+ Requires-Dist: openpyxl (>=3.1.0)
23
+ Requires-Dist: pyarrow (>=14.0.0,<19.0.0)
24
+ Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
25
+ Requires-Dist: python-dotenv
26
+ Requires-Dist: pytz (>=2025.2,<2026.0)
27
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
28
+ Requires-Dist: s3fs (==2025.10.0)
29
+ Requires-Dist: tenacity (>=8.2,<10.0)
30
+ Requires-Dist: typer (>=0.12)
31
+ Requires-Dist: tzlocal (>=5.3.1,<6.0.0)
32
+ Project-URL: Repository, https://github.com/AustralianBioCommons/gen3-dataops-toolkit
33
+ Description-Content-Type: text/markdown
34
+
35
+ # gen3-dataops-toolkit (`g3dt`)
36
+
37
+ Operate Gen3 AWS data-pipeline environments from one pip-installable CLI.
38
+
39
+ `g3dt` is the tooling half of the Gen3 DataOps platform: the
40
+ [gen3-aws-data-pipeline](https://github.com/AustralianBioCommons/gen3-aws-data-pipeline)
41
+ CDK app deploys a complete pipeline per project/environment and publishes every
42
+ resource name to AWS SSM Parameter Store; `g3dt` resolves those names at
43
+ runtime and gives operators one command surface for dictionary deploys,
44
+ metadata upload/delete, indexd registration, EC2 job dispatch, and Kubernetes
45
+ restarts. The dbt half of the platform lives in
46
+ [gen3-dbt-template](https://github.com/AustralianBioCommons/gen3-dbt-template).
47
+
48
+ **No AWS resource name is compiled into this package.** The same wheel
49
+ operates any project: it is targeted purely by `--env`, the project's SSM tree
50
+ (`/{project}/{env}/...`), and a tiny local bootstrap marker.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install gen3-dataops-toolkit
56
+ ```
57
+
58
+ ## Bootstrap (the only local configuration)
59
+
60
+ `g3dt` needs to know just the project and region — everything else comes from
61
+ SSM. Create `~/.g3dt/g3dt.yaml`:
62
+
63
+ ```yaml
64
+ project: etl # your projectId
65
+ region: ap-southeast-2
66
+ default_env: test
67
+ profiles: # optional: AWS named profile per env
68
+ test: etl_test # (omit entirely on EC2/CodeBuild — ambient
69
+ staging: etl_staging # role credentials are used)
70
+ studies: # optional: the project's study registry;
71
+ mystudy_test: # alternatively upload it once per env to
72
+ project_id: MyStudy # s3://<metadata-bucket>/config/studies.yaml
73
+ program_id: program1
74
+ s3_metadata_path: s3://my-bucket/metadata/mystudy/
75
+ ```
76
+
77
+ Search order: `./g3dt.yaml` → `~/.g3dt/g3dt.yaml` → `/etc/g3dt/g3dt.yaml`
78
+ (the EC2 job box's copy, written by CDK user-data). Env vars override:
79
+ `G3DT_PROJECT`, `AWS_REGION`, `G3DT_DEFAULT_ENV`.
80
+
81
+ ## Quick start
82
+
83
+ ```bash
84
+ g3dt config envs # environments with a deployed SSM tree
85
+ g3dt config show --env test # every resolved name — the safety check
86
+ g3dt ec2 up --env test # start the env's job box (SSM-managed)
87
+ g3dt metadata upload --study mystudy --env test --on ec2
88
+ g3dt jobs logs <run-id> --follow # live logs; laptop can sleep, job keeps going
89
+ g3dt ec2 down --env test # or let the auto-stop alarm handle it
90
+ g3dt docs # the full operations overview
91
+ ```
92
+
93
+ ## How configuration works
94
+
95
+ There are exactly two kinds of configuration:
96
+
97
+ - **INPUTS** — human-authored values, committed as
98
+ `config/<projectId>.<env>.json` in the CDK repo and read only by
99
+ `cdk deploy`. To change what an environment *declares*, edit that file and
100
+ redeploy — the value flows to SSM.
101
+ - **OUTPUTS** — every resource name the CDK creates plus the mirrored Gen3
102
+ app facts, published to SSM under `/{project}/{env}/...` on deploy. `g3dt`
103
+ reads these live (cached one round-trip per invocation) and never stores
104
+ them locally.
105
+
106
+ Because the CLI and the infrastructure read the same parameters, they cannot
107
+ disagree — and because each environment has its own tree (including its own
108
+ `ec2/instanceId`), running a job against the wrong environment's resources is
109
+ structurally impossible.
110
+
111
+
112
+ ## CI isolation and the release contract
113
+
114
+ **Only the dbt template's `ci` target is prefixed.** `g3dt config dbt-env`
115
+ emits, alongside the real names, the CI-isolation variants the template's
116
+ `ci` target consumes: `G3DT_DB_RAW_SILVER_CI` / `G3DT_DB_RAW_GOLD_CI`
117
+ (`ci_` + the real database name) and `G3DT_S3_SILVER_DATA_DIR_CI` /
118
+ `G3DT_S3_GOLD_DATA_DIR_CI` (`dbt_ci/` under the same buckets). Commit-
119
+ triggered CI builds land there; every other target (default, local) and the
120
+ release build keep the real, unprefixed names — so CI can never advance the
121
+ warehouse's Iceberg snapshots that releases pin. The library enforces the
122
+ other half: `find_db_for_model` always skips `ci_`-prefixed databases, so
123
+ `g3dt release write` can never pin a release to a CI-build snapshot.
124
+
125
+ **Snapshot pinning.** `AthenaValidationWriter.construct_json` /
126
+ `AthenaGoldWriter.construct_json` honour a pre-set `snapshot_id` (reading the
127
+ table `FOR VERSION AS OF` that snapshot) and only fetch the latest snapshot
128
+ when unpinned — the contract the release-JSON export relies on for
129
+ reproducible releases.
130
+
131
+ **Concurrency.** `release_writer.run` processes models with a bounded thread
132
+ pool (`max_workers`, default 8) and fails at the end naming every failed
133
+ model (inserts are idempotent — re-run to fill the remainder). The S3
134
+ writers (`write_release_jsons_to_s3`, `write_validation_json_to_s3`) accept
135
+ `s3_client=` (pass one per worker thread) and `key_prefix=` (write a
136
+ verification tree without touching real artifacts).
137
+
138
+ **The validation gate.** `g3dt.validate.run_validation_gate(glue_database,
139
+ athena_s3_output, aws_region, workgroup)` queries the latest
140
+ `validation_id` in `full_validation_results` for REAL failures — the
141
+ known-noise patterns in `VALIDATION_GATE_IGNORED_ERRORS` and synthetic
142
+ studies are excluded. The validator Glue job fails when rows come back, so a
143
+ green validation Step Function means schema-clean data; the operator loop is
144
+ gate fails -> inspect the results table -> fix data -> re-run until green.
145
+ `validate_pipeline` also accepts pre-computed loop-invariants
146
+ (`schema=`/`resolver=`/`metadata_table=`) and `write_iceberg=False` so a
147
+ multi-study caller resolves the schema once, lists the validation prefix
148
+ once, and batches all studies into a single Iceberg INSERT.
149
+
150
+ ### Where the data dictionary comes from
151
+
152
+ Composed from the env's inputs as
153
+ `{dictionary_base_url}/{schema_repo}/refs/tags/{dictionary_version}/{dictionary_path}`.
154
+ Only `schema_repo` and `dictionary_version` are required; `app/dictionary_base_url`
155
+ and `app/dictionary_path` are optional and default to raw GitHub and the schema
156
+ repo's conventional layout, so environments deployed before they existed keep
157
+ working. `g3dt config show --env <env>` prints the composed URL.
158
+
159
+ ### Promoting a dictionary across environments
160
+
161
+ A dictionary version is *content*, not infrastructure: it changes far more often
162
+ than buckets or clusters do. Rather than a `cdk deploy` per environment per
163
+ version, `dict pull`, `dict upload` and `dict deploy` all accept `--version`:
164
+
165
+ ```bash
166
+ g3dt dict deploy --env test --version v1.1.7
167
+ g3dt dict deploy --env staging --version v1.1.7 # same tag, no cdk deploy
168
+ ```
169
+
170
+ An override does not persist, so `config show` keeps reporting the declared
171
+ version until the CDK config catches up — `g3dt config diff --env <env>` reports
172
+ exactly that gap and exits 1, so it can gate CI.
173
+
174
+ Synthetic data is only schema-valid against the dictionary that generated it, so
175
+ `synth generate` records the dictionary version in each batch and `synth upload`
176
+ refuses a batch that doesn't match the version being uploaded (override with
177
+ `--allow-version-mismatch`).
178
+
179
+ ## Verifying download access (check-download)
180
+
181
+ Registration alone does not prove a file can be downloaded. Two failure modes
182
+ are invisible until a user clicks the file in the portal: an Indexd record
183
+ with no storage URL (nothing to download, ever), and a record Fence refuses
184
+ to sign a URL for. `g3dt indexd check-download` walks the exact chain the
185
+ portal hits — Indexd record → storage URL → DRS object → access methods →
186
+ Fence signed URL — and reports PASS/FAIL per object, exiting non-zero if any
187
+ object fails so it can gate a deployment step.
188
+
189
+ Run it before a release, and after registering new files. The env selects the
190
+ API key secret and the key's JWT selects the commons, so there is no URL to
191
+ pass (and none to get wrong).
192
+
193
+ ```bash
194
+ g3dt indexd check-download --env staging # sample the 25 newest
195
+ g3dt indexd check-download --env staging --limit 50
196
+ g3dt indexd check-download --env prod PREFIX/<uuid-1> PREFIX/<uuid-2>
197
+ ```
198
+
199
+ With no GUIDs, the newest objects for the env's commons are sampled from the
200
+ indexd registry (latest revision per baseid). The registry may live in a
201
+ different AWS account than the commons being checked; if the env's AWS
202
+ profile cannot reach it, pass GUIDs explicitly.
203
+
204
+ Reading a failure:
205
+
206
+ | Symptom | Meaning |
207
+ |---|---|
208
+ | `Indexd status: 404` | the object is not registered — a registration problem, not a download one |
209
+ | `urls: []` / no access methods | registered but with no storage location; it can never download |
210
+ | `Access endpoint … 401` | authorization: the API key's user lacks `read-storage` on the record's `authz` resource — an authz gap, not a broken key |
211
+ | `Access endpoint … 500` | Fence has the permission but failed to sign — a service-side fault |
212
+
213
+ On a 401, compare what the record requires
214
+ (`https://commons.example.org/index/<did>`, the `authz` field) with what the
215
+ key's user actually holds (`https://commons.example.org/user/user`):
216
+ downloads require `read-storage` on the record's authz resource, which a user
217
+ holding only `create` does not have.
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ poetry install
223
+ poetry run python3 -m pytest
224
+ ```
225
+
226
+ ## Provenance
227
+
228
+ This toolkit was ported (working tree only) from
229
+ [AustralianBioCommons/acdc-aws-etl-pipeline](https://github.com/AustralianBioCommons/acdc-aws-etl-pipeline),
230
+ the ACDC ETL monolith, as part of the Gen3 DataOps platform refactor (2026).
231
+ It starts at version **2.0.0**; versions ≤ 1.2.0 on PyPI are the legacy
232
+ `acdc_aws_etl_pipeline` package, which continues to operate the legacy ACDC
233
+ pipeline unchanged.
234
+
@@ -0,0 +1,199 @@
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
+ ## Verifying download access (check-download)
146
+
147
+ Registration alone does not prove a file can be downloaded. Two failure modes
148
+ are invisible until a user clicks the file in the portal: an Indexd record
149
+ with no storage URL (nothing to download, ever), and a record Fence refuses
150
+ to sign a URL for. `g3dt indexd check-download` walks the exact chain the
151
+ portal hits — Indexd record → storage URL → DRS object → access methods →
152
+ Fence signed URL — and reports PASS/FAIL per object, exiting non-zero if any
153
+ object fails so it can gate a deployment step.
154
+
155
+ Run it before a release, and after registering new files. The env selects the
156
+ API key secret and the key's JWT selects the commons, so there is no URL to
157
+ pass (and none to get wrong).
158
+
159
+ ```bash
160
+ g3dt indexd check-download --env staging # sample the 25 newest
161
+ g3dt indexd check-download --env staging --limit 50
162
+ g3dt indexd check-download --env prod PREFIX/<uuid-1> PREFIX/<uuid-2>
163
+ ```
164
+
165
+ With no GUIDs, the newest objects for the env's commons are sampled from the
166
+ indexd registry (latest revision per baseid). The registry may live in a
167
+ different AWS account than the commons being checked; if the env's AWS
168
+ profile cannot reach it, pass GUIDs explicitly.
169
+
170
+ Reading a failure:
171
+
172
+ | Symptom | Meaning |
173
+ |---|---|
174
+ | `Indexd status: 404` | the object is not registered — a registration problem, not a download one |
175
+ | `urls: []` / no access methods | registered but with no storage location; it can never download |
176
+ | `Access endpoint … 401` | authorization: the API key's user lacks `read-storage` on the record's `authz` resource — an authz gap, not a broken key |
177
+ | `Access endpoint … 500` | Fence has the permission but failed to sign — a service-side fault |
178
+
179
+ On a 401, compare what the record requires
180
+ (`https://commons.example.org/index/<did>`, the `authz` field) with what the
181
+ key's user actually holds (`https://commons.example.org/user/user`):
182
+ downloads require `read-storage` on the record's authz resource, which a user
183
+ holding only `create` does not have.
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ poetry install
189
+ poetry run python3 -m pytest
190
+ ```
191
+
192
+ ## Provenance
193
+
194
+ This toolkit was ported (working tree only) from
195
+ [AustralianBioCommons/acdc-aws-etl-pipeline](https://github.com/AustralianBioCommons/acdc-aws-etl-pipeline),
196
+ the ACDC ETL monolith, as part of the Gen3 DataOps platform refactor (2026).
197
+ It starts at version **2.0.0**; versions ≤ 1.2.0 on PyPI are the legacy
198
+ `acdc_aws_etl_pipeline` package, which continues to operate the legacy ACDC
199
+ pipeline unchanged.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "gen3-dataops-toolkit"
3
- version = "2.1.2"
3
+ version = "2.3.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"
@@ -28,6 +28,7 @@ awswrangler = ">=3.14.0,<4.0.0"
28
28
  pyarrow = ">=14.0.0,<19.0.0"
29
29
  gen3-metadata = ">=1.4.0,<2.0.0"
30
30
  typer = ">=0.12"
31
+ openpyxl = ">=3.1.0"
31
32
 
32
33
  [tool.poetry.scripts]
33
34
  g3dt = "g3dt.cli.main:main"
@@ -33,18 +33,6 @@ def require_test_env(env: str) -> None:
33
33
  raise typer.Exit(2)
34
34
 
35
35
 
36
- def abort_if_prod(env: str) -> None:
37
- """Hard abort for bulk operations that must never touch production."""
38
- if is_prod(env):
39
- typer.secho(
40
- f"Refusing bulk operation against a production environment "
41
- f"('{env}').",
42
- fg=typer.colors.RED,
43
- err=True,
44
- )
45
- raise typer.Exit(2)
46
-
47
-
48
36
  def confirm_destructive(action: str, target: str, env: str, assume_yes: bool) -> None:
49
37
  """Gate a destructive operation with an appropriate confirmation.
50
38
 
@@ -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)