gen3-dataops-toolkit 3.2.0__tar.gz → 3.4.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 (56) hide show
  1. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/PKG-INFO +28 -2
  2. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/README.md +26 -0
  3. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/pyproject.toml +2 -2
  4. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/config_cmds.py +19 -0
  5. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/synth.py +88 -8
  6. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/config.py +35 -2
  7. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/synthetic_data/generate_synth_metadata.sh +16 -10
  8. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/validate/validate.py +86 -21
  9. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/__init__.py +0 -0
  10. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/__init__.py +0 -0
  11. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/_internal/__init__.py +0 -0
  12. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/_internal/dispatch.py +0 -0
  13. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/_internal/registry.py +0 -0
  14. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/_internal/resolve.py +0 -0
  15. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/_internal/runner.py +0 -0
  16. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/_internal/safety.py +0 -0
  17. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/delete_cmds.py +0 -0
  18. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/dict_cmds.py +0 -0
  19. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/ec2_cmds.py +0 -0
  20. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/indexd_cmds.py +0 -0
  21. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/jobs.py +0 -0
  22. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/k8s.py +0 -0
  23. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/main.py +0 -0
  24. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/metadata.py +0 -0
  25. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/pipeline_cmds.py +0 -0
  26. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/cli/release_cmds.py +0 -0
  27. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/indexd/__init__.py +0 -0
  28. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/indexd/file_access.py +0 -0
  29. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/indexd/indexd_registrar.py +0 -0
  30. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/ingest/ingest.py +0 -0
  31. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/resolver.py +0 -0
  32. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/delete/delete_all_metadata_for_project.py +0 -0
  33. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/delete/delete_metadata.sh +0 -0
  34. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/delete/delete_metadata_by_guid.py +0 -0
  35. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/dictionary/deploy_dd.sh +0 -0
  36. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/dictionary/pull_dict.sh +0 -0
  37. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/dictionary/upload_dictionary.py +0 -0
  38. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/indexd/register_indexd.py +0 -0
  39. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/indexd/verify_file_access.py +0 -0
  40. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/k8s_ops/argocd_restart_etl.sh +0 -0
  41. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/k8s_ops/argocd_restart_ms.sh +0 -0
  42. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/k8s_ops/argocd_restart_schema.sh +0 -0
  43. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/k8s_ops/login_to_pod.sh +0 -0
  44. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/k8s_ops/restart_etl_and_ms.sh +0 -0
  45. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +0 -0
  46. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +0 -0
  47. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +0 -0
  48. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/upload/metadata/upload_all_studies.sh +0 -0
  49. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/services/upload/metadata/upload_metadata.py +0 -0
  50. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/upload/__init__.py +0 -0
  51. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/upload/metadata_deleter.py +0 -0
  52. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/upload/metadata_submitter.py +0 -0
  53. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/upload/upload_synthdata_s3.py +0 -0
  54. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/utils/athena_utils.py +0 -0
  55. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/utils/dbt_utils.py +0 -0
  56. {gen3_dataops_toolkit-3.2.0 → gen3_dataops_toolkit-3.4.0}/src/g3dt/utils/release_writer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gen3-dataops-toolkit
3
- Version: 3.2.0
3
+ Version: 3.4.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
@@ -17,7 +17,7 @@ Requires-Dist: awswrangler (>=3.14.0,<4.0.0)
17
17
  Requires-Dist: boto3
18
18
  Requires-Dist: gen3 (>=4.27.4,<5.0.0)
19
19
  Requires-Dist: gen3-metadata (>=1.4.0,<2.0.0)
20
- Requires-Dist: gen3_validator (>=2.0.0,<3.0.0)
20
+ Requires-Dist: gen3_validator (>=2.3.0,<3.0.0)
21
21
  Requires-Dist: numpy (<2.0.0)
22
22
  Requires-Dist: openpyxl (>=3.1.0)
23
23
  Requires-Dist: pyarrow (>=14.0.0,<19.0.0)
@@ -182,6 +182,32 @@ Synthetic data is only schema-valid against the dictionary that generated it, so
182
182
  refuses a batch that doesn't match the version being uploaded (override with
183
183
  `--allow-version-mismatch`).
184
184
 
185
+ ### Synthetic data: LLM configuration
186
+
187
+ `synth generate --llm` and `synth deploy` generate LLM-realistic values with
188
+ [gen3-metadata-simulator](https://github.com/AustralianBioCommons/gen3-metadata-simulator).
189
+ The provider and model resolve with precedence **CLI flags > SSM > default**:
190
+ the CDK config's optional `llm` block publishes `app/llm_provider` /
191
+ `app/llm_model` to SSM, so every operator gets the deployment's values, and
192
+ `--llm-provider` / `--llm-model` override them for one run (e.g. to try a
193
+ model before adding it to the CDK config). Environments deployed without the
194
+ block fall back to provider `anthropic`, and the `--llm` path errors with
195
+ guidance when no model is configured anywhere.
196
+
197
+ Only the API key stays local — as a *path* to the file holding it, never the
198
+ key itself, set once per operator:
199
+
200
+ ```bash
201
+ g3dt config set llm_api_key_file ~/.g3dt/anthropic_api_key
202
+ g3dt synth generate AusDiab_Simulated --llm -n 5 -e test
203
+ ```
204
+
205
+ (or per run with `--llm-api-key-file`; the vendor env var `ANTHROPIC_API_KEY`
206
+ / `OPENAI_API_KEY` also works as a fallback.) The old `~/.g3dt/.env`
207
+ (`LLM_PROVIDER`/`LLM_MODEL`/`LLM_API_KEY_FILE`) is **no longer read**.
208
+ `g3dt config show --env <env>` prints the resolved provider, model, and key
209
+ path.
210
+
185
211
  ## Verifying download access (check-download)
186
212
 
187
213
  Registration alone does not prove a file can be downloaded. Two failure modes
@@ -148,6 +148,32 @@ Synthetic data is only schema-valid against the dictionary that generated it, so
148
148
  refuses a batch that doesn't match the version being uploaded (override with
149
149
  `--allow-version-mismatch`).
150
150
 
151
+ ### Synthetic data: LLM configuration
152
+
153
+ `synth generate --llm` and `synth deploy` generate LLM-realistic values with
154
+ [gen3-metadata-simulator](https://github.com/AustralianBioCommons/gen3-metadata-simulator).
155
+ The provider and model resolve with precedence **CLI flags > SSM > default**:
156
+ the CDK config's optional `llm` block publishes `app/llm_provider` /
157
+ `app/llm_model` to SSM, so every operator gets the deployment's values, and
158
+ `--llm-provider` / `--llm-model` override them for one run (e.g. to try a
159
+ model before adding it to the CDK config). Environments deployed without the
160
+ block fall back to provider `anthropic`, and the `--llm` path errors with
161
+ guidance when no model is configured anywhere.
162
+
163
+ Only the API key stays local — as a *path* to the file holding it, never the
164
+ key itself, set once per operator:
165
+
166
+ ```bash
167
+ g3dt config set llm_api_key_file ~/.g3dt/anthropic_api_key
168
+ g3dt synth generate AusDiab_Simulated --llm -n 5 -e test
169
+ ```
170
+
171
+ (or per run with `--llm-api-key-file`; the vendor env var `ANTHROPIC_API_KEY`
172
+ / `OPENAI_API_KEY` also works as a fallback.) The old `~/.g3dt/.env`
173
+ (`LLM_PROVIDER`/`LLM_MODEL`/`LLM_API_KEY_FILE`) is **no longer read**.
174
+ `g3dt config show --env <env>` prints the resolved provider, model, and key
175
+ path.
176
+
151
177
  ## Verifying download access (check-download)
152
178
 
153
179
  Registration alone does not prove a file can be downloaded. Two failure modes
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "gen3-dataops-toolkit"
3
- version = "3.2.0"
3
+ version = "3.4.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"
@@ -19,7 +19,7 @@ numpy = "<2.0.0"
19
19
  pyyaml = ">=6.0.2,<7.0.0"
20
20
  tzlocal = ">=5.3.1,<6.0.0"
21
21
  gen3 = ">=4.27.4, <5.0.0"
22
- gen3_validator = ">=2.0.0,<3.0.0"
22
+ gen3_validator = ">=2.3.0,<3.0.0"
23
23
  pytz = ">=2025.2,<2026.0"
24
24
  s3fs = "2025.10.0"
25
25
  pyjwt = ">=2.10.1,<3.0.0"
@@ -118,6 +118,11 @@ def show(
118
118
  typer.echo(f" namespace : {e.namespace}")
119
119
  typer.echo(f" cluster_name : {e.cluster_name}")
120
120
  typer.echo(f" ec2_instance_id : {e.ec2_instance_id}")
121
+ # Synthetic-data LLM facts: provider/model from SSM (the CDK's optional
122
+ # llm block); only the key *path* is local, from the marker.
123
+ typer.echo(f" llm_provider : {e.llm_provider}")
124
+ typer.echo(f" llm_model : {e.llm_model or '(not set — pass --llm-model or add the llm block to the CDK config)'}")
125
+ typer.echo(f" llm_api_key_file : {config.llm_api_key_file() or '(not set — g3dt config set llm_api_key_file <path>)'}")
121
126
  if study:
122
127
  s = study_of(study, env)
123
128
  typer.secho(f"Study: {study} -> {s.key}", bold=True)
@@ -199,6 +204,20 @@ def diff(
199
204
  check(f"gen3.{camel}", gen3.get(camel), rc.get(f"app/{leaf}"))
200
205
  check("toolkitVersion", inputs.get("toolkitVersion"), rc.get("meta/toolkitVersion"))
201
206
 
207
+ # Optional inputs: compared only when the file defines them — an absent
208
+ # input legitimately publishes no SSM parameter (or, for the dictionary
209
+ # fields, predates their addition), so absence on both sides is not drift.
210
+ for camel, leaf in {
211
+ "dictionaryBaseUrl": "dictionary_base_url",
212
+ "dictionaryPath": "dictionary_path",
213
+ }.items():
214
+ if camel in gen3:
215
+ check(f"gen3.{camel}", gen3.get(camel), rc.get(f"app/{leaf}"))
216
+ llm = inputs.get("llm") or {}
217
+ for camel, leaf in {"provider": "llm_provider", "model": "llm_model"}.items():
218
+ if camel in llm:
219
+ check(f"llm.{camel}", llm.get(camel), rc.get(f"app/{leaf}"))
220
+
202
221
  if not drift:
203
222
  typer.secho(
204
223
  f"No drift: SSM /{project}/{config.env_base(env)} matches {file}.",
@@ -9,9 +9,14 @@ Every command accepts ``--env``; targeting a **production** environment (any env
9
9
  whose name contains ``prod``) shows a warning and requires typing the env name to
10
10
  confirm — it cannot be bypassed.
11
11
 
12
- Generation defaults to keyless ``random`` data (no API calls). Pass ``--llm`` for
13
- LLM-realistic values, which needs an API key configured in a ``.env`` in the
14
- working directory (``LLM_PROVIDER`` / ``LLM_MODEL`` / ``LLM_API_KEY_FILE``).
12
+ Generation defaults to keyless ``random`` data (no API calls). Pass ``--llm``
13
+ for LLM-realistic values. The LLM provider and model come from the
14
+ environment's SSM tree (the CDK config's optional ``llm`` block, published as
15
+ ``app/llm_provider`` / ``app/llm_model``) and can be overridden per run with
16
+ ``--llm-provider`` / ``--llm-model``. Only the API key stays local: point at
17
+ its file once with ``g3dt config set llm_api_key_file <path>`` (or per run
18
+ with ``--llm-api-key-file``); the vendor env var ``ANTHROPIC_API_KEY`` /
19
+ ``OPENAI_API_KEY`` also works. The old ``~/.g3dt/.env`` is no longer read.
15
20
  """
16
21
  from __future__ import annotations
17
22
 
@@ -21,6 +26,7 @@ from typing import Optional
21
26
 
22
27
  import typer
23
28
 
29
+ from g3dt import config
24
30
  from g3dt.config import (
25
31
  dictionary_filename,
26
32
  dictionary_url,
@@ -38,6 +44,45 @@ app = typer.Typer(
38
44
 
39
45
  SYNTH_DIR = Path("~/.g3dt/synth_metadata").expanduser()
40
46
 
47
+
48
+ def _llm_env_overrides(
49
+ e,
50
+ llm_provider: Optional[str],
51
+ llm_model: Optional[str],
52
+ llm_api_key_file: Optional[Path],
53
+ ) -> dict:
54
+ """Resolve the effective LLM settings for a run: flags > SSM > default.
55
+
56
+ Returns env-var overrides for the generator script, which forwards
57
+ provider/model to gen3-metadata-simulator as CLI flags (the simulator's
58
+ own precedence puts flags first). Exits with guidance when no model is
59
+ configured anywhere; the key-file path is optional — the simulator falls
60
+ back to the vendor env var and raises its own error if neither exists.
61
+ """
62
+ effective_provider = llm_provider or e.llm_provider
63
+ effective_model = llm_model or e.llm_model
64
+ if not effective_model:
65
+ typer.secho(
66
+ "No LLM model configured. Set the llm block in the CDK config "
67
+ "(published to SSM as app/llm_model) and redeploy, or pass "
68
+ "--llm-model for this run.",
69
+ fg=typer.colors.RED,
70
+ err=True,
71
+ )
72
+ raise typer.Exit(1)
73
+ overrides = {
74
+ "G3DT_LLM_PROVIDER": effective_provider,
75
+ "G3DT_LLM_MODEL": effective_model,
76
+ }
77
+ key_file = (
78
+ str(Path(llm_api_key_file).expanduser())
79
+ if llm_api_key_file
80
+ else config.llm_api_key_file()
81
+ )
82
+ if key_file:
83
+ overrides["LLM_API_KEY_FILE"] = key_file
84
+ return overrides
85
+
41
86
  #: Written into each generated batch so `synth upload` can tell which dictionary
42
87
  #: produced it. A batch is only valid against that dictionary, and the directory
43
88
  #: name alone cannot be trusted: --schema and --version are separate options, so
@@ -110,19 +155,35 @@ def deploy(
110
155
  env: str = typer.Option(
111
156
  "test", "--env", "-e", help="Target environment (prod requires typed confirmation)."
112
157
  ),
158
+ llm_provider: Optional[str] = typer.Option(
159
+ None, "--llm-provider",
160
+ help="LLM vendor override (anthropic|openai); default: the env's SSM app/llm_provider.",
161
+ ),
162
+ llm_model: Optional[str] = typer.Option(
163
+ None, "--llm-model",
164
+ help="LLM model override; default: the env's SSM app/llm_model.",
165
+ ),
166
+ llm_api_key_file: Optional[Path] = typer.Option(
167
+ None, "--llm-api-key-file", exists=True, dir_okay=False,
168
+ help="Path to the file holding the LLM API key; default: the marker's "
169
+ "llm_api_key_file (set once: g3dt config set llm_api_key_file <path>).",
170
+ ),
113
171
  ) -> None:
114
172
  """Full end-to-end synthetic deploy (dict + LLM-generate + upload + restarts).
115
173
 
116
174
  Wraps services/synthetic_data/full_deploy_dd_and_synth.sh (LLM-backed
117
- generation). Requires an LLM key configured in .env.
175
+ generation). Provider/model come from the env's SSM tree unless
176
+ overridden; the API key path comes from --llm-api-key-file or the marker.
118
177
  """
119
178
  e = env_of(env)
120
179
  safety.confirm_prod_strict("synthetic full deploy", env)
180
+ env_vars = script_env(e)
181
+ env_vars.update(_llm_env_overrides(e, llm_provider, llm_model, llm_api_key_file))
121
182
  runner.run(
122
183
  runner.bash_script(
123
184
  "services/synthetic_data/full_deploy_dd_and_synth.sh", env
124
185
  ),
125
- env=script_env(e),
186
+ env=env_vars,
126
187
  )
127
188
 
128
189
 
@@ -147,8 +208,22 @@ def generate(
147
208
  llm: bool = typer.Option(
148
209
  False,
149
210
  "--llm",
150
- help="Generate LLM-realistic values; reads LLM config from a .env in the "
151
- "working directory. Default is keyless random data (no API key, no API calls).",
211
+ help="Generate LLM-realistic values; provider/model resolve from the "
212
+ "env's SSM tree (override with --llm-provider/--llm-model). Default is "
213
+ "keyless random data (no API key, no API calls).",
214
+ ),
215
+ llm_provider: Optional[str] = typer.Option(
216
+ None, "--llm-provider",
217
+ help="LLM vendor override (anthropic|openai); default: the env's SSM app/llm_provider.",
218
+ ),
219
+ llm_model: Optional[str] = typer.Option(
220
+ None, "--llm-model",
221
+ help="LLM model override; default: the env's SSM app/llm_model.",
222
+ ),
223
+ llm_api_key_file: Optional[Path] = typer.Option(
224
+ None, "--llm-api-key-file", exists=True, dir_okay=False,
225
+ help="Path to the file holding the LLM API key; default: the marker's "
226
+ "llm_api_key_file (set once: g3dt config set llm_api_key_file <path>).",
152
227
  ),
153
228
  seed: int = typer.Option(None, "--seed", help="RNG seed for reproducible output."),
154
229
  schema: str = typer.Option(
@@ -227,11 +302,16 @@ def generate(
227
302
  args += ["--num-records", num_records]
228
303
  if seed is not None:
229
304
  args += ["--seed", str(seed)]
305
+ env_vars = script_env(e, ver)
306
+ if effective_provider == "llm":
307
+ env_vars.update(
308
+ _llm_env_overrides(e, llm_provider, llm_model, llm_api_key_file)
309
+ )
230
310
  runner.run(
231
311
  runner.bash_script(
232
312
  "services/synthetic_data/generate_synth_metadata.sh", *args
233
313
  ),
234
- env=script_env(e, ver),
314
+ env=env_vars,
235
315
  )
236
316
  # Only after a successful generate: runner.run raises on failure, so a
237
317
  # half-written batch is never stamped as valid.
@@ -50,11 +50,21 @@ DEFAULT_REGION = "ap-southeast-2"
50
50
  DEFAULT_DICT_BASE_URL = "https://raw.githubusercontent.com"
51
51
  DEFAULT_DICT_PATH = "dictionary/prod_dict/acdc_schema.json"
52
52
 
53
+ #: Synthetic-data LLM facts, published by the CDK's OPTIONAL ``llm`` config
54
+ #: block as ``app/llm_provider`` / ``app/llm_model`` and consumed by
55
+ #: gen3-metadata-simulator through ``g3dt synth``. Optional app inputs:
56
+ #: environments deployed without the block fall back to this provider default.
57
+ #: The model deliberately has no default — ``g3dt synth`` errors with guidance
58
+ #: when the ``--llm`` path is used and no model is configured anywhere.
59
+ DEFAULT_LLM_PROVIDER = "anthropic"
60
+
53
61
  #: Marker locations, most specific first.
54
62
  MARKER_PATHS = ("g3dt.yaml", "~/.g3dt/g3dt.yaml", "/etc/g3dt/g3dt.yaml")
55
63
 
56
- #: Marker keys `g3dt config set` may write.
57
- SETTABLE_MARKER_KEYS = ("project", "region", "default_env")
64
+ #: Marker keys `g3dt config set` may write. ``llm_api_key_file`` is the path
65
+ #: to the file holding the synth LLM API key — the one LLM setting that stays
66
+ #: local (the provider and model come from SSM; the key never leaves the box).
67
+ SETTABLE_MARKER_KEYS = ("project", "region", "default_env", "llm_api_key_file")
58
68
 
59
69
  #: Gen3 app facts mirrored to SSM /{project}/{env}/app/* by the CDK.
60
70
  REQUIRED_APP_KEYS = (
@@ -150,6 +160,18 @@ def require_project(marker: Optional[dict] = None) -> str:
150
160
  return project
151
161
 
152
162
 
163
+ def llm_api_key_file(marker: Optional[dict] = None) -> Optional[str]:
164
+ """Path to the file holding the synth LLM API key, from the marker.
165
+
166
+ Set once per operator with ``g3dt config set llm_api_key_file <path>``.
167
+ Returns ``None`` when unset — gen3-metadata-simulator then falls back to
168
+ the vendor's standard env var (``ANTHROPIC_API_KEY`` / ``OPENAI_API_KEY``).
169
+ """
170
+ m = marker if marker is not None else load_marker()
171
+ value = m.get("llm_api_key_file")
172
+ return str(Path(str(value)).expanduser()) if value else None
173
+
174
+
153
175
  def set_marker_value(key: str, value: str) -> Tuple[Optional[str], str, Path]:
154
176
  """Set one bootstrap key in the user's marker file and write it back.
155
177
 
@@ -230,6 +252,11 @@ class EnvConfig:
230
252
  # hand-built EnvConfig still composes a valid URL.
231
253
  dictionary_base_url: str = DEFAULT_DICT_BASE_URL
232
254
  dictionary_path: str = DEFAULT_DICT_PATH
255
+ # Optional synthetic-data LLM inputs (SSM app/llm_provider, app/llm_model).
256
+ # The model has no default on purpose: synth's --llm path checks and errors
257
+ # with guidance rather than silently picking a model.
258
+ llm_provider: str = DEFAULT_LLM_PROVIDER
259
+ llm_model: Optional[str] = None
233
260
 
234
261
 
235
262
  def _app_or_default(rc, leaf: str, default: str) -> str:
@@ -289,6 +316,10 @@ def resolve_env(env: str, project: Optional[str] = None) -> EnvConfig:
289
316
  rc, "dictionary_base_url", DEFAULT_DICT_BASE_URL
290
317
  ),
291
318
  dictionary_path=_app_or_default(rc, "dictionary_path", DEFAULT_DICT_PATH),
319
+ # Same optional-app-fact contract: the CDK publishes these only when
320
+ # the config has an llm block, so absence means "use the defaults".
321
+ llm_provider=_app_or_default(rc, "llm_provider", DEFAULT_LLM_PROVIDER),
322
+ llm_model=(_app_or_default(rc, "llm_model", "") or None),
292
323
  )
293
324
 
294
325
 
@@ -395,6 +426,8 @@ def script_env(e: EnvConfig, version: Optional[str] = None) -> Dict[str, str]:
395
426
  "G3DT_NAMESPACE": e.namespace,
396
427
  "G3DT_CLUSTER_NAME": e.cluster_name,
397
428
  "G3DT_SCHEMA_REPO": e.schema_repo,
429
+ "G3DT_LLM_PROVIDER": e.llm_provider,
430
+ "G3DT_LLM_MODEL": e.llm_model,
398
431
  }
399
432
  env.update({k: v for k, v in values.items() if v is not None})
400
433
  return env
@@ -8,14 +8,13 @@
8
8
  # The tool takes a LOCAL bundled Gen3 schema file (pulled by pull_dict.sh into
9
9
  # ~/.g3dt/schemas/acdc_schema_<version>.json, or $G3DT_SCHEMA_DIR if set). The
10
10
  # default provider is keyless 'random'; pass --provider llm for LLM-realistic
11
- # values, in which case LLM config is read from ~/.g3dt/.env (or $G3DT_ENV_FILE
12
- # if set): LLM_PROVIDER / LLM_MODEL / LLM_API_KEY_FILE.
11
+ # values. The LLM vendor/model arrive as $G3DT_LLM_PROVIDER / $G3DT_LLM_MODEL
12
+ # (resolved by g3dt: CLI flags > SSM app/llm_* > default) and are forwarded to
13
+ # the simulator as flags; the API key file path arrives as $LLM_API_KEY_FILE.
14
+ # The old ~/.g3dt/.env is no longer read.
13
15
 
14
16
  set -euo pipefail
15
17
 
16
- # LLM provider config file (lives outside the installed package).
17
- ENV_FILE="${G3DT_ENV_FILE:-$HOME/.g3dt/.env}"
18
-
19
18
  usage() {
20
19
  cat <<EOF
21
20
  Usage: $(basename "$0") --schema <path> --version <ver> [options]
@@ -32,8 +31,8 @@ Options:
32
31
  --num-records N|n1,n2 Records per study: one number for all, or a comma list
33
32
  (one per study). Default: ${DEFAULT_NUM_RECORDS}
34
33
  --provider random|llm Value strategy. Default: ${DEFAULT_PROVIDER}
35
- 'random' needs no key; 'llm' reads LLM config from
36
- ${ENV_FILE}.
34
+ 'random' needs no key; 'llm' uses \$G3DT_LLM_PROVIDER /
35
+ \$G3DT_LLM_MODEL / \$LLM_API_KEY_FILE (set by g3dt).
37
36
  --seed N RNG seed for reproducible output.
38
37
  --output-root DIR Root output dir. Default: ${DEFAULT_OUTPUT_ROOT}
39
38
  -h, --help Show this help and exit.
@@ -123,9 +122,16 @@ for i in "${!STUDY_ARRAY[@]}"; do
123
122
  --num-records "$N"
124
123
  --provider "$PROVIDER")
125
124
  [[ -n "$SEED" ]] && CMD+=(--seed "$SEED")
126
- # Point the LLM provider at the user-level env file regardless of the caller's CWD.
127
- if [[ "$PROVIDER" == "llm" && -f "${ENV_FILE}" ]]; then
128
- CMD+=(--env-file "${ENV_FILE}")
125
+ if [[ "$PROVIDER" == "llm" ]]; then
126
+ # Vendor/model resolved by g3dt (flags > SSM > default) and forwarded
127
+ # as simulator flags, because the simulator's own precedence puts
128
+ # flags above any .env or environment variable.
129
+ [[ -n "${G3DT_LLM_PROVIDER:-}" ]] && CMD+=(--llm-provider "$G3DT_LLM_PROVIDER")
130
+ [[ -n "${G3DT_LLM_MODEL:-}" ]] && CMD+=(--llm-model "$G3DT_LLM_MODEL")
131
+ # Neutralize any .env in the caller's CWD: /dev/null exists (satisfies
132
+ # the simulator's exists=True check) and dotenv-parses to empty, so
133
+ # only the flags above and the inherited $LLM_API_KEY_FILE apply.
134
+ CMD+=(--env-file /dev/null)
129
135
  fi
130
136
  "${CMD[@]}"
131
137
  done
@@ -479,6 +479,49 @@ def truncate_linkage_results(linkage_results_dict: dict) -> dict:
479
479
  return linkage_results_dict
480
480
 
481
481
 
482
+ #: The eight fields every result row carries, as emitted by
483
+ #: ``gen3_validator.validate.validate_object`` (FAIL rows) and
484
+ #: ``gen3_validator.validate.error_record`` (ERROR rows).
485
+ VALIDATION_RESULT_BASE_COLUMNS = [
486
+ "node",
487
+ "index",
488
+ "validation_result",
489
+ "invalid_key",
490
+ "schema_path",
491
+ "validator",
492
+ "validator_value",
493
+ "validation_error",
494
+ ]
495
+
496
+ #: Full column contract of the results table, in write order.
497
+ VALIDATION_RESULT_COLUMNS = [
498
+ "validation_id",
499
+ "index",
500
+ "node",
501
+ "study_id",
502
+ "validation_result",
503
+ "invalid_key",
504
+ "schema_path",
505
+ "validator",
506
+ "validator_value",
507
+ "validation_error",
508
+ "schema_version",
509
+ ]
510
+
511
+ #: Written when a study validates clean, so that every run leaves exactly one
512
+ #: gradeable row. Excluded by the validation gate — see
513
+ #: :func:`run_validation_gate`.
514
+ PASS_MARKER_ROW = {
515
+ "node": None,
516
+ "index": None,
517
+ "validation_result": "PASS",
518
+ "invalid_key": None,
519
+ "schema_path": None,
520
+ "validator": None,
521
+ "validator_value": None,
522
+ "validation_error": None,
523
+ }
524
+
482
525
  #: validation_error patterns treated as known noise by the validation gate —
483
526
  #: structural artefacts of the export (null-typed fields, extra export-side
484
527
  #: properties, the Gen3 'programs' root requirement), not data quality issues.
@@ -497,9 +540,14 @@ def run_validation_gate(glue_database: str, athena_s3_output: str,
497
540
 
498
541
  The gate looks at the most recent ``validation_id`` in the results table,
499
542
  filters out the known-noise error patterns
500
- (:data:`VALIDATION_GATE_IGNORED_ERRORS`) and synthetic studies, and
501
- aggregates what remains. An empty result means the latest validation run
502
- is clean; any rows are genuine schema failures an operator must fix.
543
+ (:data:`VALIDATION_GATE_IGNORED_ERRORS`), PASS markers and synthetic
544
+ studies, and aggregates what remains. An empty result means the latest
545
+ validation run is clean; any rows are genuine schema failures an operator
546
+ must fix.
547
+
548
+ ERROR rows are deliberately NOT filtered out. A record whose node is absent
549
+ from the dictionary could not be checked at all, which is a failure to
550
+ validate rather than a clean result, and must hold the gate closed.
503
551
 
504
552
  Callers (the validator Glue job) FAIL the run when rows come back, so the
505
553
  validation Step Function goes red until the data is fixed and validation
@@ -523,6 +571,7 @@ WHERE validation_id = (
523
571
  FROM "{glue_database}"."{results_table}"
524
572
  )
525
573
  {ignore_clauses}
574
+ AND validation_result <> 'PASS'
526
575
  AND study_id NOT LIKE '%synthetic%'
527
576
  GROUP BY node, study_id, invalid_key, validator_value, validation_error
528
577
  ORDER BY node, study_id
@@ -567,6 +616,7 @@ def validate_pipeline(
567
616
  resolver=None,
568
617
  metadata_table: pd.DataFrame = None,
569
618
  write_iceberg: bool = True,
619
+ results_table: str = "full_validation_results",
570
620
  ) -> pd.DataFrame:
571
621
  """
572
622
  Orchestrate the validation workflow for a study: load + resolve schema, find the latest
@@ -592,9 +642,14 @@ def validate_pipeline(
592
642
  write_iceberg: When False, skip the per-study Iceberg write and leave it
593
643
  to the caller (who can concatenate several studies' results into a
594
644
  single write). The CSV write-back always happens.
645
+ results_table: Table to write into when ``write_iceberg`` is True. Callers
646
+ validating an isolated warehouse (e.g. the CI databases) MUST pass
647
+ their own — the real and CI results must never share a table, since
648
+ the gate grades whichever run has the greatest validation_id.
595
649
 
596
650
  Returns:
597
- The study's validation results DataFrame.
651
+ The study's validation results DataFrame. Never empty: a study with no
652
+ failures yields a single PASS marker row.
598
653
 
599
654
  Raises:
600
655
  RuntimeError: When expected inputs are missing or a pipeline step fails.
@@ -663,24 +718,34 @@ def validate_pipeline(
663
718
  logger.info("Getting Schema Version")
664
719
  schema_version = resolver.get_schema_version(schema = schema)
665
720
 
666
- full_validation_results_df = pd.DataFrame(results)
721
+ if not results:
722
+ # No failures. Record the run anyway: the validation gate grades
723
+ # the greatest validation_id in the results table, so a clean run
724
+ # that writes nothing would leave the previous FAILING run as the
725
+ # latest and the gate could never go green no matter how many
726
+ # times the data was fixed. A PASS marker also guarantees the
727
+ # table exists after the first run, which is what stops a fresh
728
+ # environment reporting TABLE_NOT_FOUND.
729
+ logger.info("No validation failures for study '%s'; writing PASS marker.", study_id)
730
+ results = [PASS_MARKER_ROW.copy()]
731
+
732
+ # Build with an explicit column list rather than inferring it from the
733
+ # rows: pd.DataFrame([]) has NO columns, so the reindex below used to
734
+ # raise KeyError and surface as "Validation failed." — a clean study
735
+ # failing with the same message as a dirty one.
736
+ full_validation_results_df = pd.DataFrame(results, columns=VALIDATION_RESULT_BASE_COLUMNS)
667
737
  full_validation_results_df["validation_id"] = latest_validation_id
668
738
  full_validation_results_df["study_id"] = study_id
669
739
  full_validation_results_df["schema_version"] = schema_version
670
-
671
- full_validation_results_df = full_validation_results_df[[
672
- "validation_id",
673
- "index",
674
- "node",
675
- "study_id",
676
- "validation_result",
677
- "invalid_key",
678
- "schema_path",
679
- "validator",
680
- "validator_value",
681
- "validation_error",
682
- "schema_version"
683
- ]]
740
+
741
+ full_validation_results_df = full_validation_results_df[VALIDATION_RESULT_COLUMNS]
742
+
743
+ # 'index' is the only numeric column. A PASS-only frame leaves it
744
+ # entirely null, which would infer a different Iceberg column type
745
+ # than a frame containing failures — and full_validation_results
746
+ # already exists in deployed environments. Pin it so both shapes
747
+ # write the same schema.
748
+ full_validation_results_df = full_validation_results_df.astype({"index": "Int64"})
684
749
 
685
750
  logger.info("Validation completed (%s rows).", len(full_validation_results_df))
686
751
  except Exception as e:
@@ -703,12 +768,12 @@ def validate_pipeline(
703
768
  logger.info(
704
769
  "Writing Parquet to Glue DB '%s', table '%s'.",
705
770
  glue_database,
706
- "full_validation_results",
771
+ results_table,
707
772
  )
708
773
  write_iceberg_to_db(
709
774
  df=full_validation_results_df,
710
775
  database=glue_database,
711
- table="full_validation_results",
776
+ table=results_table,
712
777
  athena_s3_output=athena_s3_output,
713
778
  workgroup=workgroup,
714
779
  )