gen3-dataops-toolkit 3.7.4__tar.gz → 3.8.1__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 (60) hide show
  1. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/PKG-INFO +37 -27
  2. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/README.md +36 -26
  3. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/pyproject.toml +1 -1
  4. gen3_dataops_toolkit-3.8.1/src/g3dt/cli/_internal/resolve.py +85 -0
  5. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/_internal/runner.py +14 -2
  6. gen3_dataops_toolkit-3.8.1/src/g3dt/cli/_internal/safety.py +130 -0
  7. gen3_dataops_toolkit-3.8.1/src/g3dt/cli/config_cmds.py +722 -0
  8. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/delete_cmds.py +3 -2
  9. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/dict_cmds.py +7 -4
  10. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/ec2_cmds.py +7 -3
  11. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/indexd_cmds.py +5 -3
  12. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/jobs.py +5 -1
  13. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/k8s.py +7 -4
  14. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/main.py +39 -6
  15. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/metadata.py +5 -3
  16. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/pipeline_cmds.py +5 -2
  17. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/release_cmds.py +3 -1
  18. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/synth.py +66 -5
  19. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/config.py +178 -16
  20. gen3_dataops_toolkit-3.8.1/src/g3dt/contexts.py +313 -0
  21. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/upload/metadata/upload_all_studies.sh +5 -1
  22. gen3_dataops_toolkit-3.7.4/src/g3dt/cli/_internal/resolve.py +0 -22
  23. gen3_dataops_toolkit-3.7.4/src/g3dt/cli/_internal/safety.py +0 -98
  24. gen3_dataops_toolkit-3.7.4/src/g3dt/cli/config_cmds.py +0 -325
  25. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/__init__.py +0 -0
  26. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/__init__.py +0 -0
  27. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/_internal/__init__.py +0 -0
  28. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/_internal/dispatch.py +0 -0
  29. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/cli/_internal/registry.py +0 -0
  30. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/indexd/__init__.py +0 -0
  31. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/indexd/file_access.py +0 -0
  32. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/indexd/indexd_registrar.py +0 -0
  33. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/ingest/ingest.py +0 -0
  34. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/resolver.py +0 -0
  35. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/delete/delete_all_metadata_for_project.py +0 -0
  36. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/delete/delete_metadata.sh +0 -0
  37. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/delete/delete_metadata_by_guid.py +0 -0
  38. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/dictionary/deploy_dd.sh +0 -0
  39. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/dictionary/pull_dict.sh +0 -0
  40. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/dictionary/upload_dictionary.py +0 -0
  41. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/indexd/register_indexd.py +0 -0
  42. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/indexd/verify_file_access.py +0 -0
  43. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/k8s_ops/argocd_restart_etl.sh +0 -0
  44. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/k8s_ops/argocd_restart_ms.sh +0 -0
  45. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/k8s_ops/argocd_restart_schema.sh +0 -0
  46. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/k8s_ops/login_to_pod.sh +0 -0
  47. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/k8s_ops/restart_etl_and_ms.sh +0 -0
  48. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +0 -0
  49. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +0 -0
  50. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/synthetic_data/generate_synth_metadata.sh +0 -0
  51. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +0 -0
  52. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/services/upload/metadata/upload_metadata.py +0 -0
  53. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/upload/__init__.py +0 -0
  54. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/upload/metadata_deleter.py +0 -0
  55. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/upload/metadata_submitter.py +0 -0
  56. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/upload/upload_synthdata_s3.py +0 -0
  57. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/utils/athena_utils.py +0 -0
  58. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/utils/dbt_utils.py +0 -0
  59. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/utils/release_writer.py +0 -0
  60. {gen3_dataops_toolkit-3.7.4 → gen3_dataops_toolkit-3.8.1}/src/g3dt/validate/validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gen3-dataops-toolkit
3
- Version: 3.7.4
3
+ Version: 3.8.1
4
4
  Summary: Gen3 DataOps toolkit (g3dt): operate SSM-published Gen3 data pipeline environments
5
5
  License: Apache-2.0
6
6
  Author: JoshuaHarris391
@@ -46,8 +46,9 @@ restarts. The dbt half of the platform lives in
46
46
  [gen3-dbt-template](https://github.com/AustralianBioCommons/gen3-dbt-template).
47
47
 
48
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.
49
+ operates any project: it is targeted by a **context** — a named
50
+ (project, env, profile, region) tuple — and the project's SSM tree
51
+ (`/{project}/{env}/...`).
51
52
 
52
53
  ## Install
53
54
 
@@ -55,41 +56,50 @@ operates any project: it is targeted purely by `--env`, the project's SSM tree
55
56
  pip install gen3-dataops-toolkit
56
57
  ```
57
58
 
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/
59
+ ## Bootstrap: discover, then use
60
+
61
+ ```bash
62
+ g3dt config discover <aws-profile> --add # logs the profile in if needed,
63
+ # lists every deployed env its
64
+ # account holds, registers them
65
+ g3dt config contexts # list them (current marked *)
66
+ g3dt config use myproj/test # point g3dt at one
67
+ ```
68
+
69
+ That writes `~/.g3dt/g3dt.yaml` for you. **Every command prints the active
70
+ context first** (on stderr) — read that line before anything else:
71
+
72
+ ```
73
+ ctx myproj/test → project=myproj env=test profile=myproj_test region=ap-southeast-2
75
74
  ```
76
75
 
76
+ Production contexts are marked `[PROD]`, switching to one asks for
77
+ confirmation, and destructive actions on them require typing the context
78
+ name — `--yes` never bypasses that.
79
+
80
+ The marker can also be written by hand (design doc:
81
+ `docs/design/contexts.md`); the study registry lives either in a top-level
82
+ `studies:` block or per env at `s3://<metadata-bucket>/config/studies.yaml`.
77
83
  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`.
84
+ (the EC2 job box's copy, written by CDK user-data). Legacy markers
85
+ (`project`/`default_env`/`profiles:` keys) keep working unchanged, as do the
86
+ env-var overrides `G3DT_PROJECT`, `AWS_REGION`, `G3DT_DEFAULT_ENV` — that is
87
+ the file-less CodeBuild/EC2 path.
80
88
 
81
89
  ## Quick start
82
90
 
83
91
  ```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
92
+ g3dt config show # every resolved name — the safety check
93
+ g3dt ec2 up # start the context's job box (SSM-managed)
94
+ g3dt metadata upload --study mystudy --on ec2
88
95
  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
96
+ g3dt ec2 down # or let the auto-stop alarm handle it
90
97
  g3dt docs # the full operations overview
91
98
  ```
92
99
 
100
+ (`--env <name>` still works everywhere and selects the matching context;
101
+ CodeBuild's `g3dt config dbt-env --env $ENV` contract is unchanged.)
102
+
93
103
  ## How configuration works
94
104
 
95
105
  There are exactly two kinds of configuration:
@@ -12,8 +12,9 @@ restarts. The dbt half of the platform lives in
12
12
  [gen3-dbt-template](https://github.com/AustralianBioCommons/gen3-dbt-template).
13
13
 
14
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.
15
+ operates any project: it is targeted by a **context** — a named
16
+ (project, env, profile, region) tuple — and the project's SSM tree
17
+ (`/{project}/{env}/...`).
17
18
 
18
19
  ## Install
19
20
 
@@ -21,41 +22,50 @@ operates any project: it is targeted purely by `--env`, the project's SSM tree
21
22
  pip install gen3-dataops-toolkit
22
23
  ```
23
24
 
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/
25
+ ## Bootstrap: discover, then use
26
+
27
+ ```bash
28
+ g3dt config discover <aws-profile> --add # logs the profile in if needed,
29
+ # lists every deployed env its
30
+ # account holds, registers them
31
+ g3dt config contexts # list them (current marked *)
32
+ g3dt config use myproj/test # point g3dt at one
33
+ ```
34
+
35
+ That writes `~/.g3dt/g3dt.yaml` for you. **Every command prints the active
36
+ context first** (on stderr) — read that line before anything else:
37
+
38
+ ```
39
+ ctx myproj/test → project=myproj env=test profile=myproj_test region=ap-southeast-2
41
40
  ```
42
41
 
42
+ Production contexts are marked `[PROD]`, switching to one asks for
43
+ confirmation, and destructive actions on them require typing the context
44
+ name — `--yes` never bypasses that.
45
+
46
+ The marker can also be written by hand (design doc:
47
+ `docs/design/contexts.md`); the study registry lives either in a top-level
48
+ `studies:` block or per env at `s3://<metadata-bucket>/config/studies.yaml`.
43
49
  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`.
50
+ (the EC2 job box's copy, written by CDK user-data). Legacy markers
51
+ (`project`/`default_env`/`profiles:` keys) keep working unchanged, as do the
52
+ env-var overrides `G3DT_PROJECT`, `AWS_REGION`, `G3DT_DEFAULT_ENV` — that is
53
+ the file-less CodeBuild/EC2 path.
46
54
 
47
55
  ## Quick start
48
56
 
49
57
  ```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
58
+ g3dt config show # every resolved name — the safety check
59
+ g3dt ec2 up # start the context's job box (SSM-managed)
60
+ g3dt metadata upload --study mystudy --on ec2
54
61
  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
62
+ g3dt ec2 down # or let the auto-stop alarm handle it
56
63
  g3dt docs # the full operations overview
57
64
  ```
58
65
 
66
+ (`--env <name>` still works everywhere and selects the matching context;
67
+ CodeBuild's `g3dt config dbt-env --env $ENV` contract is unchanged.)
68
+
59
69
  ## How configuration works
60
70
 
61
71
  There are exactly two kinds of configuration:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "gen3-dataops-toolkit"
3
- version = "3.7.4"
3
+ version = "3.8.1"
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"
@@ -0,0 +1,85 @@
1
+ """Thin wrappers that turn config resolution errors into clean CLI exits.
2
+
3
+ 3.8.0 additions: :func:`active_env` / :func:`announce_context` implement the
4
+ universal context banner (design doc docs/design/contexts.md section 6), and
5
+ :func:`rc_of` is the single resolved-config idiom that replaces the hand-rolled
6
+ ``load_marker``/``require_project``/``env_base``/``aws_profile_for`` block that
7
+ used to be copy-pasted across command modules (one copy of which forgot the
8
+ ``_ec2`` → ambient rule — the class of bug this unification removes).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Optional
13
+
14
+ import typer
15
+
16
+ from g3dt import config, contexts
17
+
18
+
19
+ def env_of(env: str) -> config.EnvConfig:
20
+ try:
21
+ return config.resolve_env(env)
22
+ except config.ConfigError as exc:
23
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
24
+ raise typer.Exit(1)
25
+
26
+
27
+ def study_of(study: str, env: str) -> config.StudyConfig:
28
+ try:
29
+ return config.resolve_study(study, env)
30
+ except config.ConfigError as exc:
31
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
32
+ raise typer.Exit(1)
33
+
34
+
35
+ def active_env(env: Optional[str]) -> str:
36
+ """Resolve the acting context, print the banner, return the effective env.
37
+
38
+ Every command that takes ``--env`` calls this first. The returned string
39
+ keeps a caller-supplied ``_ec2`` suffix intact (dispatch/auth mechanics
40
+ downstream depend on it, and the remote wire form never changes).
41
+ """
42
+ try:
43
+ ctx, effective = contexts.resolve_context(
44
+ ctx_name=contexts.override(), env=env
45
+ )
46
+ except config.ConfigError as exc:
47
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
48
+ raise typer.Exit(1)
49
+ contexts.set_active(ctx)
50
+ contexts.print_banner(ctx, effective)
51
+ return effective
52
+
53
+
54
+ def announce_context() -> None:
55
+ """Banner-only entry point for commands that take no ``--env``.
56
+
57
+ Never fails: with nothing configured it prints the "(none configured)"
58
+ banner and returns — `jobs list`, `version` etc. must keep working on a
59
+ bare machine.
60
+ """
61
+ try:
62
+ ctx, effective = contexts.resolve_context(required=False)
63
+ except config.ConfigError:
64
+ ctx, effective = None, None
65
+ contexts.set_active(ctx)
66
+ contexts.print_banner(ctx, effective)
67
+
68
+
69
+ def rc_of(env: str):
70
+ """Resolved SSM config for ``env`` with the correct credential rule.
71
+
72
+ ``_ec2``-suffixed envs always use the ambient chain (never a laptop
73
+ profile) — the rule three call sites applied and two forgot.
74
+ """
75
+ from g3dt import resolver
76
+
77
+ marker = config.load_marker()
78
+ project = config.require_project(marker)
79
+ base = config.env_base(env)
80
+ profile = None if env.endswith("_ec2") else config.aws_profile_for(env, marker)
81
+ try:
82
+ return resolver.resolve(project, base, profile=profile)
83
+ except config.ConfigError as exc:
84
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
85
+ raise typer.Exit(1)
@@ -38,8 +38,20 @@ def python_script(relpath: str, *args: str) -> List[str]:
38
38
 
39
39
 
40
40
  def bash_script(relpath: str, *args: str) -> List[str]:
41
- """Build an argv that runs a packaged shell script via bash."""
42
- return ["bash", str(package_path(relpath)), *[str(a) for a in args]]
41
+ """Build an argv that runs a packaged shell script via bash.
42
+
43
+ The CLI's own interpreter is passed through as ``G3DT_PYTHON`` (via an
44
+ ``env`` prefix, so no process-global mutation) because wrapped scripts
45
+ that shell back into packaged Python services must use the interpreter
46
+ that owns this g3dt installation. A bare ``python3`` is NOT that on
47
+ Amazon Linux 2023, where the system default is 3.9 but g3dt is installed
48
+ under 3.11 — observed live on acdc/staging as ``ModuleNotFoundError:
49
+ No module named 'g3dt'`` from every study in an upload-all run.
50
+ """
51
+ return [
52
+ "env", f"G3DT_PYTHON={sys.executable}",
53
+ "bash", str(package_path(relpath)), *[str(a) for a in args],
54
+ ]
43
55
 
44
56
 
45
57
  def run(
@@ -0,0 +1,130 @@
1
+ """Confirmation and environment guards for destructive / production operations.
2
+
3
+ These mirror (and strengthen) the guards already baked into the shell scripts:
4
+ the test-only ``synth deploy`` guard, the prod aborts in the bulk scripts, and
5
+ the optional delete confirmation prompts.
6
+
7
+ 3.8.0: the guards are context-aware. Production is any env/study key whose
8
+ name contains ``prod`` (unchanged), OR an active context classified production
9
+ (``production: true`` flag, or 'prod' in its name — see
10
+ ``contexts.is_production``). When a *named* context from the marker is active,
11
+ the typed confirmation token is the **context name** (e.g. ``acdc/prod``);
12
+ legacy and synthetic contexts keep the historical env-name/target tokens, so
13
+ older muscle memory and the pinned safety tests are unaffected. ``--yes``
14
+ never bypasses a production prompt, and confirmation always happens locally,
15
+ before any EC2 dispatch.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import typer
22
+
23
+ from g3dt.config import env_base
24
+
25
+
26
+ def is_prod(env: str) -> bool:
27
+ """True if the environment name refers to production."""
28
+ return "prod" in env.lower()
29
+
30
+
31
+ def _active_prod_context():
32
+ """The active context when it is production-classified, else ``None``."""
33
+ from g3dt import contexts
34
+
35
+ ctx = contexts.active()
36
+ if ctx is not None and contexts.is_production(ctx):
37
+ return ctx
38
+ return None
39
+
40
+
41
+ def _named(ctx) -> bool:
42
+ """True for a context the operator configured by name (marker source)."""
43
+ return ctx is not None and ctx.source == "marker"
44
+
45
+
46
+ def _typed_gate(header: str, token: str) -> None:
47
+ typer.secho(header, fg=typer.colors.RED, bold=True)
48
+ # default="" so an empty entry (just pressing Enter) returns immediately
49
+ # and aborts, instead of click re-prompting forever.
50
+ typed = typer.prompt(
51
+ f"Type '{token}' to confirm", default="", show_default=False
52
+ )
53
+ if typed.strip() != token:
54
+ typer.secho(
55
+ "Confirmation did not match. Aborting.",
56
+ fg=typer.colors.RED,
57
+ err=True,
58
+ )
59
+ raise typer.Exit(1)
60
+
61
+
62
+ def require_test_env(env: str) -> None:
63
+ """Abort unless ``env`` is the test environment (``test`` or ``test_ec2``).
64
+
65
+ A hard guard for any command that must never run outside test. (The ``synth``
66
+ commands no longer use this — they allow any env and gate prod with
67
+ :func:`confirm_prod_strict` instead.)
68
+ """
69
+ if env_base(env) != "test":
70
+ typer.secho(
71
+ f"Refusing: this command is only allowed for the 'test' "
72
+ f"environment (got '{env}').",
73
+ fg=typer.colors.RED,
74
+ err=True,
75
+ )
76
+ raise typer.Exit(2)
77
+
78
+
79
+ def confirm_destructive(action: str, target: str, env: str, assume_yes: bool) -> None:
80
+ """Gate a destructive operation with an appropriate confirmation.
81
+
82
+ * Production (env name, study key, or the active context's
83
+ classification): ALWAYS require a typed confirmation, even with
84
+ ``--yes`` (so automation can never silently delete prod data). The
85
+ token is the active context's name when one is configured, else the
86
+ ``target``.
87
+ * Non-production: a simple y/N prompt, skippable with ``--yes``.
88
+
89
+ Confirmation always happens locally, before any EC2 dispatch (SSM has no
90
+ TTY), after which the remote job is invoked with ``--yes``.
91
+ """
92
+ ctx = _active_prod_context()
93
+ if is_prod(env) or ctx is not None:
94
+ token = ctx.name if _named(ctx) else target
95
+ _typed_gate(
96
+ f"PRODUCTION {action} targeting '{target}' (env={env}"
97
+ + (f", ctx={ctx.name}" if ctx is not None else "")
98
+ + ").",
99
+ token,
100
+ )
101
+ return
102
+
103
+ if assume_yes:
104
+ return
105
+ if not typer.confirm(f"{action} targeting '{target}' (env={env}). Proceed?"):
106
+ typer.secho("Aborted.", fg=typer.colors.YELLOW)
107
+ raise typer.Exit(1)
108
+
109
+
110
+ def confirm_prod_strict(action: str, env: str) -> None:
111
+ """Warn and require a typed confirmation before any action on production.
112
+
113
+ Production is any env whose name contains ``prod`` (see :func:`is_prod`)
114
+ or an active production-classified context. Non-production returns
115
+ immediately (no prompt). The confirmation cannot be bypassed, so
116
+ automation can never silently act on prod. The token is the active
117
+ context's name when one is configured, else the env name.
118
+
119
+ Used by the ``synth`` commands, which may target any configured environment.
120
+ """
121
+ ctx = _active_prod_context()
122
+ if not is_prod(env) and ctx is None:
123
+ return
124
+ token = ctx.name if _named(ctx) else env
125
+ _typed_gate(
126
+ f"PRODUCTION {action} targeting env '{env}'"
127
+ + (f" (ctx={ctx.name})" if ctx is not None else "")
128
+ + ".",
129
+ token,
130
+ )