gen3-dataops-toolkit 2.0.0__py3-none-any.whl

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 (53) hide show
  1. g3dt/__init__.py +0 -0
  2. g3dt/cli/__init__.py +5 -0
  3. g3dt/cli/_internal/__init__.py +1 -0
  4. g3dt/cli/_internal/dispatch.py +428 -0
  5. g3dt/cli/_internal/registry.py +65 -0
  6. g3dt/cli/_internal/resolve.py +22 -0
  7. g3dt/cli/_internal/runner.py +76 -0
  8. g3dt/cli/_internal/safety.py +110 -0
  9. g3dt/cli/config_cmds.py +202 -0
  10. g3dt/cli/delete_cmds.py +101 -0
  11. g3dt/cli/dict_cmds.py +102 -0
  12. g3dt/cli/ec2_cmds.py +114 -0
  13. g3dt/cli/indexd_cmds.py +57 -0
  14. g3dt/cli/jobs.py +83 -0
  15. g3dt/cli/k8s.py +54 -0
  16. g3dt/cli/main.py +110 -0
  17. g3dt/cli/metadata.py +76 -0
  18. g3dt/cli/synth.py +206 -0
  19. g3dt/config.py +393 -0
  20. g3dt/indexd/__init__.py +0 -0
  21. g3dt/indexd/indexd_registrar.py +244 -0
  22. g3dt/ingest/ingest.py +629 -0
  23. g3dt/resolver.py +163 -0
  24. g3dt/services/delete/delete_all_metadata_for_project.py +170 -0
  25. g3dt/services/delete/delete_metadata.sh +153 -0
  26. g3dt/services/delete/delete_metadata_by_guid.py +338 -0
  27. g3dt/services/dictionary/deploy_dd.sh +65 -0
  28. g3dt/services/dictionary/pull_dict.sh +59 -0
  29. g3dt/services/dictionary/upload_dictionary.py +109 -0
  30. g3dt/services/indexd/register_indexd.py +240 -0
  31. g3dt/services/k8s_ops/argocd_restart_etl.sh +140 -0
  32. g3dt/services/k8s_ops/argocd_restart_ms.sh +102 -0
  33. g3dt/services/k8s_ops/argocd_restart_schema.sh +106 -0
  34. g3dt/services/k8s_ops/login_to_pod.sh +110 -0
  35. g3dt/services/k8s_ops/restart_etl_and_ms.sh +56 -0
  36. g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +183 -0
  37. g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +124 -0
  38. g3dt/services/synthetic_data/generate_synth_metadata.sh +133 -0
  39. g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +165 -0
  40. g3dt/services/upload/metadata/upload_all_studies.sh +108 -0
  41. g3dt/services/upload/metadata/upload_metadata.py +152 -0
  42. g3dt/upload/__init__.py +1 -0
  43. g3dt/upload/metadata_deleter.py +265 -0
  44. g3dt/upload/metadata_submitter.py +1093 -0
  45. g3dt/upload/upload_synthdata_s3.py +164 -0
  46. g3dt/utils/athena_utils.py +834 -0
  47. g3dt/utils/dbt_utils.py +66 -0
  48. g3dt/utils/release_writer.py +188 -0
  49. g3dt/validate/validate.py +609 -0
  50. gen3_dataops_toolkit-2.0.0.dist-info/METADATA +125 -0
  51. gen3_dataops_toolkit-2.0.0.dist-info/RECORD +53 -0
  52. gen3_dataops_toolkit-2.0.0.dist-info/WHEEL +4 -0
  53. gen3_dataops_toolkit-2.0.0.dist-info/entry_points.txt +3 -0
g3dt/cli/jobs.py ADDED
@@ -0,0 +1,83 @@
1
+ """`acdc jobs` — track EC2-dispatched runs by their friendly run id."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ import typer
7
+
8
+ from g3dt.cli._internal import dispatch, registry
9
+
10
+ app = typer.Typer(no_args_is_help=True, help="Track EC2-dispatched jobs.")
11
+
12
+
13
+ def _status_color(state: str) -> str:
14
+ """Map an SSM/derived status to a terminal colour for the all-runs listing."""
15
+ if state == "Success":
16
+ return typer.colors.GREEN
17
+ if state in ("Failed", "Cancelled", "TimedOut"):
18
+ return typer.colors.RED
19
+ if state in ("Pending", "InProgress", "Delayed", "pending"):
20
+ return typer.colors.YELLOW
21
+ return typer.colors.BRIGHT_BLACK # n/a (ssh) / unknown
22
+
23
+
24
+ @app.command(name="list")
25
+ def list_runs() -> None:
26
+ """List recorded EC2 dispatches (most recent first)."""
27
+ runs = registry.all_runs()
28
+ if not runs:
29
+ typer.echo("No dispatched runs recorded.")
30
+ return
31
+ for run_id in sorted(runs, reverse=True):
32
+ rec = runs[run_id]
33
+ typer.echo(
34
+ f"{run_id} env={rec.get('env')} "
35
+ f"via={rec.get('mechanism')} instance={rec.get('instance_id')}"
36
+ )
37
+
38
+
39
+ @app.command()
40
+ def status(
41
+ run_id: Optional[str] = typer.Argument(
42
+ None, help="Run id from dispatch. Omit to show all runs with their status."
43
+ ),
44
+ ) -> None:
45
+ """Show SSM status for one run, or all recorded runs if no run id is given."""
46
+ if run_id is None:
47
+ runs = registry.all_runs()
48
+ if not runs:
49
+ typer.echo("No dispatched runs recorded.")
50
+ return
51
+ for rid in sorted(runs, reverse=True):
52
+ rec = runs[rid]
53
+ state = dispatch.status_label(rec)
54
+ line = f"{rid} env={rec.get('env')} via={rec.get('mechanism')} "
55
+ typer.echo(line, nl=False)
56
+ typer.secho(state, fg=_status_color(state))
57
+ return
58
+
59
+ inv = dispatch.status(run_id)
60
+ state = inv.get("Status", "unknown") if isinstance(inv, dict) else "unknown"
61
+ typer.secho(f"{run_id}: {state}", bold=True)
62
+ if isinstance(inv, dict):
63
+ out = inv.get("StandardOutputContent")
64
+ err = inv.get("StandardErrorContent")
65
+ if out:
66
+ typer.echo(out)
67
+ if err:
68
+ typer.secho(err, fg=typer.colors.RED)
69
+
70
+
71
+ @app.command()
72
+ def stop(run_id: str = typer.Argument(..., help="Run id from dispatch.")) -> None:
73
+ """Stop (cancel) a running EC2-dispatched job."""
74
+ dispatch.stop(run_id)
75
+
76
+
77
+ @app.command()
78
+ def logs(
79
+ run_id: str = typer.Argument(..., help="Run id from dispatch."),
80
+ follow: bool = typer.Option(False, "--follow", "-f", help="Stream new output as it arrives."),
81
+ ) -> None:
82
+ """Print (and optionally follow) the CloudWatch logs for a dispatched run."""
83
+ dispatch.logs(run_id, follow=follow)
g3dt/cli/k8s.py ADDED
@@ -0,0 +1,54 @@
1
+ """`g3dt k8s` — restart Gen3 microservices / ETL via ArgoCD (LOCAL only).
2
+
3
+ These use ``argocd login --sso`` (a browser flow), so they cannot run headless
4
+ on EC2. The wrapped scripts receive their settings as ``G3DT_*`` environment
5
+ variables resolved from SSM — they read no config files.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import typer
10
+
11
+ from g3dt.config import script_env
12
+ from g3dt.cli._internal import runner
13
+ from g3dt.cli._internal.resolve import env_of
14
+
15
+ app = typer.Typer(no_args_is_help=True, help="ArgoCD / Kubernetes restarts (local).")
16
+
17
+ _SCHEMA = "services/k8s_ops/argocd_restart_schema.sh"
18
+ _ETL = "services/k8s_ops/argocd_restart_etl.sh"
19
+ _ETL_AND_MS = "services/k8s_ops/restart_etl_and_ms.sh"
20
+
21
+
22
+ @app.command(name="restart-schema")
23
+ def restart_schema(
24
+ env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
25
+ sync: bool = typer.Option(False, "--sync", "-s", help="argocd app sync first."),
26
+ ) -> None:
27
+ """Restart sheepdog/peregrine/guppy/portal (schema microservices)."""
28
+ e = env_of(env)
29
+ args = ["-d", e.domain, "-a", e.app_name, "-n", e.namespace]
30
+ if sync:
31
+ args.append("-s")
32
+ runner.run(runner.bash_script(_SCHEMA, *args), env=script_env(e))
33
+
34
+
35
+ @app.command(name="restart-etl")
36
+ def restart_etl(
37
+ env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
38
+ sync: bool = typer.Option(False, "--sync", "-s", help="argocd app sync first."),
39
+ ) -> None:
40
+ """Create + run the ETL cronjob and wait for completion."""
41
+ e = env_of(env)
42
+ args = ["-e", env]
43
+ if sync:
44
+ args.append("-s")
45
+ runner.run(runner.bash_script(_ETL, *args), env=script_env(e))
46
+
47
+
48
+ @app.command(name="restart-ms")
49
+ def restart_ms(
50
+ env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
51
+ ) -> None:
52
+ """Restart both ETL and schema microservices (wraps restart_etl_and_ms.sh)."""
53
+ e = env_of(env)
54
+ runner.run(runner.bash_script(_ETL_AND_MS, env), env=script_env(e))
g3dt/cli/main.py ADDED
@@ -0,0 +1,110 @@
1
+ """Root ``g3dt`` Typer application.
2
+
3
+ Assembles every command group and the top-level ``version`` / ``docs`` helpers.
4
+ The console-script entry point in pyproject.toml points at :func:`main`.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import typer
9
+
10
+ from g3dt.cli import (
11
+ config_cmds,
12
+ delete_cmds,
13
+ dict_cmds,
14
+ ec2_cmds,
15
+ indexd_cmds,
16
+ jobs,
17
+ k8s,
18
+ metadata,
19
+ synth,
20
+ )
21
+
22
+ app = typer.Typer(
23
+ no_args_is_help=True,
24
+ rich_markup_mode="rich",
25
+ help="Gen3 DataOps toolkit. Run [bold]g3dt docs[/bold] for an overview.",
26
+ )
27
+
28
+ app.add_typer(dict_cmds.app, name="dict")
29
+ app.add_typer(synth.app, name="synth")
30
+ app.add_typer(metadata.app, name="metadata")
31
+ app.add_typer(delete_cmds.app, name="delete")
32
+ app.add_typer(k8s.app, name="k8s")
33
+ app.add_typer(indexd_cmds.app, name="indexd")
34
+ app.add_typer(ec2_cmds.app, name="ec2")
35
+ app.add_typer(jobs.app, name="jobs")
36
+ app.add_typer(config_cmds.app, name="config")
37
+
38
+
39
+ _DOCS = """\
40
+ Gen3 DataOps toolkit (g3dt) — operations overview
41
+ =================================================
42
+
43
+ Configuration: two kinds, nothing else
44
+ - INPUTS live in the CDK repo (gen3-aws-data-pipeline) as
45
+ config/<project>.<env>.json, read only by `cdk deploy`.
46
+ - Everything else is resolved live from SSM (/{project}/{env}/...), which
47
+ `cdk deploy` publishes. The only local file is the g3dt.yaml marker
48
+ (project/region/default_env, optional profiles:/studies: maps), searched
49
+ at ./g3dt.yaml, ~/.g3dt/g3dt.yaml, /etc/g3dt/g3dt.yaml.
50
+
51
+ Mental model: two execution planes
52
+ - Control plane (LOCAL): dict deploy, k8s restarts. These use the interactive
53
+ `argocd login --sso` browser flow and AWS named profiles, so they run on
54
+ your laptop only.
55
+ - Data plane (LONG jobs): metadata upload/delete, indexd register. Add
56
+ `--on ec2` to run them on the env's job box via SSM (disconnect-safe);
57
+ watch with `g3dt jobs status|logs <run-id> --follow`.
58
+
59
+ Discover everything
60
+ g3dt --help list all command groups
61
+ g3dt <group> --help commands + options for a group
62
+ g3dt config envs environments with a deployed SSM tree
63
+ g3dt config studies studies from your g3dt.yaml marker
64
+ g3dt config show --env test resolved settings (safe, read-only)
65
+
66
+ Typical release runbook (staging shown; repeat for prod with care)
67
+ 1. g3dt dict deploy --env staging
68
+ 2. g3dt metadata upload --study <study> --env staging --on ec2
69
+ 3. g3dt jobs logs <run-id> --follow
70
+ 4. g3dt k8s restart-etl --env staging
71
+
72
+ Synthetic data (test only, all local)
73
+ g3dt synth deploy --env test
74
+
75
+ EC2 / SSM prerequisites
76
+ - The env's job box is created by the CDK (ec2-job-runner stack): SSM-managed,
77
+ toolkit pre-installed by user-data, instance id published to SSM.
78
+ - Local profile needs: ssm:SendCommand / ssm:GetCommandInvocation,
79
+ s3:GetObject on the log prefix, ec2:Start/Stop/DescribeInstances.
80
+
81
+ NOT run by this CLI: the Glue jobs (validation, release-JSON) and the CodeBuild
82
+ dbt pipelines. Those are deployed and triggered via the CDK repo and the
83
+ project's dbt repo.
84
+ """
85
+
86
+
87
+ @app.command()
88
+ def docs() -> None:
89
+ """Print the operations overview (mental model, runbook, prerequisites)."""
90
+ typer.echo(_DOCS)
91
+
92
+
93
+ @app.command()
94
+ def version() -> None:
95
+ """Print the installed gen3-dataops-toolkit version."""
96
+ try:
97
+ from importlib.metadata import version as _v
98
+
99
+ typer.echo(_v("gen3-dataops-toolkit"))
100
+ except Exception: # pragma: no cover - fallback when not installed
101
+ typer.echo("unknown")
102
+
103
+
104
+ def main() -> None:
105
+ """Console-script entry point."""
106
+ app()
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
g3dt/cli/metadata.py ADDED
@@ -0,0 +1,76 @@
1
+ """`g3dt metadata` — upload real study metadata to Gen3 (data-plane).
2
+
3
+ These are the multi-hour jobs, so they support ``--on ec2`` to run on the
4
+ env's EC2 job box via SSM Run Command (disconnect-safe) instead of the laptop.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import typer
9
+
10
+ from g3dt.cli._internal import dispatch
11
+ from g3dt.cli._internal.dispatch import Target
12
+ from g3dt.cli._internal.resolve import study_of
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="Upload study metadata to Gen3.")
15
+
16
+ _UPLOAD = "services/upload/metadata/upload_metadata.py"
17
+ _UPLOAD_ALL = "services/upload/metadata/upload_all_studies.sh"
18
+
19
+
20
+ @app.command()
21
+ def upload(
22
+ study: str = typer.Option(..., "--study", "-s", help="Study, e.g. ausdiab."),
23
+ env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
24
+ node: str = typer.Option(None, "--node", help="Submit only this node type."),
25
+ on: Target = typer.Option(Target.local, "--on", help="Run local or on ec2."),
26
+ ) -> None:
27
+ """Upload a study's release metadata to Gen3 sheepdog.
28
+
29
+ Examples:
30
+ g3dt metadata upload --study ausdiab --env staging
31
+ g3dt metadata upload --study ausdiab --env staging --on ec2
32
+ """
33
+ s = study_of(study, env)
34
+
35
+ def build_args(env_name):
36
+ a = ["--study", s.key, "--env", env_name]
37
+ if node:
38
+ a += ["--specific-node", node]
39
+ return a
40
+
41
+ def remote_cli(env_name):
42
+ a = ["metadata", "upload", "--study", study, "--env", env_name]
43
+ if node:
44
+ a += ["--node", node]
45
+ return a
46
+
47
+ dispatch.run_or_dispatch(
48
+ on, env, _UPLOAD, build_args, "metadata-upload", remote_cli=remote_cli,
49
+ )
50
+
51
+
52
+ @app.command(name="upload-all")
53
+ def upload_all(
54
+ studies: str = typer.Option(
55
+ ..., "--studies", help="Comma-separated studies, e.g. ausdiab,caughtcad."
56
+ ),
57
+ env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
58
+ on: Target = typer.Option(Target.local, "--on", help="Run local or on ec2."),
59
+ ) -> None:
60
+ """Upload several studies sequentially (wraps upload_all_studies.sh).
61
+
62
+ The wrapped script aborts on any 'prod' environment.
63
+ """
64
+ names = [s.strip() for s in studies.split(",") if s.strip()]
65
+ keys = [study_of(name, env).key for name in names]
66
+
67
+ def build_args(env_name):
68
+ return ["--studies", ",".join(keys), "--env", env_name]
69
+
70
+ def remote_cli(env_name):
71
+ return ["metadata", "upload-all", "--studies", studies, "--env", env_name]
72
+
73
+ dispatch.run_or_dispatch(
74
+ on, env, _UPLOAD_ALL, build_args, "metadata-upload-all",
75
+ interpreter="bash", remote_cli=remote_cli,
76
+ )
g3dt/cli/synth.py ADDED
@@ -0,0 +1,206 @@
1
+ """`g3dt synth` — synthetic data lifecycle for any configured environment.
2
+
3
+ Generation uses **gen3-metadata-simulator** (schema-valid). It runs locally:
4
+ synthetic metadata is generated on the laptop (writing under
5
+ ``~/.g3dt/synth_metadata/<version>/<study>/``) and uploaded/deleted from
6
+ there, so there is nothing to run on EC2.
7
+
8
+ Every command accepts ``--env``; targeting a **production** environment (any env
9
+ whose name contains ``prod``) shows a warning and requires typing the env name to
10
+ confirm — it cannot be bypassed.
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``).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+
20
+ import typer
21
+
22
+ from g3dt.config import script_env
23
+ from g3dt.cli._internal import runner, safety
24
+ from g3dt.cli._internal.resolve import env_of
25
+ from g3dt.cli.dict_cmds import SCHEMA_DIR, dict_url
26
+
27
+ app = typer.Typer(
28
+ no_args_is_help=True,
29
+ help="Synthetic data lifecycle (local generation; prod requires typed confirmation).",
30
+ )
31
+
32
+ SYNTH_DIR = Path("~/.g3dt/synth_metadata").expanduser()
33
+
34
+
35
+ @app.command()
36
+ def deploy(
37
+ env: str = typer.Option(
38
+ "test", "--env", "-e", help="Target environment (prod requires typed confirmation)."
39
+ ),
40
+ ) -> None:
41
+ """Full end-to-end synthetic deploy (dict + LLM-generate + upload + restarts).
42
+
43
+ Wraps services/synthetic_data/full_deploy_dd_and_synth.sh (LLM-backed
44
+ generation). Requires an LLM key configured in .env.
45
+ """
46
+ e = env_of(env)
47
+ safety.confirm_prod_strict("synthetic full deploy", env)
48
+ runner.run(
49
+ runner.bash_script(
50
+ "services/synthetic_data/full_deploy_dd_and_synth.sh", env
51
+ ),
52
+ env=script_env(e),
53
+ )
54
+
55
+
56
+ @app.command()
57
+ def generate(
58
+ studies: str = typer.Argument(
59
+ ...,
60
+ help="Simulated study id(s); comma-separated for many, e.g. AusDiab_Simulated.",
61
+ ),
62
+ env: str = typer.Option(
63
+ "test", "--env", "-e", help="Target environment (prod requires typed confirmation)."
64
+ ),
65
+ num_records: str = typer.Option(
66
+ None,
67
+ "--num-records",
68
+ "-n",
69
+ help="Records per study: one number for all, or a comma list (one per study).",
70
+ ),
71
+ provider: str = typer.Option(
72
+ "random", "--provider", help="Value strategy: 'random' (default, keyless) or 'llm'."
73
+ ),
74
+ llm: bool = typer.Option(
75
+ False,
76
+ "--llm",
77
+ help="Generate LLM-realistic values; reads LLM config from a .env in the "
78
+ "working directory. Default is keyless random data (no API key, no API calls).",
79
+ ),
80
+ seed: int = typer.Option(None, "--seed", help="RNG seed for reproducible output."),
81
+ schema: str = typer.Option(
82
+ None, "--schema", help="Gen3 schema path (default: pulled for the version)."
83
+ ),
84
+ version: str = typer.Option(
85
+ None, "--version", help="Version label for output dir (default: env dictionary_version)."
86
+ ),
87
+ ) -> None:
88
+ """Generate synthetic metadata locally with gen3-metadata-simulator.
89
+
90
+ STUDIES is one simulated study id, or several comma-separated. Defaults to
91
+ keyless, schema-valid random data (no API key, no API calls). Pass --llm to
92
+ generate LLM-realistic values instead.
93
+
94
+ Examples:
95
+ g3dt synth generate AusDiab_Simulated -n 5 --seed 1
96
+ g3dt synth generate AusDiab_Simulated --llm -n 5
97
+ g3dt synth generate "AusDiab_Simulated,Baker-Biobank_Simulated" -n "30,60"
98
+ """
99
+ e = env_of(env)
100
+ safety.confirm_prod_strict("synthetic generation", env)
101
+
102
+ # A comma list of per-study counts must line up with the studies given.
103
+ if num_records and "," in num_records:
104
+ n_counts = len(num_records.split(","))
105
+ n_studies = len(studies.split(","))
106
+ if n_counts != n_studies:
107
+ typer.secho(
108
+ f"--num-records has {n_counts} values but {n_studies} studies "
109
+ f"were given (pass one count, or one per study).",
110
+ fg=typer.colors.RED,
111
+ err=True,
112
+ )
113
+ raise typer.Exit(1)
114
+
115
+ ver = version or e.dictionary_version
116
+ schema_path = schema or str(SCHEMA_DIR / f"acdc_schema_{ver}.json")
117
+
118
+ # Ensure the schema is available locally; pull it if missing.
119
+ if not Path(schema_path).exists():
120
+ typer.secho(f"Schema not found locally; pulling {ver}...", fg=typer.colors.YELLOW)
121
+ runner.run(
122
+ runner.bash_script("services/dictionary/pull_dict.sh", dict_url(e, ver)),
123
+ env=script_env(e),
124
+ )
125
+
126
+ effective_provider = "llm" if llm else provider
127
+ args = [
128
+ "--schema", schema_path,
129
+ "--version", ver,
130
+ "--provider", effective_provider,
131
+ "--studies", studies,
132
+ ]
133
+ if num_records:
134
+ args += ["--num-records", num_records]
135
+ if seed is not None:
136
+ args += ["--seed", str(seed)]
137
+ runner.run(
138
+ runner.bash_script(
139
+ "services/synthetic_data/generate_synth_metadata.sh", *args
140
+ ),
141
+ env=script_env(e),
142
+ )
143
+
144
+
145
+ @app.command()
146
+ def upload(
147
+ env: str = typer.Option(
148
+ "test", "--env", "-e", help="Target environment (prod requires typed confirmation)."
149
+ ),
150
+ version: str = typer.Option(
151
+ None, "--version", help="Dictionary version dir (default: the env's version)."
152
+ ),
153
+ ) -> None:
154
+ """Upload generated synthetic metadata to Gen3 (reads local files)."""
155
+ e = env_of(env)
156
+ safety.confirm_prod_strict("synthetic metadata upload", env)
157
+ v = version or e.dictionary_version
158
+ base_dir = str(SYNTH_DIR / v) + "/"
159
+ args = ["--base-dir", base_dir, "--aws-secret-name", e.aws_secret_name]
160
+ if e.aws_profile:
161
+ args += ["--aws-profile", e.aws_profile]
162
+ runner.run(
163
+ runner.python_script(
164
+ "services/synthetic_data/upload_synth_metadata_sheepdog.py", *args
165
+ ),
166
+ env=script_env(e),
167
+ )
168
+
169
+
170
+ @app.command()
171
+ def delete(
172
+ env: str = typer.Option(
173
+ "test", "--env", "-e", help="Target environment (prod requires typed confirmation)."
174
+ ),
175
+ projects: str = typer.Option(
176
+ None, "--projects", "-p", help="Comma-separated simulated project ids."
177
+ ),
178
+ import_order: str = typer.Option(
179
+ None,
180
+ "--import-order",
181
+ help="DataImportOrder.txt path (default: DataImportOrder.txt in the cwd).",
182
+ ),
183
+ ) -> None:
184
+ """Delete previously-uploaded synthetic metadata from Gen3."""
185
+ e = env_of(env)
186
+ safety.confirm_prod_strict("synthetic metadata deletion", env)
187
+ order = import_order or "DataImportOrder.txt"
188
+ args = ["-i", order, "-s", e.aws_secret_name]
189
+ if e.aws_profile:
190
+ args += ["-profile", e.aws_profile]
191
+ if projects:
192
+ args += ["-p", projects]
193
+ runner.run(
194
+ runner.python_script(
195
+ "services/synthetic_data/delete_synth_metadata_sheepdog.py", *args
196
+ ),
197
+ env=script_env(e),
198
+ )
199
+
200
+
201
+ @app.command(name="install-simulator")
202
+ def install_simulator() -> None:
203
+ """Install the gen3-metadata-simulator generator (the 'synth' extra)."""
204
+ import sys
205
+
206
+ runner.run([sys.executable, "-m", "pip", "install", "gen3-metadata-simulator"])