gen3-dataops-toolkit 4.1.0__tar.gz → 4.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.
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/PKG-INFO +15 -1
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/README.md +14 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/pyproject.toml +2 -2
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/delete_cmds.py +133 -11
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/synth.py +9 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/config.py +74 -1
- gen3_dataops_toolkit-4.3.0/src/g3dt/import_order.py +265 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/delete/delete_all_metadata_for_project.py +65 -23
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/delete/delete_metadata.sh +46 -9
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/delete/delete_metadata_by_guid.py +40 -19
- gen3_dataops_toolkit-4.3.0/src/g3dt/services/delete/delete_synth_metadata_by_version.py +355 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/dictionary/upload_dictionary.py +13 -8
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/synthetic_data/generate_synth_metadata.sh +8 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/upload/metadata_deleter.py +78 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/utils/athena_utils.py +0 -9
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/aws_quiet.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/dispatch.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/helptext.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/registry.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/resolve.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/runner.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/_internal/safety.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/config_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/dict_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/ec2_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/indexd_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/jobs.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/k8s.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/main.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/metadata.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/pipeline_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/release_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/cli/study_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/contexts.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/indexd/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/indexd/file_access.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/indexd/indexd_registrar.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/ingest/ingest.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/resolver.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/dictionary/deploy_dd.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/dictionary/pull_dict.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/indexd/register_indexd.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/indexd/verify_file_access.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/k8s_ops/argocd_restart_etl.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/k8s_ops/argocd_restart_ms.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/k8s_ops/argocd_restart_schema.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/k8s_ops/login_to_pod.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/k8s_ops/restart_etl_and_ms.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/upload/metadata/upload_all_studies.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/services/upload/metadata/upload_metadata.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/studies.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/upload/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/upload/metadata_submitter.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/upload/upload_synthdata_s3.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/utils/dbt_utils.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/src/g3dt/utils/release_writer.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.3.0}/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: 4.
|
|
3
|
+
Version: 4.3.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
|
|
@@ -194,6 +194,20 @@ version until the CDK config catches up —
|
|
|
194
194
|
`g3dt config diff --env <env> --file <wrapper>/config/<project>.<env>.json`
|
|
195
195
|
reports exactly that gap and exits 1, so it can gate CI.
|
|
196
196
|
|
|
197
|
+
### Deleting metadata: where the node order comes from
|
|
198
|
+
|
|
199
|
+
`delete metadata` walks nodes children-before-parents. The order is resolved
|
|
200
|
+
from the first available of: an explicit `--import-order <path|s3://…>`
|
|
201
|
+
(failures are fatal — an explicit source is never silently skipped), the
|
|
202
|
+
registered study's release bucket (`DataImportOrder.txt` next to the release's
|
|
203
|
+
node JSONs), a `DataImportOrder.txt` in the current directory, or a
|
|
204
|
+
topological sort derived from the dictionary itself — the `--dict-version`
|
|
205
|
+
tag's bundle when given (downloaded to `~/.g3dt/schemas` if needed), else the
|
|
206
|
+
env's deployed dictionary. Deriving matters when deleting data submitted
|
|
207
|
+
under an older dictionary whose node layout differs from today's: pass
|
|
208
|
+
`--dict-version <old-tag>`. With `--on ec2`, `--import-order` must be an
|
|
209
|
+
`s3://` URI (a laptop path does not exist on the box).
|
|
210
|
+
|
|
197
211
|
Synthetic data is only schema-valid against the dictionary that generated it, so
|
|
198
212
|
`synth generate` records the dictionary version in each batch and `synth upload`
|
|
199
213
|
refuses a batch that doesn't match the version being uploaded (override with
|
|
@@ -160,6 +160,20 @@ version until the CDK config catches up —
|
|
|
160
160
|
`g3dt config diff --env <env> --file <wrapper>/config/<project>.<env>.json`
|
|
161
161
|
reports exactly that gap and exits 1, so it can gate CI.
|
|
162
162
|
|
|
163
|
+
### Deleting metadata: where the node order comes from
|
|
164
|
+
|
|
165
|
+
`delete metadata` walks nodes children-before-parents. The order is resolved
|
|
166
|
+
from the first available of: an explicit `--import-order <path|s3://…>`
|
|
167
|
+
(failures are fatal — an explicit source is never silently skipped), the
|
|
168
|
+
registered study's release bucket (`DataImportOrder.txt` next to the release's
|
|
169
|
+
node JSONs), a `DataImportOrder.txt` in the current directory, or a
|
|
170
|
+
topological sort derived from the dictionary itself — the `--dict-version`
|
|
171
|
+
tag's bundle when given (downloaded to `~/.g3dt/schemas` if needed), else the
|
|
172
|
+
env's deployed dictionary. Deriving matters when deleting data submitted
|
|
173
|
+
under an older dictionary whose node layout differs from today's: pass
|
|
174
|
+
`--dict-version <old-tag>`. With `--on ec2`, `--import-order` must be an
|
|
175
|
+
`s3://` URI (a laptop path does not exist on the box).
|
|
176
|
+
|
|
163
177
|
Synthetic data is only schema-valid against the dictionary that generated it, so
|
|
164
178
|
`synth generate` records the dictionary version in each batch and `synth upload`
|
|
165
179
|
refuses a batch that doesn't match the version being uploaded (override with
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "gen3-dataops-toolkit"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.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"
|
|
@@ -44,7 +44,7 @@ moto = ">=5.0"
|
|
|
44
44
|
optional = true
|
|
45
45
|
|
|
46
46
|
[tool.poetry.group.synth.dependencies]
|
|
47
|
-
gen3-metadata-simulator = "^0.3
|
|
47
|
+
gen3-metadata-simulator = "^0.5.3"
|
|
48
48
|
|
|
49
49
|
[build-system]
|
|
50
50
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
@@ -7,6 +7,19 @@ the bare names (a specific version like ``0.9.8``, resolved via an Athena GUID
|
|
|
7
7
|
lookup, or ``all`` for every version). A bare study with no version anywhere
|
|
8
8
|
is refused (exit 2).
|
|
9
9
|
|
|
10
|
+
``--synthetic`` switches to registry-free mode for synthetic data: each
|
|
11
|
+
``--studies`` name is the Gen3 project code itself (no SSM study registry —
|
|
12
|
+
synthetic projects are never registered), bare names default to version
|
|
13
|
+
``all``, and a specific version is matched verbatim against the records'
|
|
14
|
+
``data_version`` property via GraphQL rather than Athena receipts (synthetic
|
|
15
|
+
uploads write none).
|
|
16
|
+
|
|
17
|
+
The node deletion order comes from, in order: an explicit ``--import-order``
|
|
18
|
+
(path or s3:// URI; failures are fatal, never silently skipped), the
|
|
19
|
+
registered study's release bucket, a ``DataImportOrder.txt`` in the current
|
|
20
|
+
directory, or a topological sort derived from the dictionary itself — the
|
|
21
|
+
``--dict-version`` bundle when given, else the env's deployed dictionary.
|
|
22
|
+
|
|
10
23
|
Every command confirms before acting. Production always requires typing the
|
|
11
24
|
target id, even with ``--yes``. Deleting ALL versions always prompts, even with
|
|
12
25
|
``--yes``. Confirmation happens locally before any EC2 dispatch (SSM has no
|
|
@@ -61,12 +74,30 @@ def _normalise_version(raw: str, where: str) -> str:
|
|
|
61
74
|
return match.group(1)
|
|
62
75
|
|
|
63
76
|
|
|
64
|
-
def
|
|
77
|
+
def _synthetic_version(raw: str) -> str:
|
|
78
|
+
"""Canonicalise a synthetic version token: ``all`` (any case) or verbatim.
|
|
79
|
+
|
|
80
|
+
Synthetic versions are matched exactly against the records' ``data_version``
|
|
81
|
+
property, and the natural label there is the dictionary version WITH its
|
|
82
|
+
leading ``v`` (batch dirs are ``~/.g3dt/synth_metadata/v1.3.0/...``) — so
|
|
83
|
+
unlike ``_normalise_version`` nothing is stripped. A version that matches
|
|
84
|
+
no records is reported by the worker (skip + hint), not silently absorbed.
|
|
85
|
+
"""
|
|
86
|
+
token = raw.strip()
|
|
87
|
+
return "all" if token.lower() == "all" else token
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_study_specs(studies: str, fallback, env: str, synthetic: bool = False):
|
|
65
91
|
"""Turn ``--studies`` into ``[(resolved_study_key, version), ...]``.
|
|
66
92
|
|
|
67
93
|
Each comma-separated entry is ``name`` or ``name:version``. A bare name
|
|
68
94
|
takes *fallback* (the ``--version`` default); *fallback* is ``None`` when
|
|
69
|
-
``--version`` was not given, which makes a bare name a usage error
|
|
95
|
+
``--version`` was not given, which makes a bare name a usage error —
|
|
96
|
+
except with *synthetic*, where a bare name defaults to ``all`` (the whole
|
|
97
|
+
point of the flag is "wipe the synthetic project").
|
|
98
|
+
|
|
99
|
+
With *synthetic* the raw name IS the Gen3 project code: no study-registry
|
|
100
|
+
lookup, and version tokens pass through :func:`_synthetic_version`.
|
|
70
101
|
|
|
71
102
|
Every entry is validated before anything is dispatched, so a typo in the
|
|
72
103
|
last study cannot leave the earlier ones already deleted.
|
|
@@ -102,9 +133,15 @@ def _parse_study_specs(studies: str, fallback, env: str):
|
|
|
102
133
|
raise typer.Exit(2)
|
|
103
134
|
|
|
104
135
|
if sep:
|
|
105
|
-
version =
|
|
136
|
+
version = (
|
|
137
|
+
_synthetic_version(raw_version)
|
|
138
|
+
if synthetic
|
|
139
|
+
else _normalise_version(raw_version, f"for study '{name}'")
|
|
140
|
+
)
|
|
106
141
|
elif fallback is not None:
|
|
107
142
|
version = fallback
|
|
143
|
+
elif synthetic:
|
|
144
|
+
version = "all"
|
|
108
145
|
else:
|
|
109
146
|
typer.secho(
|
|
110
147
|
f"No version for study '{name}': add ':<version>' to it "
|
|
@@ -115,7 +152,7 @@ def _parse_study_specs(studies: str, fallback, env: str):
|
|
|
115
152
|
)
|
|
116
153
|
raise typer.Exit(2)
|
|
117
154
|
|
|
118
|
-
specs.append((study_of(name, env).key, version))
|
|
155
|
+
specs.append((name if synthetic else study_of(name, env).key, version))
|
|
119
156
|
|
|
120
157
|
if not specs:
|
|
121
158
|
typer.secho("--studies is empty.", fg=typer.colors.RED, err=True)
|
|
@@ -141,6 +178,35 @@ def metadata(
|
|
|
141
178
|
"':version', e.g. 0.9.8, or 'all' for every version.",
|
|
142
179
|
),
|
|
143
180
|
node: Optional[str] = typer.Option(None, "--node", help="Delete only this node type."),
|
|
181
|
+
synthetic: bool = typer.Option(
|
|
182
|
+
False,
|
|
183
|
+
"--synthetic",
|
|
184
|
+
help="Registry-free synthetic-data mode: each --studies name is the "
|
|
185
|
+
"Gen3 project id itself (no SSM study registry). Bare names "
|
|
186
|
+
"default to version 'all'; a specific version matches records' "
|
|
187
|
+
"data_version property verbatim.",
|
|
188
|
+
),
|
|
189
|
+
program_id: Optional[str] = typer.Option(
|
|
190
|
+
None,
|
|
191
|
+
"--program-id",
|
|
192
|
+
help="Gen3 program for --synthetic (default: program1). "
|
|
193
|
+
"Invalid without --synthetic.",
|
|
194
|
+
),
|
|
195
|
+
import_order: Optional[str] = typer.Option(
|
|
196
|
+
None,
|
|
197
|
+
"--import-order",
|
|
198
|
+
help="Path or s3:// URI of DataImportOrder.txt. Default: auto — the "
|
|
199
|
+
"study's release bucket (registered studies), then "
|
|
200
|
+
"./DataImportOrder.txt, then derived from the dictionary. "
|
|
201
|
+
"With --on ec2 only s3:// URIs are accepted.",
|
|
202
|
+
),
|
|
203
|
+
dict_version: Optional[str] = typer.Option(
|
|
204
|
+
None,
|
|
205
|
+
"--dict-version",
|
|
206
|
+
help="Dictionary git tag to derive the node order from (verbatim, "
|
|
207
|
+
"e.g. v1.3.0). Default: the env's deployed dictionary. Only "
|
|
208
|
+
"used when the order is derived.",
|
|
209
|
+
),
|
|
144
210
|
yes: bool = typer.Option(
|
|
145
211
|
False, "--yes", "-y", help="Skip the non-prod prompt (specific-version only)."
|
|
146
212
|
),
|
|
@@ -156,12 +222,44 @@ def metadata(
|
|
|
156
222
|
|
|
157
223
|
g3dt delete metadata --studies "ausdiab:0.7.5,cdah:0.8.1" --env staging
|
|
158
224
|
g3dt delete metadata --studies "ausdiab:all,cdah" --version 0.9.8 --env staging
|
|
225
|
+
g3dt delete metadata --studies "synthetic_dataset_1,synthetic_dataset_2" --env test --synthetic
|
|
159
226
|
"""
|
|
160
227
|
env = resolve.active_env(env)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
if program_id is not None and not synthetic:
|
|
229
|
+
typer.secho(
|
|
230
|
+
"--program-id is only valid with --synthetic (registered studies "
|
|
231
|
+
"carry their program in the study registry).",
|
|
232
|
+
fg=typer.colors.RED,
|
|
233
|
+
err=True,
|
|
234
|
+
)
|
|
235
|
+
raise typer.Exit(2)
|
|
236
|
+
if import_order and dict_version:
|
|
237
|
+
typer.secho(
|
|
238
|
+
"--import-order names the exact file; --dict-version derives one "
|
|
239
|
+
"— pass only one.",
|
|
240
|
+
fg=typer.colors.RED,
|
|
241
|
+
err=True,
|
|
242
|
+
)
|
|
243
|
+
raise typer.Exit(2)
|
|
244
|
+
# A laptop path forwarded to the EC2 box would resolve against the box's
|
|
245
|
+
# filesystem — at best a crash after confirmation, at worst a same-named
|
|
246
|
+
# DIFFERENT file ordering the delete. s3:// URIs (and --dict-version)
|
|
247
|
+
# resolve identically anywhere, so only those may travel.
|
|
248
|
+
if on == Target.ec2 and import_order and not import_order.startswith("s3://"):
|
|
249
|
+
typer.secho(
|
|
250
|
+
"--import-order with --on ec2 must be an s3:// URI (a local path "
|
|
251
|
+
"does not exist on the box). Upload the file, or run locally.",
|
|
252
|
+
fg=typer.colors.RED,
|
|
253
|
+
err=True,
|
|
254
|
+
)
|
|
255
|
+
raise typer.Exit(2)
|
|
256
|
+
if version is None:
|
|
257
|
+
fallback = None
|
|
258
|
+
elif synthetic:
|
|
259
|
+
fallback = _synthetic_version(version)
|
|
260
|
+
else:
|
|
261
|
+
fallback = _normalise_version(version, "for --version")
|
|
262
|
+
specs = _parse_study_specs(studies, fallback, env, synthetic=synthetic)
|
|
165
263
|
versions = [v for _, v in specs]
|
|
166
264
|
|
|
167
265
|
# The typed production confirmation stays the study keys alone: short
|
|
@@ -171,13 +269,17 @@ def metadata(
|
|
|
171
269
|
uniform = len(set(versions)) == 1
|
|
172
270
|
any_all = "all" in versions
|
|
173
271
|
|
|
272
|
+
prefix = "synthetic " if synthetic else ""
|
|
174
273
|
if uniform and versions[0] == "all":
|
|
175
|
-
action = "deletion of ALL VERSIONS"
|
|
274
|
+
action = f"{prefix}deletion of ALL VERSIONS"
|
|
176
275
|
elif uniform:
|
|
177
|
-
|
|
276
|
+
# Synthetic versions are verbatim data_version values (often already
|
|
277
|
+
# v-prefixed); Athena versions are canonical x.y.z, displayed with v.
|
|
278
|
+
shown = versions[0] if synthetic else f"v{versions[0]}"
|
|
279
|
+
action = f"{prefix}deletion of {shown}"
|
|
178
280
|
else:
|
|
179
281
|
plan = ", ".join(f"{key}:{v}" for key, v in specs)
|
|
180
|
-
action = f"deletion of per-study versions [{plan}]"
|
|
282
|
+
action = f"{prefix}deletion of per-study versions [{plan}]"
|
|
181
283
|
|
|
182
284
|
# Deleting every version is the most destructive path: always prompt (pass
|
|
183
285
|
# assume_yes=False so --yes can't bypass it; prod still types the target).
|
|
@@ -200,6 +302,15 @@ def metadata(
|
|
|
200
302
|
]
|
|
201
303
|
if node:
|
|
202
304
|
a += ["--node", node]
|
|
305
|
+
if synthetic:
|
|
306
|
+
# Program is always passed explicitly: the shell default makes it
|
|
307
|
+
# optional on the wire, but an explicit value keeps the contract
|
|
308
|
+
# visible in logs and SSM command history.
|
|
309
|
+
a += ["--synthetic", "--program-id", program_id or "program1"]
|
|
310
|
+
if import_order:
|
|
311
|
+
a += ["--import-order", import_order]
|
|
312
|
+
if dict_version:
|
|
313
|
+
a += ["--dict-version", dict_version]
|
|
203
314
|
return a
|
|
204
315
|
|
|
205
316
|
def remote_cli(env_name):
|
|
@@ -212,6 +323,17 @@ def metadata(
|
|
|
212
323
|
a.append("--yes")
|
|
213
324
|
if node:
|
|
214
325
|
a += ["--node", node]
|
|
326
|
+
if synthetic:
|
|
327
|
+
# Without this the remote re-entry would re-parse --studies
|
|
328
|
+
# against the study registry on the box and exit 2.
|
|
329
|
+
a.append("--synthetic")
|
|
330
|
+
if program_id is not None:
|
|
331
|
+
a += ["--program-id", program_id]
|
|
332
|
+
if import_order:
|
|
333
|
+
# Guaranteed s3:// by the pre-dispatch gate above.
|
|
334
|
+
a += ["--import-order", import_order]
|
|
335
|
+
if dict_version:
|
|
336
|
+
a += ["--dict-version", dict_version]
|
|
215
337
|
return a
|
|
216
338
|
|
|
217
339
|
dispatch.run_or_dispatch(
|
|
@@ -370,6 +370,13 @@ def generate(
|
|
|
370
370
|
None, "--version", "-v",
|
|
371
371
|
help="Version label for output dir (default: env dictionary_version).",
|
|
372
372
|
),
|
|
373
|
+
data_version: Optional[str] = typer.Option(
|
|
374
|
+
None, "--data-version",
|
|
375
|
+
help="Stamp every generated record's data_version property with this "
|
|
376
|
+
"value (requires the dictionary to declare data_version). Makes "
|
|
377
|
+
"the batch deletable later with "
|
|
378
|
+
"'delete metadata --synthetic --version <value>'.",
|
|
379
|
+
),
|
|
373
380
|
) -> None:
|
|
374
381
|
"""Generate synthetic metadata locally with gen3-metadata-simulator.
|
|
375
382
|
|
|
@@ -431,6 +438,8 @@ def generate(
|
|
|
431
438
|
args += ["--num-records", num_records]
|
|
432
439
|
if seed is not None:
|
|
433
440
|
args += ["--seed", str(seed)]
|
|
441
|
+
if data_version:
|
|
442
|
+
args += ["--data-version", data_version]
|
|
434
443
|
env_vars = script_env(e, ver)
|
|
435
444
|
if effective_provider is Provider.llm:
|
|
436
445
|
env_vars.update(
|
|
@@ -40,6 +40,7 @@ import re
|
|
|
40
40
|
from dataclasses import dataclass
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
from typing import Dict, List, Optional, Tuple
|
|
43
|
+
from urllib.parse import unquote, urlsplit
|
|
43
44
|
|
|
44
45
|
import yaml
|
|
45
46
|
|
|
@@ -393,6 +394,8 @@ class EnvConfig:
|
|
|
393
394
|
dictionary_version: str
|
|
394
395
|
aws_profile: Optional[str]
|
|
395
396
|
aws_secret_name: str
|
|
397
|
+
# Canonical scheme-less "bucket/key" (normalize_s3_location at resolve time;
|
|
398
|
+
# callers prepend s3:// themselves).
|
|
396
399
|
schema_s3_uri: str
|
|
397
400
|
domain: str
|
|
398
401
|
app_name: str
|
|
@@ -421,6 +424,74 @@ def _app_or_default(rc, leaf: str, default: str) -> str:
|
|
|
421
424
|
return (rc.get(f"app/{leaf}") or "").strip() or default
|
|
422
425
|
|
|
423
426
|
|
|
427
|
+
#: The two S3 endpoint URL host styles. Path-style must be tried first: a bare
|
|
428
|
+
#: ``s3.<region>.amazonaws.com`` host would otherwise match the virtual-hosted
|
|
429
|
+
#: pattern with bucket "s3". The virtual-hosted bucket group is greedy so
|
|
430
|
+
#: dotted bucket names keep their dots.
|
|
431
|
+
_S3_PATH_STYLE_HOST = re.compile(r"^s3([.-][a-z0-9-]+)*\.amazonaws\.com$")
|
|
432
|
+
_S3_VIRTUAL_HOST = re.compile(r"^(?P<bucket>.+)\.s3([.-][a-z0-9-]+)*\.amazonaws\.com$")
|
|
433
|
+
|
|
434
|
+
#: Remediation appended to normalize_s3_location errors for the SSM app fact.
|
|
435
|
+
_SCHEMA_S3_URI_HINT = (
|
|
436
|
+
"Fix gen3.schemaS3Uri in the CDK config (gen3-aws-data-pipeline) and "
|
|
437
|
+
"re-run `cdk deploy`."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def normalize_s3_location(
|
|
442
|
+
value: str, *, param: str = "app/schema_s3_uri", hint: Optional[str] = None
|
|
443
|
+
) -> str:
|
|
444
|
+
"""Canonicalize an operator-supplied S3 location to scheme-less ``bucket[/key]``.
|
|
445
|
+
|
|
446
|
+
Accepted forms: ``bucket/key`` (canonical), ``s3://bucket/key`` — a repeated
|
|
447
|
+
scheme (``s3://s3://...``) is tolerated, since that is exactly what a
|
|
448
|
+
scheme-carrying SSM value produces once callers prepend ``s3://`` — and the
|
|
449
|
+
two S3 endpoint URL styles (``https://<bucket>.s3.<region>.amazonaws.com/<key>``,
|
|
450
|
+
``https://s3.<region>.amazonaws.com/<bucket>/<key>``); query strings on URL
|
|
451
|
+
forms (presigned links, ``?versionId=``) are dropped.
|
|
452
|
+
|
|
453
|
+
An object key is NOT required — ``resolve_env`` gates every command, so
|
|
454
|
+
shape is enforced at point of use (upload). The key is preserved verbatim,
|
|
455
|
+
trailing slash included, except for percent-decoding of URL forms.
|
|
456
|
+
|
|
457
|
+
:raises ConfigError: empty value, unrecognizable http(s) host (e.g. an AWS
|
|
458
|
+
console page URL), or a bucket segment containing ``:`` (the
|
|
459
|
+
``s3:/bucket`` one-slash typo).
|
|
460
|
+
"""
|
|
461
|
+
original = value.strip()
|
|
462
|
+
|
|
463
|
+
def _bad() -> ConfigError:
|
|
464
|
+
msg = (
|
|
465
|
+
f"Cannot interpret {param} value '{original}' as an S3 location. "
|
|
466
|
+
"Accepted forms: bucket/key, s3://bucket/key, or an S3 endpoint "
|
|
467
|
+
"URL (https://<bucket>.s3.<region>.amazonaws.com/<key> or "
|
|
468
|
+
"https://s3.<region>.amazonaws.com/<bucket>/<key>)."
|
|
469
|
+
)
|
|
470
|
+
return ConfigError(msg + (f" {hint}" if hint else ""))
|
|
471
|
+
|
|
472
|
+
v = original
|
|
473
|
+
if not v:
|
|
474
|
+
raise _bad()
|
|
475
|
+
while v.lower().startswith("s3://"):
|
|
476
|
+
v = v[len("s3://") :]
|
|
477
|
+
if v.lower().startswith(("https://", "http://")):
|
|
478
|
+
parts = urlsplit(v)
|
|
479
|
+
host = parts.hostname or ""
|
|
480
|
+
path = unquote(parts.path)
|
|
481
|
+
if _S3_PATH_STYLE_HOST.match(host):
|
|
482
|
+
v = path.lstrip("/")
|
|
483
|
+
else:
|
|
484
|
+
m = _S3_VIRTUAL_HOST.match(host)
|
|
485
|
+
if not m:
|
|
486
|
+
raise _bad()
|
|
487
|
+
key = path.lstrip("/")
|
|
488
|
+
v = f"{m.group('bucket')}/{key}" if key else m.group("bucket")
|
|
489
|
+
bucket = v.split("/", 1)[0]
|
|
490
|
+
if not bucket or ":" in bucket:
|
|
491
|
+
raise _bad()
|
|
492
|
+
return v
|
|
493
|
+
|
|
494
|
+
|
|
424
495
|
def resolve_env(env: str, project: Optional[str] = None) -> EnvConfig:
|
|
425
496
|
"""Resolve one environment: app INPUT facts + CDK OUTPUT names, from SSM.
|
|
426
497
|
|
|
@@ -454,7 +525,9 @@ def resolve_env(env: str, project: Optional[str] = None) -> EnvConfig:
|
|
|
454
525
|
dictionary_version=rc.app("dictionary_version"),
|
|
455
526
|
aws_profile=profile,
|
|
456
527
|
aws_secret_name=rc.app("aws_secret_name"),
|
|
457
|
-
schema_s3_uri=
|
|
528
|
+
schema_s3_uri=normalize_s3_location(
|
|
529
|
+
rc.app("schema_s3_uri"), hint=_SCHEMA_S3_URI_HINT
|
|
530
|
+
),
|
|
458
531
|
domain=rc.app("domain"),
|
|
459
532
|
app_name=rc.app("app_name"),
|
|
460
533
|
namespace=rc.app("namespace"),
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Node import-order resolution for metadata deletion.
|
|
2
|
+
|
|
3
|
+
Deleting Gen3 metadata must walk nodes children-before-parents, which the
|
|
4
|
+
toolkit historically took from a ``DataImportOrder.txt`` in the caller's
|
|
5
|
+
working directory — a silent dependency that crashed runs started anywhere
|
|
6
|
+
else. This module resolves the order from an explicit chain of sources:
|
|
7
|
+
|
|
8
|
+
1. an explicit ``--import-order`` (local path or ``s3://`` URI) — fatal if
|
|
9
|
+
unreadable, never silently skipped;
|
|
10
|
+
2. the study's release bucket (registered studies only — releases ship a
|
|
11
|
+
``DataImportOrder.txt`` next to the node JSONs);
|
|
12
|
+
3. a ``DataImportOrder.txt`` in the current directory (legacy behavior);
|
|
13
|
+
4. derivation from the dictionary itself: a topological sort over the raw
|
|
14
|
+
bundle's ``links`` (proven byte-identical to simulator-written order
|
|
15
|
+
files), reading either the deployed dictionary at ``schema_s3_uri`` or a
|
|
16
|
+
``--dict-version`` bundle cached under ``~/.g3dt/schemas``.
|
|
17
|
+
|
|
18
|
+
Deliberately NOT used: ``gen3_validator``'s ``DataDictionary.get_node_order``
|
|
19
|
+
(it force-moves core_metadata_collection last, which a deletion reversal would
|
|
20
|
+
delete FIRST — before the file nodes that link to it) and its
|
|
21
|
+
``get_node_link`` (inspects only ``links[0]`` for subgroups).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import urllib.error
|
|
29
|
+
import urllib.request
|
|
30
|
+
from collections import deque
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import List, Optional, Sequence, Tuple
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
#: Bundle keys that are shared definitions, not nodes.
|
|
37
|
+
_META_KEYS = frozenset(
|
|
38
|
+
{"_definitions.yaml", "_terms.yaml", "_settings.yaml", "root.yaml", "metaschema.yaml"}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_ORDER_FILENAME = "DataImportOrder.txt"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ImportOrderError(Exception):
|
|
45
|
+
"""A node order could not be resolved; the message names the fix."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def derive_import_order(schema: dict) -> List[str]:
|
|
49
|
+
"""Topologically sort a raw Gen3 dictionary bundle into submission order.
|
|
50
|
+
|
|
51
|
+
Works on the UNRESOLVED bundle ({"<node>.yaml": {...}, ...}): ``links``
|
|
52
|
+
blocks are self-contained (no $refs), so no schema resolution is needed.
|
|
53
|
+
Skips the shared ``_*.yaml`` definitions, entries without ``properties``,
|
|
54
|
+
and ``submittable: false`` nodes (which removes ``program``). Every
|
|
55
|
+
``links`` entry is flattened — subgroup members and plain links alike —
|
|
56
|
+
into edges ``parent -> child``; edges to nodes outside the set are
|
|
57
|
+
dropped (this handles ``project -> program``).
|
|
58
|
+
|
|
59
|
+
Kahn's algorithm with sorted tie-breaking keeps the output deterministic
|
|
60
|
+
and byte-compatible with simulator-written DataImportOrder.txt files.
|
|
61
|
+
Members of a dependency cycle (impossible in a valid Gen3 dictionary) are
|
|
62
|
+
appended at the end with a warning rather than dropped.
|
|
63
|
+
|
|
64
|
+
Returns SUBMISSION order (parents first); callers reverse for deletion.
|
|
65
|
+
"""
|
|
66
|
+
nodes = {}
|
|
67
|
+
for key, value in schema.items():
|
|
68
|
+
if key in _META_KEYS or not isinstance(value, dict):
|
|
69
|
+
continue
|
|
70
|
+
if "properties" not in value or value.get("submittable") is False:
|
|
71
|
+
continue
|
|
72
|
+
name = key[: -len(".yaml")] if key.endswith(".yaml") else key
|
|
73
|
+
nodes[name] = value
|
|
74
|
+
|
|
75
|
+
graph = {n: [] for n in nodes}
|
|
76
|
+
in_degree = {n: 0 for n in nodes}
|
|
77
|
+
for name, node in nodes.items():
|
|
78
|
+
for entry in node.get("links", []) or []:
|
|
79
|
+
members = entry.get("subgroup", [entry]) if isinstance(entry, dict) else []
|
|
80
|
+
for member in members:
|
|
81
|
+
parent = member.get("target_type")
|
|
82
|
+
if parent in nodes:
|
|
83
|
+
graph[parent].append(name)
|
|
84
|
+
in_degree[name] += 1
|
|
85
|
+
|
|
86
|
+
queue = deque(sorted(n for n, d in in_degree.items() if d == 0))
|
|
87
|
+
ordered: List[str] = []
|
|
88
|
+
while queue:
|
|
89
|
+
node = queue.popleft()
|
|
90
|
+
ordered.append(node)
|
|
91
|
+
for child in sorted(graph[node]):
|
|
92
|
+
in_degree[child] -= 1
|
|
93
|
+
if in_degree[child] == 0:
|
|
94
|
+
queue.append(child)
|
|
95
|
+
queue = deque(sorted(queue))
|
|
96
|
+
|
|
97
|
+
leftovers = sorted(n for n in nodes if n not in ordered)
|
|
98
|
+
if leftovers:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Dictionary link cycle detected; appending unordered node(s): %s "
|
|
101
|
+
"— deleting them may require a re-run.",
|
|
102
|
+
", ".join(leftovers),
|
|
103
|
+
)
|
|
104
|
+
ordered.extend(leftovers)
|
|
105
|
+
return ordered
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ensure_local_dictionary(env_cfg, version: str, schema_dir: Optional[Path] = None) -> Path:
|
|
109
|
+
"""Return the local bundle for ``version``, downloading it if needed.
|
|
110
|
+
|
|
111
|
+
The cache is ``$G3DT_SCHEMA_DIR`` (else ``~/.g3dt/schemas``), shared with
|
|
112
|
+
pull_dict.sh. A cache hit requires a non-empty file that parses as JSON —
|
|
113
|
+
``wget -O`` leaves a zero-byte file behind on a failed pull, so mere
|
|
114
|
+
existence is not trusted. Downloads are validated as JSON BEFORE being
|
|
115
|
+
written, to a temp name replaced atomically, so a failed or partial
|
|
116
|
+
download can never poison the cache.
|
|
117
|
+
"""
|
|
118
|
+
from g3dt import config
|
|
119
|
+
|
|
120
|
+
if schema_dir is None:
|
|
121
|
+
schema_dir = Path(
|
|
122
|
+
os.environ.get("G3DT_SCHEMA_DIR", "~/.g3dt/schemas")
|
|
123
|
+
).expanduser()
|
|
124
|
+
target = schema_dir / config.dictionary_filename(env_cfg, version)
|
|
125
|
+
|
|
126
|
+
if target.exists() and target.stat().st_size > 0:
|
|
127
|
+
try:
|
|
128
|
+
json.loads(target.read_text(encoding="utf-8"))
|
|
129
|
+
return target
|
|
130
|
+
except ValueError:
|
|
131
|
+
logger.warning("Cached dictionary %s is corrupt; re-downloading.", target)
|
|
132
|
+
|
|
133
|
+
url = config.dictionary_url(env_cfg, version)
|
|
134
|
+
logger.info("Downloading dictionary %s from %s", version, url)
|
|
135
|
+
try:
|
|
136
|
+
with urllib.request.urlopen(url) as resp:
|
|
137
|
+
body = resp.read()
|
|
138
|
+
except (urllib.error.HTTPError, urllib.error.URLError) as exc:
|
|
139
|
+
raise ImportOrderError(
|
|
140
|
+
f"Could not download dictionary tag '{version}' from {url}: {exc}. "
|
|
141
|
+
f"Check the tag exists in the schema repo, or pass --import-order."
|
|
142
|
+
)
|
|
143
|
+
try:
|
|
144
|
+
json.loads(body.decode("utf-8"))
|
|
145
|
+
except ValueError:
|
|
146
|
+
raise ImportOrderError(
|
|
147
|
+
f"Downloaded dictionary tag '{version}' from {url} is not valid "
|
|
148
|
+
f"JSON — is the tag/path right?"
|
|
149
|
+
)
|
|
150
|
+
schema_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
tmp = target.with_name(target.name + ".part")
|
|
152
|
+
tmp.write_bytes(body)
|
|
153
|
+
os.replace(tmp, target)
|
|
154
|
+
return target
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def to_deletion_order(nodes: Sequence[str], exclude_nodes: Sequence[str]) -> List[str]:
|
|
158
|
+
"""Filter excluded nodes out of a submission order and reverse it."""
|
|
159
|
+
kept = [n for n in nodes if n not in exclude_nodes]
|
|
160
|
+
kept.reverse()
|
|
161
|
+
return kept
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def resolve_import_order(
|
|
165
|
+
*,
|
|
166
|
+
env_cfg,
|
|
167
|
+
session,
|
|
168
|
+
import_order: Optional[str] = None,
|
|
169
|
+
dict_version: Optional[str] = None,
|
|
170
|
+
study_cfg=None,
|
|
171
|
+
cwd: Optional[Path] = None,
|
|
172
|
+
) -> Tuple[List[str], str]:
|
|
173
|
+
"""Resolve the node SUBMISSION order for a delete, returning (nodes, source).
|
|
174
|
+
|
|
175
|
+
The chain (first hit wins): explicit ``import_order`` (fatal on failure —
|
|
176
|
+
an explicit source is never silently skipped), the registered study's
|
|
177
|
+
release bucket (``study_cfg.s3_metadata_path``; a listing error warns and
|
|
178
|
+
falls through), a ``DataImportOrder.txt`` in the current directory
|
|
179
|
+
(legacy behavior), then derivation from the dictionary — the
|
|
180
|
+
``dict_version`` bundle when given, else the deployed dictionary at
|
|
181
|
+
``s3://{env_cfg.schema_s3_uri}``.
|
|
182
|
+
|
|
183
|
+
``source`` is a human-readable description of the winning step; workers
|
|
184
|
+
log it so operators can always see what ordered a destructive run.
|
|
185
|
+
"""
|
|
186
|
+
from g3dt.upload.metadata_submitter import (
|
|
187
|
+
find_data_import_order_file_s3,
|
|
188
|
+
read_data_import_order_txt_s3,
|
|
189
|
+
read_metadata_json_s3,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# 1. Explicit flag: local path or s3:// URI. Failures are fatal.
|
|
193
|
+
if import_order:
|
|
194
|
+
if import_order.startswith("s3://"):
|
|
195
|
+
try:
|
|
196
|
+
nodes = read_data_import_order_txt_s3(import_order, session)
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
raise ImportOrderError(
|
|
199
|
+
f"Could not read --import-order {import_order}: {exc}"
|
|
200
|
+
)
|
|
201
|
+
return nodes, f"explicit --import-order {import_order}"
|
|
202
|
+
path = Path(import_order)
|
|
203
|
+
try:
|
|
204
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
205
|
+
except OSError as exc:
|
|
206
|
+
raise ImportOrderError(
|
|
207
|
+
f"Could not read --import-order {import_order}: {exc}"
|
|
208
|
+
)
|
|
209
|
+
return (
|
|
210
|
+
[line.strip() for line in lines if line.strip()],
|
|
211
|
+
f"explicit --import-order {path}",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# 2. Registered study: the release ships its own DataImportOrder.txt.
|
|
215
|
+
if study_cfg is not None and str(
|
|
216
|
+
getattr(study_cfg, "s3_metadata_path", "")
|
|
217
|
+
).startswith("s3://"):
|
|
218
|
+
try:
|
|
219
|
+
uri = find_data_import_order_file_s3(
|
|
220
|
+
s3_uri=study_cfg.s3_metadata_path, session=session
|
|
221
|
+
)
|
|
222
|
+
return (
|
|
223
|
+
read_data_import_order_txt_s3(uri, session),
|
|
224
|
+
f"release bucket {uri}",
|
|
225
|
+
)
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"No usable DataImportOrder.txt under %s (%s) — falling back.",
|
|
229
|
+
study_cfg.s3_metadata_path,
|
|
230
|
+
exc,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# 3. Legacy: a DataImportOrder.txt in the working directory.
|
|
234
|
+
local = (cwd or Path.cwd()) / _ORDER_FILENAME
|
|
235
|
+
if local.exists():
|
|
236
|
+
lines = local.read_text(encoding="utf-8").splitlines()
|
|
237
|
+
return (
|
|
238
|
+
[line.strip() for line in lines if line.strip()],
|
|
239
|
+
f"{_ORDER_FILENAME} in current directory ({local.resolve()})",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# 4. Derive from the dictionary.
|
|
243
|
+
if dict_version:
|
|
244
|
+
bundle_path = ensure_local_dictionary(env_cfg, dict_version)
|
|
245
|
+
schema = json.loads(bundle_path.read_text(encoding="utf-8"))
|
|
246
|
+
source = f"derived from dictionary {dict_version} ({bundle_path})"
|
|
247
|
+
else:
|
|
248
|
+
uri = f"s3://{env_cfg.schema_s3_uri}"
|
|
249
|
+
try:
|
|
250
|
+
schema = read_metadata_json_s3(uri, session)
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
raise ImportOrderError(
|
|
253
|
+
f"No DataImportOrder.txt found and the deployed dictionary at "
|
|
254
|
+
f"{uri} (SSM app/schema_s3_uri) could not be read: {exc}. "
|
|
255
|
+
f"Pass --import-order <path|s3://...> or --dict-version <tag>."
|
|
256
|
+
)
|
|
257
|
+
source = f"derived from deployed dictionary {uri}"
|
|
258
|
+
|
|
259
|
+
nodes = derive_import_order(schema)
|
|
260
|
+
if not nodes:
|
|
261
|
+
raise ImportOrderError(
|
|
262
|
+
f"Deriving the node order produced no submittable nodes — is the "
|
|
263
|
+
f"object read for '{source}' a Gen3 dictionary bundle?"
|
|
264
|
+
)
|
|
265
|
+
return nodes, source
|