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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. g3dt/__init__.py +0 -0
  2. g3dt/cli/__init__.py +5 -0
  3. g3dt/cli/_internal/__init__.py +1 -0
  4. g3dt/cli/_internal/dispatch.py +428 -0
  5. g3dt/cli/_internal/registry.py +65 -0
  6. g3dt/cli/_internal/resolve.py +22 -0
  7. g3dt/cli/_internal/runner.py +76 -0
  8. g3dt/cli/_internal/safety.py +110 -0
  9. g3dt/cli/config_cmds.py +202 -0
  10. g3dt/cli/delete_cmds.py +101 -0
  11. g3dt/cli/dict_cmds.py +102 -0
  12. g3dt/cli/ec2_cmds.py +114 -0
  13. g3dt/cli/indexd_cmds.py +57 -0
  14. g3dt/cli/jobs.py +83 -0
  15. g3dt/cli/k8s.py +54 -0
  16. g3dt/cli/main.py +110 -0
  17. g3dt/cli/metadata.py +76 -0
  18. g3dt/cli/synth.py +206 -0
  19. g3dt/config.py +393 -0
  20. g3dt/indexd/__init__.py +0 -0
  21. g3dt/indexd/indexd_registrar.py +244 -0
  22. g3dt/ingest/ingest.py +629 -0
  23. g3dt/resolver.py +163 -0
  24. g3dt/services/delete/delete_all_metadata_for_project.py +170 -0
  25. g3dt/services/delete/delete_metadata.sh +153 -0
  26. g3dt/services/delete/delete_metadata_by_guid.py +338 -0
  27. g3dt/services/dictionary/deploy_dd.sh +65 -0
  28. g3dt/services/dictionary/pull_dict.sh +59 -0
  29. g3dt/services/dictionary/upload_dictionary.py +109 -0
  30. g3dt/services/indexd/register_indexd.py +240 -0
  31. g3dt/services/k8s_ops/argocd_restart_etl.sh +140 -0
  32. g3dt/services/k8s_ops/argocd_restart_ms.sh +102 -0
  33. g3dt/services/k8s_ops/argocd_restart_schema.sh +106 -0
  34. g3dt/services/k8s_ops/login_to_pod.sh +110 -0
  35. g3dt/services/k8s_ops/restart_etl_and_ms.sh +56 -0
  36. g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +183 -0
  37. g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +124 -0
  38. g3dt/services/synthetic_data/generate_synth_metadata.sh +133 -0
  39. g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +165 -0
  40. g3dt/services/upload/metadata/upload_all_studies.sh +108 -0
  41. g3dt/services/upload/metadata/upload_metadata.py +152 -0
  42. g3dt/upload/__init__.py +1 -0
  43. g3dt/upload/metadata_deleter.py +265 -0
  44. g3dt/upload/metadata_submitter.py +1093 -0
  45. g3dt/upload/upload_synthdata_s3.py +164 -0
  46. g3dt/utils/athena_utils.py +834 -0
  47. g3dt/utils/dbt_utils.py +66 -0
  48. g3dt/utils/release_writer.py +188 -0
  49. g3dt/validate/validate.py +609 -0
  50. gen3_dataops_toolkit-2.0.0.dist-info/METADATA +125 -0
  51. gen3_dataops_toolkit-2.0.0.dist-info/RECORD +53 -0
  52. gen3_dataops_toolkit-2.0.0.dist-info/WHEEL +4 -0
  53. gen3_dataops_toolkit-2.0.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,338 @@
1
+ import sys
2
+ import time
3
+ import logging
4
+ import argparse
5
+ import yaml
6
+ from g3dt.upload.metadata_submitter import (
7
+ create_boto3_session,
8
+ get_gen3_api_key_aws_secret,
9
+ infer_api_endpoint_from_jwt,
10
+ create_gen3_submission_class,
11
+ )
12
+ from g3dt.upload.metadata_deleter import (
13
+ query_metadata_upload_guids,
14
+ delete_records_by_guid,
15
+ )
16
+
17
+ # ANSI colour codes (matching metadata_submitter.py style)
18
+ GREEN = "\033[92m"
19
+ RED = "\033[91m"
20
+ YELLOW = "\033[93m"
21
+ BLUE = "\033[94m"
22
+ RESET = "\033[0m"
23
+
24
+ EXCLUDE_NODES = [
25
+ "program",
26
+ "project",
27
+ "acknowledgement",
28
+ "publication",
29
+ ]
30
+
31
+ # Exit code that signals "study exists but has no data at this version —
32
+ # skipped". A bulk caller (services/delete/delete_metadata.sh) treats this as a
33
+ # skip-and-continue rather than a failure. Only emitted with --skip-if-empty.
34
+ SKIP_EXIT_CODE = 3
35
+
36
+
37
+ def setup_logger():
38
+ logger = logging.getLogger()
39
+ logger.setLevel(logging.INFO)
40
+ if not logger.handlers:
41
+ handler = logging.StreamHandler(sys.stdout)
42
+ formatter = logging.Formatter(
43
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
44
+ )
45
+ handler.setFormatter(formatter)
46
+ logger.addHandler(handler)
47
+ return logger
48
+
49
+
50
+ # Shared config resolution (SSM-backed) — see src/g3dt/config.py
51
+ from g3dt import config as g3dt_config, resolver # noqa: E402
52
+
53
+
54
+ def load_import_order(import_order_path, exclude_nodes=None):
55
+ """
56
+ Reads the DataImportOrder.txt file and returns the node list
57
+ in deletion order (reversed, with excluded nodes removed).
58
+ """
59
+ if exclude_nodes is None:
60
+ exclude_nodes = EXCLUDE_NODES
61
+ with open(import_order_path, 'r', encoding='utf-8') as f:
62
+ nodes = [line.strip() for line in f if line.strip()]
63
+ nodes = [n for n in nodes if n not in exclude_nodes]
64
+ nodes.reverse()
65
+ return nodes
66
+
67
+
68
+ def main():
69
+ logger = setup_logger()
70
+
71
+ parser = argparse.ArgumentParser(
72
+ description=(
73
+ "Delete Gen3 metadata records by GUID. Queries the Athena "
74
+ "metadata_upload table for matching records per node "
75
+ "(in reverse DataImportOrder) and deletes them from Gen3."
76
+ ),
77
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
78
+ )
79
+ parser.add_argument(
80
+ "--study",
81
+ required=True,
82
+ help=(
83
+ "Study key (bare or env-suffixed) "
84
+ "(e.g. ausdiab, caughtcad, edcad, cdah)"
85
+ ),
86
+ )
87
+ parser.add_argument(
88
+ "--env",
89
+ required=True,
90
+ help="Environment to use (selects AWS secret, profile, etc.)",
91
+ )
92
+ parser.add_argument(
93
+ "--version",
94
+ required=True,
95
+ help="Metadata version to filter on (e.g. 0.8.1)",
96
+ )
97
+ parser.add_argument(
98
+ "--import-order",
99
+ default="DataImportOrder.txt",
100
+ help="Path to DataImportOrder.txt",
101
+ )
102
+ parser.add_argument(
103
+ "--node",
104
+ default=None,
105
+ help=(
106
+ "Delete only a specific node (e.g. 'subject'). "
107
+ "If omitted, all nodes are processed in reverse "
108
+ "DataImportOrder."
109
+ ),
110
+ )
111
+ parser.add_argument(
112
+ "--prompt",
113
+ action="store_true",
114
+ default=False,
115
+ help="Prompt for confirmation before deleting.",
116
+ )
117
+ parser.add_argument(
118
+ "--batch-size",
119
+ type=int,
120
+ default=40,
121
+ help="Number of UUIDs per DELETE request.",
122
+ )
123
+ parser.add_argument(
124
+ "--batch-delay",
125
+ type=float,
126
+ default=0.5,
127
+ help="Seconds to pause between batches.",
128
+ )
129
+ parser.add_argument(
130
+ "--verbose",
131
+ action="store_true",
132
+ default=False,
133
+ help="Log full API response JSON for each request.",
134
+ )
135
+ parser.add_argument(
136
+ "--delay",
137
+ type=float,
138
+ default=1.0,
139
+ help="Seconds to wait between nodes.",
140
+ )
141
+ parser.add_argument(
142
+ "--skip-if-empty",
143
+ action="store_true",
144
+ default=False,
145
+ help=(
146
+ "If the study has no data at the given version, exit with the skip "
147
+ "code (3) instead of 0. Used by the bulk delete loop to "
148
+ "skip-and-continue rather than treat it as a failure."
149
+ ),
150
+ )
151
+
152
+ args = parser.parse_args()
153
+
154
+ if args.verbose:
155
+ logger.setLevel(logging.DEBUG)
156
+
157
+ # Env facts + resource names from SSM; the study registry from the marker
158
+ # or s3://<metadata-bucket>/config/studies.yaml.
159
+ try:
160
+ env_cfg = g3dt_config.resolve_env(args.env)
161
+ except g3dt_config.ConfigError as exc:
162
+ logger.error(str(exc))
163
+ sys.exit(1)
164
+ try:
165
+ study_cfg = g3dt_config.resolve_study(args.study, args.env)
166
+ except g3dt_config.ConfigError as exc:
167
+ if args.skip_if_empty:
168
+ logger.warning(
169
+ "Study '%s' not found in configuration — skipping.", args.study
170
+ )
171
+ sys.exit(SKIP_EXIT_CODE)
172
+ logger.error(str(exc))
173
+ sys.exit(1)
174
+
175
+ project_id = study_cfg.project_id
176
+ program_id = study_cfg.program_id
177
+
178
+ aws_secret_name = env_cfg.aws_secret_name
179
+ aws_profile = env_cfg.aws_profile
180
+ aws_region = env_cfg.region
181
+
182
+ rc = resolver.resolve(
183
+ g3dt_config.require_project(),
184
+ g3dt_config.env_base(args.env),
185
+ profile=aws_profile,
186
+ )
187
+ # Upload-tracking table: conventional name in the env's metadata DB
188
+ # (exactly like the CDK's `releases` table).
189
+ database = rc.metadata_db
190
+ table = g3dt_config.METADATA_UPLOAD_TABLE
191
+ athena_s3_output = rc.athena_output_location
192
+ workgroup = rc.athena_workgroup
193
+
194
+ # Construct compound project_id for Athena query
195
+ compound_project_id = f"{program_id}-{project_id}"
196
+
197
+ logger.info(
198
+ "Study: %s | Env: %s | Project: %s | Version: %s",
199
+ args.study,
200
+ args.env,
201
+ compound_project_id,
202
+ args.version,
203
+ )
204
+
205
+ # AWS and Gen3 authentication
206
+ session = create_boto3_session(aws_profile=aws_profile)
207
+ api_key = get_gen3_api_key_aws_secret(
208
+ secret_name=aws_secret_name,
209
+ region_name=aws_region,
210
+ session=session,
211
+ )
212
+
213
+ # Derive api_endpoint from JWT
214
+ api_endpoint = infer_api_endpoint_from_jwt(api_key['api_key'])
215
+ logger.info("Derived API endpoint: %s", api_endpoint)
216
+
217
+ # Create Gen3Submission instance
218
+ sub = create_gen3_submission_class(api_key)
219
+
220
+ # Determine node list
221
+ if args.node:
222
+ nodes_to_delete = [args.node]
223
+ logger.info(
224
+ "%s[SINGLE NODE]%s Targeting node: %s",
225
+ BLUE, RESET, args.node,
226
+ )
227
+ else:
228
+ nodes_to_delete = load_import_order(args.import_order)
229
+ logger.info(
230
+ "Loaded %s nodes from %s (deletion order, "
231
+ "excluding %s)",
232
+ len(nodes_to_delete),
233
+ args.import_order,
234
+ EXCLUDE_NODES,
235
+ )
236
+
237
+ if args.prompt:
238
+ confirm = input(
239
+ f"Proceed with deletion for project "
240
+ f"{compound_project_id}, version {args.version}, "
241
+ f"{len(nodes_to_delete)} node(s)? (yes/no): "
242
+ ).strip().lower()
243
+ if confirm != "yes":
244
+ logger.info("Deletion cancelled by user.")
245
+ return
246
+
247
+ # Process each node
248
+ total_deleted = 0
249
+ total_skipped = 0
250
+ total_nodes = len(nodes_to_delete)
251
+
252
+ for idx, node in enumerate(nodes_to_delete, start=1):
253
+ logger.info(
254
+ "%s[Node %d/%d]%s | Project: %-10s | "
255
+ "Node: %-25s | Querying...",
256
+ BLUE, idx, total_nodes, RESET,
257
+ compound_project_id, node,
258
+ )
259
+
260
+ df = query_metadata_upload_guids(
261
+ database=database,
262
+ table=table,
263
+ project_id=compound_project_id,
264
+ api_endpoint=api_endpoint,
265
+ version=args.version,
266
+ athena_s3_output=athena_s3_output,
267
+ workgroup=workgroup,
268
+ aws_region=aws_region,
269
+ aws_profile=aws_profile,
270
+ node=node,
271
+ )
272
+
273
+ if df.empty:
274
+ logger.info(
275
+ "%s[SKIP]%s | Project: %-10s | "
276
+ "Node: %-25s | No records found",
277
+ YELLOW, RESET,
278
+ compound_project_id, node,
279
+ )
280
+ total_skipped += 1
281
+ continue
282
+
283
+ uuids = df['gen3_guid'].dropna().unique().tolist()
284
+ logger.info(
285
+ "%s[DELETE]%s | Project: %-10s | "
286
+ "Node: %-25s | Records: %s",
287
+ BLUE, RESET,
288
+ compound_project_id, node, len(uuids),
289
+ )
290
+
291
+ delete_records_by_guid(
292
+ gen3_submission=sub,
293
+ program_id=program_id,
294
+ project_id=project_id,
295
+ uuids=uuids,
296
+ batch_size=args.batch_size,
297
+ batch_delay=args.batch_delay,
298
+ verbose=args.verbose,
299
+ )
300
+
301
+ logger.info(
302
+ "%s[SUCCESS]%s | Project: %-10s | "
303
+ "Node: %-25s | Deleted: %s",
304
+ GREEN, RESET,
305
+ compound_project_id, node, len(uuids),
306
+ )
307
+ total_deleted += len(uuids)
308
+
309
+ if idx < total_nodes:
310
+ time.sleep(args.delay)
311
+
312
+ logger.info(
313
+ "=========================================="
314
+ )
315
+ logger.info(
316
+ "Deletion complete. Total deleted: %s | "
317
+ "Nodes skipped: %s | Nodes processed: %s",
318
+ total_deleted,
319
+ total_skipped,
320
+ total_nodes - total_skipped,
321
+ )
322
+
323
+ # No records matched the requested version across any node. This usually
324
+ # means the version was never uploaded (or the data was uploaded without a
325
+ # version), so surface an actionable hint rather than a silent "0 deleted".
326
+ if total_deleted == 0:
327
+ logger.warning(
328
+ "Data version '%s' not found for study '%s'. Ensure each data node "
329
+ "has a `data_version` property for versioning to work.",
330
+ args.version,
331
+ args.study,
332
+ )
333
+ if args.skip_if_empty:
334
+ sys.exit(SKIP_EXIT_CODE)
335
+
336
+
337
+ if __name__ == "__main__":
338
+ main()
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+ set -o pipefail
6
+
7
+ # 1. Pull the dictionary at a specific version.
8
+ # 2. Upload the dictionary to S3.
9
+ # 3. Restart microservices (schema).
10
+
11
+ # Usage: bash deploy_dd.sh <profile>
12
+ # <profile> is display-only; all configuration comes from G3DT_* environment
13
+ # variables exported by the g3dt CLI (g3dt.config.script_env).
14
+ PROFILE=$1
15
+
16
+ if [ -z "$PROFILE" ]; then
17
+ echo "Usage: $0 <profile> (test|staging|prod) — run via the g3dt CLI"
18
+ exit 1
19
+ fi
20
+
21
+ # Defining script paths (sibling scripts ship together inside the package)
22
+ SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
23
+ SERVICE_DIR="${SCRIPT_DIR}/.."
24
+
25
+ # Configuration from G3DT_* env vars (fail loudly if a required one is missing)
26
+ VERSION="${G3DT_DICTIONARY_VERSION:?G3DT_DICTIONARY_VERSION not set — run via the g3dt CLI}"
27
+ SCHEMA_S3_URI="${G3DT_SCHEMA_S3_URI:?G3DT_SCHEMA_S3_URI not set — run via the g3dt CLI}"
28
+ DOMAIN="${G3DT_DOMAIN:?G3DT_DOMAIN not set — run via the g3dt CLI}"
29
+ APP_NAME="${G3DT_APP_NAME:?G3DT_APP_NAME not set — run via the g3dt CLI}"
30
+ NAMESPACE="${G3DT_NAMESPACE:?G3DT_NAMESPACE not set — run via the g3dt CLI}"
31
+ CLUSTER_NAME="${G3DT_CLUSTER_NAME:?G3DT_CLUSTER_NAME not set — run via the g3dt CLI}"
32
+ SCHEMA_REPO="${G3DT_SCHEMA_REPO:?G3DT_SCHEMA_REPO not set — run via the g3dt CLI}"
33
+ REGION="${G3DT_REGION:-ap-southeast-2}"
34
+ EKS_ARN="${G3DT_EKS_ARN:-}"
35
+ # Downloaded schemas live outside the installed package.
36
+ SCHEMA_DIR="${G3DT_SCHEMA_DIR:-$HOME/.g3dt/schemas}"
37
+ ARGO_SCRIPT_DIR="${SERVICE_DIR}/k8s_ops"
38
+
39
+ # Never export an empty AWS_PROFILE (empty means ambient credentials).
40
+ if [ -n "${G3DT_AWS_PROFILE:-}" ]; then
41
+ export AWS_PROFILE="${G3DT_AWS_PROFILE}"
42
+ echo "==== [0] Configuring AWS PROFILE as '${AWS_PROFILE}' for profile '${PROFILE}' ===="
43
+ else
44
+ echo "==== [0] No AWS profile set for '${PROFILE}' — using ambient AWS credentials ===="
45
+ fi
46
+
47
+ echo "Updating kubeconfig for cluster: ${CLUSTER_NAME}"
48
+ # eks_arn is optional; only pass --role-arn when it is set.
49
+ ROLE_ARG=""
50
+ if [ -n "${EKS_ARN}" ] && [ "${EKS_ARN}" != "null" ]; then
51
+ ROLE_ARG="--role-arn ${EKS_ARN}"
52
+ fi
53
+ if ! aws eks update-kubeconfig --name "${CLUSTER_NAME}" --region "${REGION}" ${ROLE_ARG}; then
54
+ echo "Error: Failed to update kubeconfig for cluster ${CLUSTER_NAME}"
55
+ exit 1
56
+ fi
57
+
58
+ echo "==== [1] Pulling dictionary for version ${VERSION} ===="
59
+ bash "${SERVICE_DIR}/dictionary/pull_dict.sh" "https://raw.githubusercontent.com/${SCHEMA_REPO}/refs/tags/${VERSION}/dictionary/prod_dict/acdc_schema.json"
60
+
61
+ echo "==== [2] Uploading dictionary to S3: s3://${SCHEMA_S3_URI} ===="
62
+ python3 "${SERVICE_DIR}/dictionary/upload_dictionary.py" "${SCHEMA_DIR}/acdc_schema_${VERSION}.json" "s3://${SCHEMA_S3_URI}"
63
+
64
+ echo "==== [3] Restarting microservices (schema) ===="
65
+ bash "${ARGO_SCRIPT_DIR}/argocd_restart_schema.sh" -d "${DOMAIN}" -a "${APP_NAME}" -n "${NAMESPACE}"
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+
3
+ show_help() {
4
+ echo "Usage: $0 <dict_url> [output_file]"
5
+ echo
6
+ echo "Download a dictionary JSON file from the specified URL."
7
+ echo
8
+ echo "Arguments:"
9
+ echo " dict_url The URL to download the dictionary JSON from."
10
+ echo " output_file Optional. The file to save the dictionary as (default: acdc_schema.json)."
11
+ }
12
+
13
+ if [[ "$1" == "-h" || "$1" == "--help" || "$#" -lt 1 ]]; then
14
+ show_help
15
+ exit 1
16
+ fi
17
+
18
+ DICT_URL="$1"
19
+
20
+ # Downloads are written outside the installed package (never into site-packages).
21
+ SCHEMA_DIR="${G3DT_SCHEMA_DIR:-$HOME/.g3dt/schemas}"
22
+ mkdir -p "$SCHEMA_DIR"
23
+
24
+ # Extract version tag after '/tags/' or '/tag/' or '/refs/tags/' in the URL
25
+ if [[ "$DICT_URL" =~ /tags/([^/]+)/ ]]; then
26
+ VERSION="${BASH_REMATCH[1]}"
27
+ elif [[ "$DICT_URL" =~ /refs/tags/([^/]+)/ ]]; then
28
+ VERSION="${BASH_REMATCH[1]}"
29
+ else
30
+ VERSION="unknown"
31
+ fi
32
+
33
+ # Set output filename, including version tag if not overridden by user
34
+ if [[ -n "$2" ]]; then
35
+ OUTPUT_BASENAME="$2"
36
+ else
37
+ # Get the filename from the URL
38
+ BASE_NAME=$(basename "$DICT_URL")
39
+ # Insert version before file extension if possible
40
+ if [[ "$BASE_NAME" == *.* ]]; then
41
+ EXTENSION="${BASE_NAME##*.}"
42
+ NAME_NO_EXT="${BASE_NAME%.*}"
43
+ OUTPUT_BASENAME="${NAME_NO_EXT}_${VERSION}.${EXTENSION}"
44
+ else
45
+ OUTPUT_BASENAME="${BASE_NAME}_${VERSION}"
46
+ fi
47
+ fi
48
+
49
+ OUTPUT_FILE="${SCHEMA_DIR}/${OUTPUT_BASENAME}"
50
+
51
+ echo "Downloading dictionary from $DICT_URL..."
52
+ wget -O "$OUTPUT_FILE" "$DICT_URL"
53
+
54
+ if [ $? -eq 0 ]; then
55
+ echo "Download complete: $OUTPUT_FILE"
56
+ else
57
+ echo "Download failed." >&2
58
+ exit 1
59
+ fi
@@ -0,0 +1,109 @@
1
+ import boto3
2
+ import json
3
+ import logging
4
+ import sys
5
+ from botocore.exceptions import ClientError
6
+
7
+ logging.basicConfig(
8
+ level=logging.INFO,
9
+ format="%(asctime)s %(levelname)s %(message)s",
10
+ handlers=[logging.StreamHandler()]
11
+ )
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def get_s3_client(profile_name=None):
16
+ """
17
+ Creates an S3 client with optional AWS profile.
18
+
19
+ Args:
20
+ profile_name (str, optional): AWS profile name.
21
+
22
+ Returns:
23
+ boto3.client: S3 client.
24
+ """
25
+ if profile_name:
26
+ session = boto3.Session(profile_name=profile_name)
27
+ return session.client("s3")
28
+ return boto3.client("s3")
29
+
30
+
31
+ def get_dict_version(dict_file_path):
32
+ """
33
+ Extracts the dictionary version from the provided JSON/YAML combo settings.
34
+
35
+ Args:
36
+ dict_file_path (str): Path to the dictionary file.
37
+
38
+ Returns:
39
+ str: Dictionary version.
40
+ """
41
+ with open(dict_file_path, "r", encoding="utf-8") as f:
42
+ dict_data = json.load(f)
43
+ return dict_data.get("_settings.yaml", {}).get("_dict_version", None)
44
+
45
+
46
+ def upload_dict_to_s3(dict_file_path, s3_target_uri, dict_version, profile_name=None):
47
+ """
48
+ Uploads a dictionary file to S3 with metadata.
49
+
50
+ Args:
51
+ dict_file_path (str): Local dictionary path.
52
+ s3_target_uri (str): URI like s3://bucket/key.
53
+ dict_version (str): Dictionary version string.
54
+ profile_name (str, optional): AWS profile name.
55
+
56
+ Returns:
57
+ bool: True on success, False otherwise.
58
+ """
59
+ if not s3_target_uri.startswith("s3://"):
60
+ logger.error(f"Invalid S3 URI: {s3_target_uri}")
61
+ return False
62
+
63
+ try:
64
+ s3_path = s3_target_uri[len("s3://") :]
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)
69
+ # Set S3 metadata key to "version" instead of "dict_version"
70
+ extra_args = {"Metadata": {"version": dict_version or "unknown"}}
71
+ s3_client = get_s3_client(profile_name)
72
+ s3_client.upload_file(dict_file_path, bucket, key, ExtraArgs=extra_args)
73
+ logger.info(
74
+ f"Successfully uploaded '{dict_file_path}' (version: {dict_version}) to {s3_target_uri}"
75
+ )
76
+ return True
77
+ except ClientError as e:
78
+ logger.error(f"Failed to upload file to {s3_target_uri}: {e}")
79
+ raise
80
+ except Exception as e:
81
+ logger.error(f"Unexpected error uploading file: {e}")
82
+ raise
83
+
84
+
85
+ def main():
86
+ if len(sys.argv) < 3 or len(sys.argv) > 4:
87
+ print(
88
+ "Usage: python upload_dictionary.py <local_file_path> <s3_uri> [aws_profile]",
89
+ file=sys.stderr,
90
+ )
91
+ sys.exit(1)
92
+
93
+ local_file_path = sys.argv[1]
94
+ s3_uri = sys.argv[2]
95
+ profile_name = sys.argv[3] if len(sys.argv) == 4 else None
96
+
97
+ dict_version = get_dict_version(local_file_path)
98
+ if dict_version is None:
99
+ logger.error(f"Could not determine dictionary version from {local_file_path}")
100
+ sys.exit(1)
101
+
102
+ success = upload_dict_to_s3(local_file_path, s3_uri, dict_version, profile_name)
103
+ if not success:
104
+ sys.exit(1)
105
+ # You may add a completion message if desired
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()