gen3-dataops-toolkit 4.1.0__tar.gz → 4.2.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.2.0}/PKG-INFO +1 -1
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/pyproject.toml +2 -2
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/delete_cmds.py +83 -11
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/synth.py +9 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/config.py +74 -1
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/delete/delete_all_metadata_for_project.py +24 -4
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/delete/delete_metadata.sh +29 -7
- gen3_dataops_toolkit-4.2.0/src/g3dt/services/delete/delete_synth_metadata_by_version.py +334 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/dictionary/upload_dictionary.py +13 -8
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/synthetic_data/generate_synth_metadata.sh +8 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/metadata_deleter.py +78 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/README.md +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/aws_quiet.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/dispatch.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/helptext.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/registry.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/resolve.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/runner.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/safety.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/config_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/dict_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/ec2_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/indexd_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/jobs.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/k8s.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/main.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/metadata.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/pipeline_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/release_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/study_cmds.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/contexts.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/indexd/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/indexd/file_access.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/indexd/indexd_registrar.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/ingest/ingest.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/resolver.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/delete/delete_metadata_by_guid.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/dictionary/deploy_dd.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/dictionary/pull_dict.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/indexd/register_indexd.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/indexd/verify_file_access.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/k8s_ops/argocd_restart_etl.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/k8s_ops/argocd_restart_ms.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/k8s_ops/argocd_restart_schema.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/k8s_ops/login_to_pod.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/k8s_ops/restart_etl_and_ms.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/upload/metadata/upload_all_studies.sh +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/upload/metadata/upload_metadata.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/studies.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/__init__.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/metadata_submitter.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/upload_synthdata_s3.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/utils/athena_utils.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/utils/dbt_utils.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/utils/release_writer.py +0 -0
- {gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/validate/validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "gen3-dataops-toolkit"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.2.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,13 @@ 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
|
+
|
|
10
17
|
Every command confirms before acting. Production always requires typing the
|
|
11
18
|
target id, even with ``--yes``. Deleting ALL versions always prompts, even with
|
|
12
19
|
``--yes``. Confirmation happens locally before any EC2 dispatch (SSM has no
|
|
@@ -61,12 +68,30 @@ def _normalise_version(raw: str, where: str) -> str:
|
|
|
61
68
|
return match.group(1)
|
|
62
69
|
|
|
63
70
|
|
|
64
|
-
def
|
|
71
|
+
def _synthetic_version(raw: str) -> str:
|
|
72
|
+
"""Canonicalise a synthetic version token: ``all`` (any case) or verbatim.
|
|
73
|
+
|
|
74
|
+
Synthetic versions are matched exactly against the records' ``data_version``
|
|
75
|
+
property, and the natural label there is the dictionary version WITH its
|
|
76
|
+
leading ``v`` (batch dirs are ``~/.g3dt/synth_metadata/v1.3.0/...``) — so
|
|
77
|
+
unlike ``_normalise_version`` nothing is stripped. A version that matches
|
|
78
|
+
no records is reported by the worker (skip + hint), not silently absorbed.
|
|
79
|
+
"""
|
|
80
|
+
token = raw.strip()
|
|
81
|
+
return "all" if token.lower() == "all" else token
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_study_specs(studies: str, fallback, env: str, synthetic: bool = False):
|
|
65
85
|
"""Turn ``--studies`` into ``[(resolved_study_key, version), ...]``.
|
|
66
86
|
|
|
67
87
|
Each comma-separated entry is ``name`` or ``name:version``. A bare name
|
|
68
88
|
takes *fallback* (the ``--version`` default); *fallback* is ``None`` when
|
|
69
|
-
``--version`` was not given, which makes a bare name a usage error
|
|
89
|
+
``--version`` was not given, which makes a bare name a usage error —
|
|
90
|
+
except with *synthetic*, where a bare name defaults to ``all`` (the whole
|
|
91
|
+
point of the flag is "wipe the synthetic project").
|
|
92
|
+
|
|
93
|
+
With *synthetic* the raw name IS the Gen3 project code: no study-registry
|
|
94
|
+
lookup, and version tokens pass through :func:`_synthetic_version`.
|
|
70
95
|
|
|
71
96
|
Every entry is validated before anything is dispatched, so a typo in the
|
|
72
97
|
last study cannot leave the earlier ones already deleted.
|
|
@@ -102,9 +127,15 @@ def _parse_study_specs(studies: str, fallback, env: str):
|
|
|
102
127
|
raise typer.Exit(2)
|
|
103
128
|
|
|
104
129
|
if sep:
|
|
105
|
-
version =
|
|
130
|
+
version = (
|
|
131
|
+
_synthetic_version(raw_version)
|
|
132
|
+
if synthetic
|
|
133
|
+
else _normalise_version(raw_version, f"for study '{name}'")
|
|
134
|
+
)
|
|
106
135
|
elif fallback is not None:
|
|
107
136
|
version = fallback
|
|
137
|
+
elif synthetic:
|
|
138
|
+
version = "all"
|
|
108
139
|
else:
|
|
109
140
|
typer.secho(
|
|
110
141
|
f"No version for study '{name}': add ':<version>' to it "
|
|
@@ -115,7 +146,7 @@ def _parse_study_specs(studies: str, fallback, env: str):
|
|
|
115
146
|
)
|
|
116
147
|
raise typer.Exit(2)
|
|
117
148
|
|
|
118
|
-
specs.append((study_of(name, env).key, version))
|
|
149
|
+
specs.append((name if synthetic else study_of(name, env).key, version))
|
|
119
150
|
|
|
120
151
|
if not specs:
|
|
121
152
|
typer.secho("--studies is empty.", fg=typer.colors.RED, err=True)
|
|
@@ -141,6 +172,20 @@ def metadata(
|
|
|
141
172
|
"':version', e.g. 0.9.8, or 'all' for every version.",
|
|
142
173
|
),
|
|
143
174
|
node: Optional[str] = typer.Option(None, "--node", help="Delete only this node type."),
|
|
175
|
+
synthetic: bool = typer.Option(
|
|
176
|
+
False,
|
|
177
|
+
"--synthetic",
|
|
178
|
+
help="Registry-free synthetic-data mode: each --studies name is the "
|
|
179
|
+
"Gen3 project id itself (no SSM study registry). Bare names "
|
|
180
|
+
"default to version 'all'; a specific version matches records' "
|
|
181
|
+
"data_version property verbatim.",
|
|
182
|
+
),
|
|
183
|
+
program_id: Optional[str] = typer.Option(
|
|
184
|
+
None,
|
|
185
|
+
"--program-id",
|
|
186
|
+
help="Gen3 program for --synthetic (default: program1). "
|
|
187
|
+
"Invalid without --synthetic.",
|
|
188
|
+
),
|
|
144
189
|
yes: bool = typer.Option(
|
|
145
190
|
False, "--yes", "-y", help="Skip the non-prod prompt (specific-version only)."
|
|
146
191
|
),
|
|
@@ -156,12 +201,24 @@ def metadata(
|
|
|
156
201
|
|
|
157
202
|
g3dt delete metadata --studies "ausdiab:0.7.5,cdah:0.8.1" --env staging
|
|
158
203
|
g3dt delete metadata --studies "ausdiab:all,cdah" --version 0.9.8 --env staging
|
|
204
|
+
g3dt delete metadata --studies "synthetic_dataset_1,synthetic_dataset_2" --env test --synthetic
|
|
159
205
|
"""
|
|
160
206
|
env = resolve.active_env(env)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
207
|
+
if program_id is not None and not synthetic:
|
|
208
|
+
typer.secho(
|
|
209
|
+
"--program-id is only valid with --synthetic (registered studies "
|
|
210
|
+
"carry their program in the study registry).",
|
|
211
|
+
fg=typer.colors.RED,
|
|
212
|
+
err=True,
|
|
213
|
+
)
|
|
214
|
+
raise typer.Exit(2)
|
|
215
|
+
if version is None:
|
|
216
|
+
fallback = None
|
|
217
|
+
elif synthetic:
|
|
218
|
+
fallback = _synthetic_version(version)
|
|
219
|
+
else:
|
|
220
|
+
fallback = _normalise_version(version, "for --version")
|
|
221
|
+
specs = _parse_study_specs(studies, fallback, env, synthetic=synthetic)
|
|
165
222
|
versions = [v for _, v in specs]
|
|
166
223
|
|
|
167
224
|
# The typed production confirmation stays the study keys alone: short
|
|
@@ -171,13 +228,17 @@ def metadata(
|
|
|
171
228
|
uniform = len(set(versions)) == 1
|
|
172
229
|
any_all = "all" in versions
|
|
173
230
|
|
|
231
|
+
prefix = "synthetic " if synthetic else ""
|
|
174
232
|
if uniform and versions[0] == "all":
|
|
175
|
-
action = "deletion of ALL VERSIONS"
|
|
233
|
+
action = f"{prefix}deletion of ALL VERSIONS"
|
|
176
234
|
elif uniform:
|
|
177
|
-
|
|
235
|
+
# Synthetic versions are verbatim data_version values (often already
|
|
236
|
+
# v-prefixed); Athena versions are canonical x.y.z, displayed with v.
|
|
237
|
+
shown = versions[0] if synthetic else f"v{versions[0]}"
|
|
238
|
+
action = f"{prefix}deletion of {shown}"
|
|
178
239
|
else:
|
|
179
240
|
plan = ", ".join(f"{key}:{v}" for key, v in specs)
|
|
180
|
-
action = f"deletion of per-study versions [{plan}]"
|
|
241
|
+
action = f"{prefix}deletion of per-study versions [{plan}]"
|
|
181
242
|
|
|
182
243
|
# Deleting every version is the most destructive path: always prompt (pass
|
|
183
244
|
# assume_yes=False so --yes can't bypass it; prod still types the target).
|
|
@@ -200,6 +261,11 @@ def metadata(
|
|
|
200
261
|
]
|
|
201
262
|
if node:
|
|
202
263
|
a += ["--node", node]
|
|
264
|
+
if synthetic:
|
|
265
|
+
# Program is always passed explicitly: the shell default makes it
|
|
266
|
+
# optional on the wire, but an explicit value keeps the contract
|
|
267
|
+
# visible in logs and SSM command history.
|
|
268
|
+
a += ["--synthetic", "--program-id", program_id or "program1"]
|
|
203
269
|
return a
|
|
204
270
|
|
|
205
271
|
def remote_cli(env_name):
|
|
@@ -212,6 +278,12 @@ def metadata(
|
|
|
212
278
|
a.append("--yes")
|
|
213
279
|
if node:
|
|
214
280
|
a += ["--node", node]
|
|
281
|
+
if synthetic:
|
|
282
|
+
# Without this the remote re-entry would re-parse --studies
|
|
283
|
+
# against the study registry on the box and exit 2.
|
|
284
|
+
a.append("--synthetic")
|
|
285
|
+
if program_id is not None:
|
|
286
|
+
a += ["--program-id", program_id]
|
|
215
287
|
return a
|
|
216
288
|
|
|
217
289
|
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"),
|
|
@@ -81,6 +81,21 @@ def main():
|
|
|
81
81
|
required=True,
|
|
82
82
|
help="Environment to use (selects AWS secret, profile, etc.)",
|
|
83
83
|
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--synthetic",
|
|
86
|
+
action="store_true",
|
|
87
|
+
default=False,
|
|
88
|
+
help=(
|
|
89
|
+
"Registry-free mode for synthetic data: --study is used directly "
|
|
90
|
+
"as the Gen3 project code (no SSM study registry lookup), with "
|
|
91
|
+
"the program from --program-id."
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--program-id",
|
|
96
|
+
default="program1",
|
|
97
|
+
help="Gen3 program for --synthetic mode.",
|
|
98
|
+
)
|
|
84
99
|
parser.add_argument(
|
|
85
100
|
"--import-order",
|
|
86
101
|
default="DataImportOrder.txt",
|
|
@@ -106,16 +121,21 @@ def main():
|
|
|
106
121
|
|
|
107
122
|
# Env facts from SSM; the study registry from the marker or
|
|
108
123
|
# SSM /{project}/{env}/studies/* (legacy studies.yaml fallback until 5.0).
|
|
124
|
+
# Synthetic projects are not registered studies: --synthetic skips the
|
|
125
|
+
# registry and takes --study as the Gen3 project code itself.
|
|
109
126
|
try:
|
|
110
127
|
env_cfg = g3dt_config.resolve_env(args.env)
|
|
111
|
-
|
|
128
|
+
if args.synthetic:
|
|
129
|
+
project_id = args.study
|
|
130
|
+
program_id = args.program_id
|
|
131
|
+
else:
|
|
132
|
+
study_cfg = g3dt_config.resolve_study(args.study, args.env)
|
|
133
|
+
project_id = study_cfg.project_id
|
|
134
|
+
program_id = study_cfg.program_id
|
|
112
135
|
except g3dt_config.ConfigError as exc:
|
|
113
136
|
logger.error(str(exc))
|
|
114
137
|
sys.exit(1)
|
|
115
138
|
|
|
116
|
-
project_id = study_cfg.project_id
|
|
117
|
-
program_id = study_cfg.program_id
|
|
118
|
-
|
|
119
139
|
aws_secret_name = env_cfg.aws_secret_name
|
|
120
140
|
aws_profile = env_cfg.aws_profile
|
|
121
141
|
aws_region = env_cfg.region
|
|
@@ -10,21 +10,26 @@ SKIP_EXIT_CODE=3
|
|
|
10
10
|
|
|
11
11
|
usage() {
|
|
12
12
|
cat <<EOF
|
|
13
|
-
Usage: $(basename "$0") --studies <name[:version|all],...> --env <environment> [--version <version|all>] [--node <node>]
|
|
13
|
+
Usage: $(basename "$0") --studies <name[:version|all],...> --env <environment> [--version <version|all>] [--node <node>] [--synthetic] [--program-id <program>]
|
|
14
14
|
|
|
15
15
|
Delete metadata for each study sequentially, in a single job.
|
|
16
16
|
|
|
17
17
|
Arguments:
|
|
18
|
-
--studies
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
--env
|
|
22
|
-
--version
|
|
23
|
-
--node
|
|
18
|
+
--studies Comma-separated study config keys, each optionally qualified with
|
|
19
|
+
its own version (e.g. ausdiab_staging:0.7.5,cdah_staging:0.8.1,
|
|
20
|
+
or bare ausdiab_staging to take the --version default)
|
|
21
|
+
--env Environment string passed to the Python worker (e.g. staging_ec2)
|
|
22
|
+
--version Default version for bare --studies entries (e.g. 0.9.8), or 'all'
|
|
23
|
+
--node (optional) Restrict deletion to a single node type
|
|
24
|
+
--synthetic (optional) Registry-free synthetic-data mode: each study name is
|
|
25
|
+
the Gen3 project code itself (no SSM study registry lookup)
|
|
26
|
+
--program-id (optional) Gen3 program for --synthetic mode (default program1)
|
|
24
27
|
|
|
25
28
|
Behaviour:
|
|
26
29
|
* version 'all' -> delete_all_metadata_for_project.py (deletes whole nodes)
|
|
27
30
|
* version <x.y.z> -> delete_metadata_by_guid.py (Athena GUID lookup for that version)
|
|
31
|
+
* --synthetic + 'all' -> delete_all_metadata_for_project.py --synthetic
|
|
32
|
+
* --synthetic + version -> delete_synth_metadata_by_version.py (GraphQL data_version filter)
|
|
28
33
|
|
|
29
34
|
A study that exists but has no data at the requested version is skipped and the
|
|
30
35
|
loop continues. Only genuine errors (Gen3/AWS failures) count as failures.
|
|
@@ -45,6 +50,8 @@ STUDIES=""
|
|
|
45
50
|
ENV=""
|
|
46
51
|
VERSION=""
|
|
47
52
|
NODE=""
|
|
53
|
+
SYNTHETIC=0
|
|
54
|
+
PROGRAM_ID="program1"
|
|
48
55
|
|
|
49
56
|
while [[ $# -gt 0 ]]; do
|
|
50
57
|
case "$1" in
|
|
@@ -64,6 +71,14 @@ while [[ $# -gt 0 ]]; do
|
|
|
64
71
|
NODE="$2"
|
|
65
72
|
shift 2
|
|
66
73
|
;;
|
|
74
|
+
--synthetic)
|
|
75
|
+
SYNTHETIC=1
|
|
76
|
+
shift
|
|
77
|
+
;;
|
|
78
|
+
--program-id)
|
|
79
|
+
PROGRAM_ID="$2"
|
|
80
|
+
shift 2
|
|
81
|
+
;;
|
|
67
82
|
*)
|
|
68
83
|
echo "ERROR: Unknown argument: $1"
|
|
69
84
|
usage
|
|
@@ -122,6 +137,7 @@ echo "Environment : ${ENV}"
|
|
|
122
137
|
echo "Studies : ${STUDIES}"
|
|
123
138
|
echo "Version : ${VERSION:-(per study, from --studies)}"
|
|
124
139
|
[[ -n "$NODE" ]] && echo "Node : ${NODE}"
|
|
140
|
+
[[ $SYNTHETIC -eq 1 ]] && echo "Synthetic : yes (program: ${PROGRAM_ID})"
|
|
125
141
|
echo "Failure log : ${FAILED_LOG}"
|
|
126
142
|
echo "============================================"
|
|
127
143
|
echo ""
|
|
@@ -140,6 +156,12 @@ for i in "${!STUDY_NAMES[@]}"; do
|
|
|
140
156
|
if [[ "$study_version_lc" == "all" ]]; then
|
|
141
157
|
CMD=("${G3DT_PYTHON:-python3}" "${SCRIPT_DIR}/delete_all_metadata_for_project.py"
|
|
142
158
|
--study "$study" --env "$ENV")
|
|
159
|
+
[[ $SYNTHETIC -eq 1 ]] && CMD+=(--synthetic --program-id "$PROGRAM_ID")
|
|
160
|
+
[[ -n "$NODE" ]] && CMD+=(--node "$NODE")
|
|
161
|
+
elif [[ $SYNTHETIC -eq 1 ]]; then
|
|
162
|
+
CMD=("${G3DT_PYTHON:-python3}" "${SCRIPT_DIR}/delete_synth_metadata_by_version.py"
|
|
163
|
+
--study "$study" --env "$ENV" --version "$study_version"
|
|
164
|
+
--program-id "$PROGRAM_ID" --skip-if-empty)
|
|
143
165
|
[[ -n "$NODE" ]] && CMD+=(--node "$NODE")
|
|
144
166
|
else
|
|
145
167
|
CMD=("${G3DT_PYTHON:-python3}" "${SCRIPT_DIR}/delete_metadata_by_guid.py"
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Registry-free, version-filtered deletion for synthetic metadata.
|
|
2
|
+
|
|
3
|
+
Synthetic uploads write no Athena receipts (upload_synth_metadata_sheepdog
|
|
4
|
+
submits with upload_to_database=False), so delete_metadata_by_guid.py's
|
|
5
|
+
receipt lookup can never find them. This worker instead filters records
|
|
6
|
+
server-side: per node (reverse DataImportOrder), it queries sheepdog GraphQL
|
|
7
|
+
for records whose `data_version` property equals --version and deletes them.
|
|
8
|
+
The --study value is used directly as the Gen3 project code — no SSM study
|
|
9
|
+
registry involvement.
|
|
10
|
+
|
|
11
|
+
A node whose schema does not declare `data_version` makes the GraphQL query
|
|
12
|
+
error; that node is warned about and skipped. If nothing is deleted anywhere,
|
|
13
|
+
the run exits with the skip code (3, with --skip-if-empty) plus a hint, so the
|
|
14
|
+
bulk loop counts it as skipped rather than failed.
|
|
15
|
+
"""
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import logging
|
|
19
|
+
import argparse
|
|
20
|
+
|
|
21
|
+
from gen3.submission import Gen3SubmissionQueryError
|
|
22
|
+
|
|
23
|
+
from g3dt.upload.metadata_submitter import (
|
|
24
|
+
create_boto3_session,
|
|
25
|
+
get_gen3_api_key_aws_secret,
|
|
26
|
+
create_gen3_submission_class,
|
|
27
|
+
)
|
|
28
|
+
from g3dt.upload.metadata_deleter import delete_node_records_by_property
|
|
29
|
+
|
|
30
|
+
# ANSI colour codes (matching metadata_submitter.py style)
|
|
31
|
+
GREEN = "\033[92m"
|
|
32
|
+
RED = "\033[91m"
|
|
33
|
+
YELLOW = "\033[93m"
|
|
34
|
+
BLUE = "\033[94m"
|
|
35
|
+
RESET = "\033[0m"
|
|
36
|
+
|
|
37
|
+
EXCLUDE_NODES = [
|
|
38
|
+
"program",
|
|
39
|
+
"project",
|
|
40
|
+
"acknowledgement",
|
|
41
|
+
"publication",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Exit code that signals "project exists but has no data at this version —
|
|
45
|
+
# skipped". The bulk caller (services/delete/delete_metadata.sh) treats this as
|
|
46
|
+
# a skip-and-continue rather than a failure. Only emitted with --skip-if-empty.
|
|
47
|
+
SKIP_EXIT_CODE = 3
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def setup_logger():
|
|
51
|
+
logger = logging.getLogger()
|
|
52
|
+
logger.setLevel(logging.INFO)
|
|
53
|
+
if not logger.handlers:
|
|
54
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
55
|
+
formatter = logging.Formatter(
|
|
56
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
57
|
+
)
|
|
58
|
+
handler.setFormatter(formatter)
|
|
59
|
+
logger.addHandler(handler)
|
|
60
|
+
return logger
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Shared config resolution (SSM-backed) — see src/g3dt/config.py
|
|
64
|
+
from g3dt import config as g3dt_config # noqa: E402
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_import_order(import_order_path, exclude_nodes=None):
|
|
68
|
+
"""
|
|
69
|
+
Reads the DataImportOrder.txt file and returns the node list
|
|
70
|
+
in deletion order (reversed, with excluded nodes removed).
|
|
71
|
+
"""
|
|
72
|
+
if exclude_nodes is None:
|
|
73
|
+
exclude_nodes = EXCLUDE_NODES
|
|
74
|
+
with open(import_order_path, 'r', encoding='utf-8') as f:
|
|
75
|
+
nodes = [line.strip() for line in f if line.strip()]
|
|
76
|
+
nodes = [n for n in nodes if n not in exclude_nodes]
|
|
77
|
+
nodes.reverse()
|
|
78
|
+
return nodes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
logger = setup_logger()
|
|
83
|
+
|
|
84
|
+
parser = argparse.ArgumentParser(
|
|
85
|
+
description=(
|
|
86
|
+
"Delete synthetic Gen3 metadata records at one data version. "
|
|
87
|
+
"Filters each node (in reverse DataImportOrder) via sheepdog "
|
|
88
|
+
"GraphQL on the records' data_version property and deletes the "
|
|
89
|
+
"matches. --study is the Gen3 project code itself — no study "
|
|
90
|
+
"registry lookup."
|
|
91
|
+
),
|
|
92
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--study",
|
|
96
|
+
required=True,
|
|
97
|
+
help="Gen3 project code (e.g. synthetic_dataset_1)",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--env",
|
|
101
|
+
required=True,
|
|
102
|
+
help="Environment to use (selects AWS secret, profile, etc.)",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--version",
|
|
106
|
+
required=True,
|
|
107
|
+
help=(
|
|
108
|
+
"Value the records' data_version property must equal, matched "
|
|
109
|
+
"verbatim (e.g. v1.3.0)"
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--program-id",
|
|
114
|
+
default="program1",
|
|
115
|
+
help="Gen3 program the project lives under.",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--import-order",
|
|
119
|
+
default="DataImportOrder.txt",
|
|
120
|
+
help="Path to DataImportOrder.txt",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--node",
|
|
124
|
+
default=None,
|
|
125
|
+
help=(
|
|
126
|
+
"Delete only a specific node (e.g. 'subject'). "
|
|
127
|
+
"If omitted, all nodes are processed in reverse "
|
|
128
|
+
"DataImportOrder."
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--prompt",
|
|
133
|
+
action="store_true",
|
|
134
|
+
default=False,
|
|
135
|
+
help="Prompt for confirmation before deleting.",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--batch-size",
|
|
139
|
+
type=int,
|
|
140
|
+
default=40,
|
|
141
|
+
help="Number of UUIDs per DELETE request.",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--batch-delay",
|
|
145
|
+
type=float,
|
|
146
|
+
default=0.5,
|
|
147
|
+
help="Seconds to pause between batches.",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--verbose",
|
|
151
|
+
action="store_true",
|
|
152
|
+
default=False,
|
|
153
|
+
help="Log full API response JSON for each request.",
|
|
154
|
+
)
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--delay",
|
|
157
|
+
type=float,
|
|
158
|
+
default=1.0,
|
|
159
|
+
help="Seconds to wait between nodes.",
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--skip-if-empty",
|
|
163
|
+
action="store_true",
|
|
164
|
+
default=False,
|
|
165
|
+
help=(
|
|
166
|
+
"If no records match the given version, exit with the skip "
|
|
167
|
+
"code (3) instead of 0. Used by the bulk delete loop to "
|
|
168
|
+
"skip-and-continue rather than treat it as a failure."
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
args = parser.parse_args()
|
|
173
|
+
|
|
174
|
+
if args.verbose:
|
|
175
|
+
logger.setLevel(logging.DEBUG)
|
|
176
|
+
|
|
177
|
+
# Env facts from SSM. Deliberately NO resolve_study: synthetic projects
|
|
178
|
+
# are not in the study registry — the --study value IS the project code.
|
|
179
|
+
try:
|
|
180
|
+
env_cfg = g3dt_config.resolve_env(args.env)
|
|
181
|
+
except g3dt_config.ConfigError as exc:
|
|
182
|
+
logger.error(str(exc))
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
project_id = args.study
|
|
186
|
+
program_id = args.program_id
|
|
187
|
+
|
|
188
|
+
aws_secret_name = env_cfg.aws_secret_name
|
|
189
|
+
aws_profile = env_cfg.aws_profile
|
|
190
|
+
aws_region = env_cfg.region
|
|
191
|
+
|
|
192
|
+
compound_project_id = f"{program_id}-{project_id}"
|
|
193
|
+
|
|
194
|
+
logger.info(
|
|
195
|
+
"Synthetic | Env: %s | Project: %s | Version: %s",
|
|
196
|
+
args.env,
|
|
197
|
+
compound_project_id,
|
|
198
|
+
args.version,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# AWS and Gen3 authentication
|
|
202
|
+
session = create_boto3_session(aws_profile=aws_profile)
|
|
203
|
+
api_key = get_gen3_api_key_aws_secret(
|
|
204
|
+
secret_name=aws_secret_name,
|
|
205
|
+
region_name=aws_region,
|
|
206
|
+
session=session,
|
|
207
|
+
)
|
|
208
|
+
sub = create_gen3_submission_class(api_key)
|
|
209
|
+
|
|
210
|
+
# Determine node list
|
|
211
|
+
if args.node:
|
|
212
|
+
nodes_to_delete = [args.node]
|
|
213
|
+
logger.info(
|
|
214
|
+
"%s[SINGLE NODE]%s Targeting node: %s",
|
|
215
|
+
BLUE, RESET, args.node,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
nodes_to_delete = load_import_order(args.import_order)
|
|
219
|
+
logger.info(
|
|
220
|
+
"Loaded %s nodes from %s (deletion order, "
|
|
221
|
+
"excluding %s)",
|
|
222
|
+
len(nodes_to_delete),
|
|
223
|
+
args.import_order,
|
|
224
|
+
EXCLUDE_NODES,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if args.prompt:
|
|
228
|
+
confirm = input(
|
|
229
|
+
f"Proceed with deletion for project "
|
|
230
|
+
f"{compound_project_id}, data_version {args.version}, "
|
|
231
|
+
f"{len(nodes_to_delete)} node(s)? (yes/no): "
|
|
232
|
+
).strip().lower()
|
|
233
|
+
if confirm != "yes":
|
|
234
|
+
logger.info("Deletion cancelled by user.")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Process each node
|
|
238
|
+
total_deleted = 0
|
|
239
|
+
total_skipped = 0
|
|
240
|
+
failed_nodes = []
|
|
241
|
+
total_nodes = len(nodes_to_delete)
|
|
242
|
+
|
|
243
|
+
for idx, node in enumerate(nodes_to_delete, start=1):
|
|
244
|
+
logger.info(
|
|
245
|
+
"%s[Node %d/%d]%s | Project: %-10s | "
|
|
246
|
+
"Node: %-25s | Querying...",
|
|
247
|
+
BLUE, idx, total_nodes, RESET,
|
|
248
|
+
compound_project_id, node,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
deleted = delete_node_records_by_property(
|
|
253
|
+
gen3_submission=sub,
|
|
254
|
+
program_id=program_id,
|
|
255
|
+
project_id=project_id,
|
|
256
|
+
node=node,
|
|
257
|
+
property_name="data_version",
|
|
258
|
+
property_value=args.version,
|
|
259
|
+
batch_size=args.batch_size,
|
|
260
|
+
batch_delay=args.batch_delay,
|
|
261
|
+
verbose=args.verbose,
|
|
262
|
+
)
|
|
263
|
+
except Gen3SubmissionQueryError as exc:
|
|
264
|
+
logger.warning(
|
|
265
|
+
"%s[SKIP]%s | Node: %-25s | GraphQL rejected the "
|
|
266
|
+
"data_version filter (property not in this node's "
|
|
267
|
+
"schema?): %s",
|
|
268
|
+
YELLOW, RESET, node, exc,
|
|
269
|
+
)
|
|
270
|
+
total_skipped += 1
|
|
271
|
+
continue
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
logger.error(
|
|
274
|
+
"%s[FAILED]%s | Node: %-25s | %s",
|
|
275
|
+
RED, RESET, node, exc,
|
|
276
|
+
)
|
|
277
|
+
failed_nodes.append(node)
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
if deleted == 0:
|
|
281
|
+
logger.info(
|
|
282
|
+
"%s[SKIP]%s | Project: %-10s | "
|
|
283
|
+
"Node: %-25s | No records found",
|
|
284
|
+
YELLOW, RESET,
|
|
285
|
+
compound_project_id, node,
|
|
286
|
+
)
|
|
287
|
+
total_skipped += 1
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
logger.info(
|
|
291
|
+
"%s[SUCCESS]%s | Project: %-10s | "
|
|
292
|
+
"Node: %-25s | Deleted: %s",
|
|
293
|
+
GREEN, RESET,
|
|
294
|
+
compound_project_id, node, deleted,
|
|
295
|
+
)
|
|
296
|
+
total_deleted += deleted
|
|
297
|
+
|
|
298
|
+
if idx < total_nodes:
|
|
299
|
+
time.sleep(args.delay)
|
|
300
|
+
|
|
301
|
+
logger.info(
|
|
302
|
+
"=========================================="
|
|
303
|
+
)
|
|
304
|
+
logger.info(
|
|
305
|
+
"Deletion complete. Total deleted: %s | "
|
|
306
|
+
"Nodes skipped: %s | Nodes failed: %s",
|
|
307
|
+
total_deleted,
|
|
308
|
+
total_skipped,
|
|
309
|
+
len(failed_nodes),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if failed_nodes:
|
|
313
|
+
logger.error(
|
|
314
|
+
"Failed nodes: %s", ", ".join(failed_nodes)
|
|
315
|
+
)
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
|
|
318
|
+
# No records matched the requested version on any node. Usually the data
|
|
319
|
+
# was generated without the property (see `g3dt synth generate
|
|
320
|
+
# --data-version`), so surface the actionable hint rather than a silent
|
|
321
|
+
# "0 deleted".
|
|
322
|
+
if total_deleted == 0:
|
|
323
|
+
logger.warning(
|
|
324
|
+
"Data version '%s' not found for study '%s'. Ensure each data node "
|
|
325
|
+
"has a `data_version` property for versioning to work.",
|
|
326
|
+
args.version,
|
|
327
|
+
args.study,
|
|
328
|
+
)
|
|
329
|
+
if args.skip_if_empty:
|
|
330
|
+
sys.exit(SKIP_EXIT_CODE)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == "__main__":
|
|
334
|
+
main()
|
|
@@ -4,6 +4,8 @@ import logging
|
|
|
4
4
|
import sys
|
|
5
5
|
from botocore.exceptions import ClientError
|
|
6
6
|
|
|
7
|
+
from g3dt.config import ConfigError, normalize_s3_location
|
|
8
|
+
|
|
7
9
|
logging.basicConfig(
|
|
8
10
|
level=logging.INFO,
|
|
9
11
|
format="%(asctime)s %(levelname)s %(message)s",
|
|
@@ -49,23 +51,26 @@ def upload_dict_to_s3(dict_file_path, s3_target_uri, dict_version, profile_name=
|
|
|
49
51
|
|
|
50
52
|
Args:
|
|
51
53
|
dict_file_path (str): Local dictionary path.
|
|
52
|
-
s3_target_uri (str):
|
|
54
|
+
s3_target_uri (str): Target like s3://bucket/key. Forgiving: bare
|
|
55
|
+
bucket/key, a doubled s3:// scheme, and S3 endpoint https URLs
|
|
56
|
+
are all accepted (see g3dt.config.normalize_s3_location).
|
|
53
57
|
dict_version (str): Dictionary version string.
|
|
54
58
|
profile_name (str, optional): AWS profile name.
|
|
55
59
|
|
|
56
60
|
Returns:
|
|
57
61
|
bool: True on success, False otherwise.
|
|
58
62
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
try:
|
|
64
|
+
location = normalize_s3_location(s3_target_uri, param="s3_uri argument")
|
|
65
|
+
except ConfigError as e:
|
|
66
|
+
logger.error(str(e))
|
|
67
|
+
return False
|
|
68
|
+
if "/" not in location:
|
|
69
|
+
logger.error(f"S3 location missing an object key: {s3_target_uri}")
|
|
61
70
|
return False
|
|
62
71
|
|
|
63
72
|
try:
|
|
64
|
-
|
|
65
|
-
if "/" not in s3_path:
|
|
66
|
-
logger.error(f"S3 URI missing key: {s3_target_uri}")
|
|
67
|
-
return False
|
|
68
|
-
bucket, key = s3_path.split("/", 1)
|
|
73
|
+
bucket, key = location.split("/", 1)
|
|
69
74
|
# Set S3 metadata key to "version" instead of "dict_version"
|
|
70
75
|
extra_args = {"Metadata": {"version": dict_version or "unknown"}}
|
|
71
76
|
s3_client = get_s3_client(profile_name)
|
|
@@ -34,6 +34,9 @@ Options:
|
|
|
34
34
|
'random' needs no key; 'llm' uses \$G3DT_LLM_PROVIDER /
|
|
35
35
|
\$G3DT_LLM_MODEL / \$LLM_API_KEY_FILE (set by g3dt).
|
|
36
36
|
--seed N RNG seed for reproducible output.
|
|
37
|
+
--data-version V Stamp every record's data_version property with V
|
|
38
|
+
(requires the dictionary to declare data_version;
|
|
39
|
+
enables versioned deletion later).
|
|
37
40
|
--output-root DIR Root output dir. Default: ${DEFAULT_OUTPUT_ROOT}
|
|
38
41
|
-h, --help Show this help and exit.
|
|
39
42
|
|
|
@@ -57,6 +60,7 @@ STUDIES="${DEFAULT_STUDIES}"
|
|
|
57
60
|
NUM_RECORDS="${DEFAULT_NUM_RECORDS}"
|
|
58
61
|
PROVIDER="${DEFAULT_PROVIDER}"
|
|
59
62
|
SEED=""
|
|
63
|
+
DATA_VERSION=""
|
|
60
64
|
OUTPUT_ROOT="${DEFAULT_OUTPUT_ROOT}"
|
|
61
65
|
|
|
62
66
|
while [[ $# -gt 0 ]]; do
|
|
@@ -67,6 +71,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
67
71
|
--num-records) NUM_RECORDS="$2"; shift 2 ;;
|
|
68
72
|
--provider) PROVIDER="$2"; shift 2 ;;
|
|
69
73
|
--seed) SEED="$2"; shift 2 ;;
|
|
74
|
+
--data-version) DATA_VERSION="$2"; shift 2 ;;
|
|
70
75
|
--output-root) OUTPUT_ROOT="$2"; shift 2 ;;
|
|
71
76
|
-h|--help) usage; exit 0 ;;
|
|
72
77
|
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
|
|
@@ -122,6 +127,9 @@ for i in "${!STUDY_ARRAY[@]}"; do
|
|
|
122
127
|
--num-records "$N"
|
|
123
128
|
--provider "$PROVIDER")
|
|
124
129
|
[[ -n "$SEED" ]] && CMD+=(--seed "$SEED")
|
|
130
|
+
# --set pins a declared data property to a constant on every record; the
|
|
131
|
+
# simulator errors before generating if no node declares data_version.
|
|
132
|
+
[[ -n "$DATA_VERSION" ]] && CMD+=(--set "data_version=${DATA_VERSION}")
|
|
125
133
|
if [[ "$PROVIDER" == "llm" ]]; then
|
|
126
134
|
# Vendor/model resolved by g3dt (flags > SSM > default) and forwarded
|
|
127
135
|
# as simulator flags, because the simulator's own precedence puts
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/metadata_deleter.py
RENAMED
|
@@ -202,6 +202,84 @@ def delete_records_by_guid(
|
|
|
202
202
|
)
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def delete_node_records_by_property(
|
|
206
|
+
gen3_submission: Gen3Submission,
|
|
207
|
+
program_id: str,
|
|
208
|
+
project_id: str,
|
|
209
|
+
node: str,
|
|
210
|
+
property_name: str,
|
|
211
|
+
property_value: str,
|
|
212
|
+
page_size: int = 100,
|
|
213
|
+
batch_size: int = 40,
|
|
214
|
+
batch_delay: float = 0.5,
|
|
215
|
+
verbose: bool = False,
|
|
216
|
+
) -> int:
|
|
217
|
+
"""
|
|
218
|
+
Deletes every record of one node whose ``property_name`` equals
|
|
219
|
+
``property_value``, via the sheepdog GraphQL endpoint — no Athena
|
|
220
|
+
receipts required.
|
|
221
|
+
|
|
222
|
+
Pages with query -> delete -> re-query until the query returns no
|
|
223
|
+
records (the same pattern the SDK's own delete_nodes uses), so a
|
|
224
|
+
partially failed batch is simply retried on the next round. If two
|
|
225
|
+
consecutive rounds return the same first id, no progress is being
|
|
226
|
+
made (e.g. every deletion is failing) and a RuntimeError is raised.
|
|
227
|
+
|
|
228
|
+
GraphQL errors from the query (e.g. the node's schema does not
|
|
229
|
+
declare the property) propagate to the caller as
|
|
230
|
+
Gen3SubmissionQueryError — per-node tolerance is the caller's
|
|
231
|
+
decision, not this function's.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
gen3_submission (Gen3Submission): An authenticated
|
|
235
|
+
Gen3Submission instance.
|
|
236
|
+
program_id (str): The Gen3 program name.
|
|
237
|
+
project_id (str): The Gen3 project name (bare, not compound).
|
|
238
|
+
node (str): The node type to delete records from.
|
|
239
|
+
property_name (str): Node property to filter on
|
|
240
|
+
(e.g. "data_version").
|
|
241
|
+
property_value (str): Exact value the property must equal.
|
|
242
|
+
page_size (int, optional): Records fetched per query round.
|
|
243
|
+
batch_size (int, optional): Passed to delete_records_by_guid.
|
|
244
|
+
batch_delay (float, optional): Passed to delete_records_by_guid.
|
|
245
|
+
verbose (bool, optional): Passed to delete_records_by_guid.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
int: Number of distinct records deleted.
|
|
249
|
+
"""
|
|
250
|
+
compound_project_id = f"{program_id}-{project_id}"
|
|
251
|
+
seen = set()
|
|
252
|
+
first_uuid = ""
|
|
253
|
+
while True:
|
|
254
|
+
query_string = (
|
|
255
|
+
f'{{ {node} (first: {page_size}, '
|
|
256
|
+
f'project_id: "{compound_project_id}", '
|
|
257
|
+
f'{property_name}: "{property_value}") {{ id }} }}'
|
|
258
|
+
)
|
|
259
|
+
res = gen3_submission.query(query_string)
|
|
260
|
+
uuids = [x["id"] for x in res["data"][node]]
|
|
261
|
+
if not uuids:
|
|
262
|
+
break
|
|
263
|
+
if first_uuid == uuids[0]:
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"No progress deleting '{node}' records with "
|
|
266
|
+
f"{property_name}='{property_value}' — record "
|
|
267
|
+
f"{uuids[0]} keeps reappearing (deletions failing?)."
|
|
268
|
+
)
|
|
269
|
+
first_uuid = uuids[0]
|
|
270
|
+
seen.update(uuids)
|
|
271
|
+
delete_records_by_guid(
|
|
272
|
+
gen3_submission,
|
|
273
|
+
program_id,
|
|
274
|
+
project_id,
|
|
275
|
+
uuids,
|
|
276
|
+
batch_size=batch_size,
|
|
277
|
+
batch_delay=batch_delay,
|
|
278
|
+
verbose=verbose,
|
|
279
|
+
)
|
|
280
|
+
return len(seen)
|
|
281
|
+
|
|
282
|
+
|
|
205
283
|
def delete_project_metadata(
|
|
206
284
|
gen3_submission: Gen3Submission,
|
|
207
285
|
program_id: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/__init__.py
RENAMED
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/aws_quiet.py
RENAMED
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/dispatch.py
RENAMED
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/helptext.py
RENAMED
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/cli/_internal/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/indexd/indexd_registrar.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/dictionary/deploy_dd.sh
RENAMED
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/dictionary/pull_dict.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/services/k8s_ops/login_to_pod.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/metadata_submitter.py
RENAMED
|
File without changes
|
{gen3_dataops_toolkit-4.1.0 → gen3_dataops_toolkit-4.2.0}/src/g3dt/upload/upload_synthdata_s3.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|