pvw-cli 1.0.12__py3-none-any.whl → 1.0.14__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.

Potentially problematic release.


This version of pvw-cli might be problematic. Click here for more details.

@@ -8,6 +8,7 @@ import csv
8
8
  import json
9
9
  import tempfile
10
10
  import os
11
+ import time
11
12
  from rich.console import Console
12
13
  from rich.table import Table
13
14
  from rich.text import Text
@@ -115,7 +116,7 @@ def create(name, description, type, owner_id, status, parent_id, payload_file):
115
116
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
116
117
  return
117
118
 
118
- console.print(f"[green] SUCCESS:[/green] Created governance domain '{name}'")
119
+ console.print(f"[green] SUCCESS:[/green] Created governance domain '{name}'")
119
120
  console.print(json.dumps(result, indent=2))
120
121
 
121
122
  except Exception as e:
@@ -123,9 +124,20 @@ def create(name, description, type, owner_id, status, parent_id, payload_file):
123
124
 
124
125
 
125
126
  @domain.command(name="list")
126
- @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
127
- def list_domains(output_json):
128
- """List all governance domains."""
127
+ @click.option(
128
+ "--output",
129
+ type=click.Choice(["table", "json", "jsonc"]),
130
+ default="table",
131
+ help="Output format: table (default, formatted), json (plain, parseable), jsonc (colored JSON)"
132
+ )
133
+ def list_domains(output):
134
+ """List all governance domains.
135
+
136
+ Output formats:
137
+ - table: Formatted table output with Rich (default)
138
+ - json: Plain JSON for scripting (use with PowerShell ConvertFrom-Json)
139
+ - jsonc: Colored JSON with syntax highlighting for viewing
140
+ """
129
141
  try:
130
142
  client = UnifiedCatalogClient()
131
143
  args = {} # No arguments needed for list operation
@@ -147,8 +159,13 @@ def list_domains(output_json):
147
159
  console.print("[yellow]No governance domains found.[/yellow]")
148
160
  return
149
161
 
150
- # Output in JSON format if requested
151
- if output_json:
162
+ # Handle output format
163
+ if output == "json":
164
+ # Plain JSON for scripting (PowerShell compatible)
165
+ print(json.dumps(domains, indent=2))
166
+ return
167
+ elif output == "jsonc":
168
+ # Colored JSON for viewing
152
169
  _format_json_output(domains)
153
170
  return
154
171
 
@@ -274,7 +291,7 @@ def create(
274
291
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
275
292
  return
276
293
 
277
- console.print(f"[green] SUCCESS:[/green] Created data product '{name}'")
294
+ console.print(f"[green] SUCCESS:[/green] Created data product '{name}'")
278
295
  console.print(json.dumps(result, indent=2))
279
296
 
280
297
  except Exception as e:
@@ -444,7 +461,7 @@ def update(
444
461
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
445
462
  return
446
463
 
447
- console.print(f"[green] SUCCESS:[/green] Updated data product '{product_id}'")
464
+ console.print(f"[green] SUCCESS:[/green] Updated data product '{product_id}'")
448
465
  console.print(json.dumps(result, indent=2))
449
466
 
450
467
  except Exception as e:
@@ -472,11 +489,11 @@ def delete(product_id, yes):
472
489
 
473
490
  # DELETE operations may return empty response on success
474
491
  if result is None or (isinstance(result, dict) and not result.get("error")):
475
- console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
492
+ console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
476
493
  elif isinstance(result, dict) and "error" in result:
477
494
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
478
495
  else:
479
- console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
496
+ console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
480
497
  if result:
481
498
  console.print(json.dumps(result, indent=2))
482
499
 
@@ -579,7 +596,7 @@ def create_glossary(name, description, domain_id):
579
596
  return
580
597
 
581
598
  guid = result.get("guid") if isinstance(result, dict) else None
582
- console.print(f"[green] SUCCESS:[/green] Created glossary '{name}'")
599
+ console.print(f"[green] SUCCESS:[/green] Created glossary '{name}'")
583
600
  if guid:
584
601
  console.print(f"[cyan]GUID:[/cyan] {guid}")
585
602
  console.print(f"\n[dim]Use this GUID: --glossary-guid {guid}[/dim]")
@@ -664,13 +681,13 @@ def create_glossaries_for_domains():
664
681
  guid = result.get("guid") if isinstance(result, dict) else None
665
682
 
666
683
  if guid:
667
- console.print(f"[green] Created:[/green] {glossary_name} (GUID: {guid})")
684
+ console.print(f"[green] Created:[/green] {glossary_name} (GUID: {guid})")
668
685
  created_count += 1
669
686
  else:
670
- console.print(f"[yellow] Created {glossary_name} but no GUID returned[/yellow]")
687
+ console.print(f"[yellow] Created {glossary_name} but no GUID returned[/yellow]")
671
688
 
672
689
  except Exception as e:
673
- console.print(f"[red] Failed to create {glossary_name}:[/red] {str(e)}")
690
+ console.print(f"[red] Failed to create {glossary_name}:[/red] {str(e)}")
674
691
 
675
692
  console.print(f"\n[cyan]Created {created_count} new glossaries[/cyan]")
676
693
  console.print("[dim]Run 'pvw uc glossary list' to see all glossaries[/dim]")
@@ -756,7 +773,7 @@ def verify_glossary_links():
756
773
  domain_id[:8] + "...",
757
774
  glossary_info["name"],
758
775
  glossary_info["guid"][:8] + "...",
759
- "[green] Linked[/green]"
776
+ "[green] Linked[/green]"
760
777
  )
761
778
  linked_count += 1
762
779
  else:
@@ -765,7 +782,7 @@ def verify_glossary_links():
765
782
  domain_id[:8] + "...",
766
783
  "[dim]No glossary[/dim]",
767
784
  "[dim]N/A[/dim]",
768
- "[yellow] Not Linked[/yellow]"
785
+ "[yellow] Not Linked[/yellow]"
769
786
  )
770
787
  unlinked_count += 1
771
788
 
@@ -848,7 +865,7 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
848
865
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
849
866
  return
850
867
 
851
- console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
868
+ console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
852
869
  console.print(json.dumps(result, indent=2))
853
870
 
854
871
  except Exception as e:
@@ -857,9 +874,20 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
857
874
 
858
875
  @term.command(name="list")
859
876
  @click.option("--domain-id", required=True, help="Governance domain ID to list terms from")
860
- @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
861
- def list_terms(domain_id, output_json):
862
- """List all Unified Catalog terms in a governance domain."""
877
+ @click.option(
878
+ "--output",
879
+ type=click.Choice(["table", "json", "jsonc"]),
880
+ default="table",
881
+ help="Output format: table (default, formatted), json (plain, parseable), jsonc (colored JSON)"
882
+ )
883
+ def list_terms(domain_id, output):
884
+ """List all Unified Catalog terms in a governance domain.
885
+
886
+ Output formats:
887
+ - table: Formatted table output with Rich (default)
888
+ - json: Plain JSON for scripting (use with PowerShell ConvertFrom-Json)
889
+ - jsonc: Colored JSON with syntax highlighting for viewing
890
+ """
863
891
  try:
864
892
  client = UnifiedCatalogClient()
865
893
  args = {"--governance-domain-id": [domain_id]}
@@ -884,8 +912,13 @@ def list_terms(domain_id, output_json):
884
912
  console.print("[yellow]No terms found.[/yellow]")
885
913
  return
886
914
 
887
- # Output in JSON format if requested
888
- if output_json:
915
+ # Handle output format
916
+ if output == "json":
917
+ # Plain JSON for scripting (PowerShell compatible)
918
+ print(json.dumps(all_terms, indent=2))
919
+ return
920
+ elif output == "jsonc":
921
+ # Colored JSON for viewing
889
922
  _format_json_output(all_terms)
890
923
  return
891
924
 
@@ -993,7 +1026,7 @@ def delete(term_id, force):
993
1026
  gclient = Glossary()
994
1027
  result = gclient.glossaryDeleteTerm({"--termGuid": term_id})
995
1028
 
996
- console.print(f"[green] SUCCESS:[/green] Deleted term with ID: {term_id}")
1029
+ console.print(f"[green] SUCCESS:[/green] Deleted term with ID: {term_id}")
997
1030
 
998
1031
  except Exception as e:
999
1032
  console.print(f"[red]ERROR:[/red] {str(e)}")
@@ -1070,13 +1103,547 @@ def update(term_id, name, description, domain_id, status, acronym, owner_id, res
1070
1103
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1071
1104
  return
1072
1105
 
1073
- console.print(f"[green] SUCCESS:[/green] Updated glossary term '{term_id}'")
1106
+ console.print(f"[green] SUCCESS:[/green] Updated glossary term '{term_id}'")
1074
1107
  console.print(json.dumps(result, indent=2))
1075
1108
 
1076
1109
  except Exception as e:
1077
1110
  console.print(f"[red]ERROR:[/red] {str(e)}")
1078
1111
 
1079
1112
 
1113
+ @term.command(name="import-csv")
1114
+ @click.option("--csv-file", required=True, type=click.Path(exists=True), help="Path to CSV file with terms")
1115
+ @click.option("--domain-id", required=True, help="Governance domain ID for all terms")
1116
+ @click.option("--dry-run", is_flag=True, help="Preview terms without creating them")
1117
+ def import_terms_from_csv(csv_file, domain_id, dry_run):
1118
+ """Bulk import glossary terms from a CSV file.
1119
+
1120
+ CSV Format:
1121
+ name,description,status,acronyms,owner_ids,resource_name,resource_url
1122
+
1123
+ - name: Required term name
1124
+ - description: Optional description
1125
+ - status: Draft, Published, or Archived (default: Draft)
1126
+ - acronyms: Comma-separated list (e.g., "API,REST")
1127
+ - owner_ids: Comma-separated list of Entra Object IDs
1128
+ - resource_name: Name of related resource
1129
+ - resource_url: URL of related resource
1130
+
1131
+ Multiple resources can be specified by separating with semicolons.
1132
+ """
1133
+ try:
1134
+ client = UnifiedCatalogClient()
1135
+
1136
+ # Read and parse CSV
1137
+ terms = []
1138
+ with open(csv_file, 'r', encoding='utf-8') as f:
1139
+ reader = csv.DictReader(f)
1140
+ for row in reader:
1141
+ term = {
1142
+ "name": row.get("name", "").strip(),
1143
+ "description": row.get("description", "").strip(),
1144
+ "status": row.get("status", "Draft").strip(),
1145
+ "domain_id": domain_id,
1146
+ "acronyms": [],
1147
+ "owner_ids": [],
1148
+ "resources": []
1149
+ }
1150
+
1151
+ # Parse acronyms
1152
+ if row.get("acronyms"):
1153
+ term["acronyms"] = [a.strip() for a in row["acronyms"].split(",") if a.strip()]
1154
+
1155
+ # Parse owner IDs
1156
+ if row.get("owner_ids"):
1157
+ term["owner_ids"] = [o.strip() for o in row["owner_ids"].split(",") if o.strip()]
1158
+
1159
+ # Parse resources
1160
+ resource_names = row.get("resource_name", "").strip()
1161
+ resource_urls = row.get("resource_url", "").strip()
1162
+
1163
+ if resource_names and resource_urls:
1164
+ names = [n.strip() for n in resource_names.split(";") if n.strip()]
1165
+ urls = [u.strip() for u in resource_urls.split(";") if u.strip()]
1166
+ term["resources"] = [{"name": n, "url": u} for n, u in zip(names, urls)]
1167
+
1168
+ if term["name"]: # Only add if name is present
1169
+ terms.append(term)
1170
+
1171
+ if not terms:
1172
+ console.print("[yellow]No valid terms found in CSV file.[/yellow]")
1173
+ return
1174
+
1175
+ console.print(f"[cyan]Found {len(terms)} term(s) in CSV file[/cyan]")
1176
+
1177
+ if dry_run:
1178
+ console.print("\n[yellow]DRY RUN - Preview of terms to be created:[/yellow]\n")
1179
+ table = Table(title="Terms to Import")
1180
+ table.add_column("#", style="dim", width=4)
1181
+ table.add_column("Name", style="cyan")
1182
+ table.add_column("Status", style="yellow")
1183
+ table.add_column("Acronyms", style="magenta")
1184
+ table.add_column("Owners", style="green")
1185
+
1186
+ for i, term in enumerate(terms, 1):
1187
+ acronyms = ", ".join(term.get("acronyms", []))
1188
+ owners = ", ".join(term.get("owner_ids", []))
1189
+ table.add_row(
1190
+ str(i),
1191
+ term["name"],
1192
+ term["status"],
1193
+ acronyms or "-",
1194
+ owners or "-"
1195
+ )
1196
+
1197
+ console.print(table)
1198
+ console.print(f"\n[dim]Domain ID: {domain_id}[/dim]")
1199
+ return
1200
+
1201
+ # Import terms (one by one using single POST)
1202
+ success_count = 0
1203
+ failed_count = 0
1204
+ failed_terms = []
1205
+
1206
+ with console.status("[bold green]Importing terms...") as status:
1207
+ for i, term in enumerate(terms, 1):
1208
+ status.update(f"[bold green]Creating term {i}/{len(terms)}: {term['name']}")
1209
+
1210
+ try:
1211
+ # Create individual term
1212
+ args = {
1213
+ "--name": [term["name"]],
1214
+ "--description": [term.get("description", "")],
1215
+ "--governance-domain-id": [term["domain_id"]],
1216
+ "--status": [term.get("status", "Draft")],
1217
+ }
1218
+
1219
+ if term.get("acronyms"):
1220
+ args["--acronym"] = term["acronyms"]
1221
+
1222
+ if term.get("owner_ids"):
1223
+ args["--owner-id"] = term["owner_ids"]
1224
+
1225
+ if term.get("resources"):
1226
+ args["--resource-name"] = [r["name"] for r in term["resources"]]
1227
+ args["--resource-url"] = [r["url"] for r in term["resources"]]
1228
+
1229
+ result = client.create_term(args)
1230
+
1231
+ # Check if result contains an ID (indicates successful creation)
1232
+ if result and isinstance(result, dict) and result.get("id"):
1233
+ success_count += 1
1234
+ term_id = result.get("id")
1235
+ console.print(f"[green]Created: {term['name']} (ID: {term_id})[/green]")
1236
+ elif result and not (isinstance(result, dict) and "error" in result):
1237
+ # Got a response but no ID - might be an issue
1238
+ console.print(f"[yellow]WARNING: Response received for {term['name']} but no ID returned[/yellow]")
1239
+ console.print(f"[dim]Response: {json.dumps(result, indent=2)[:200]}...[/dim]")
1240
+ failed_count += 1
1241
+ failed_terms.append({"name": term["name"], "error": "No ID in response"})
1242
+ else:
1243
+ failed_count += 1
1244
+ error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else "No response"
1245
+ failed_terms.append({"name": term["name"], "error": error_msg})
1246
+ console.print(f"[red]FAILED: {term['name']} - {error_msg}[/red]")
1247
+
1248
+ except Exception as e:
1249
+ failed_count += 1
1250
+ failed_terms.append({"name": term["name"], "error": str(e)})
1251
+ console.print(f"[red]FAILED: {term['name']} - {str(e)}[/red]")
1252
+
1253
+ # Summary
1254
+ console.print("\n" + "="*60)
1255
+ console.print(f"[cyan]Import Summary:[/cyan]")
1256
+ console.print(f" Total terms: {len(terms)}")
1257
+ console.print(f" [green]Successfully created: {success_count}[/green]")
1258
+ console.print(f" [red]Failed: {failed_count}[/red]")
1259
+
1260
+ if failed_terms:
1261
+ console.print("\n[red]Failed Terms:[/red]")
1262
+ for ft in failed_terms:
1263
+ console.print(f" • {ft['name']}: {ft['error']}")
1264
+
1265
+ except Exception as e:
1266
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1267
+
1268
+
1269
+ @term.command(name="import-json")
1270
+ @click.option("--json-file", required=True, type=click.Path(exists=True), help="Path to JSON file with terms")
1271
+ @click.option("--dry-run", is_flag=True, help="Preview terms without creating them")
1272
+ def import_terms_from_json(json_file, dry_run):
1273
+ """Bulk import glossary terms from a JSON file.
1274
+
1275
+ JSON Format:
1276
+ [
1277
+ {
1278
+ "name": "Term Name",
1279
+ "description": "Description",
1280
+ "domain_id": "domain-guid",
1281
+ "status": "Draft",
1282
+ "acronyms": ["API", "REST"],
1283
+ "owner_ids": ["owner-guid-1"],
1284
+ "resources": [
1285
+ {"name": "Resource Name", "url": "https://example.com"}
1286
+ ]
1287
+ }
1288
+ ]
1289
+
1290
+ Each term must include domain_id.
1291
+ """
1292
+ try:
1293
+ client = UnifiedCatalogClient()
1294
+
1295
+ # Read and parse JSON
1296
+ with open(json_file, 'r', encoding='utf-8') as f:
1297
+ terms = json.load(f)
1298
+
1299
+ if not isinstance(terms, list):
1300
+ console.print("[red]ERROR:[/red] JSON file must contain an array of terms")
1301
+ return
1302
+
1303
+ if not terms:
1304
+ console.print("[yellow]No terms found in JSON file.[/yellow]")
1305
+ return
1306
+
1307
+ console.print(f"[cyan]Found {len(terms)} term(s) in JSON file[/cyan]")
1308
+
1309
+ if dry_run:
1310
+ console.print("\n[yellow]DRY RUN - Preview of terms to be created:[/yellow]\n")
1311
+ _format_json_output(terms)
1312
+ return
1313
+
1314
+ # Import terms
1315
+ success_count = 0
1316
+ failed_count = 0
1317
+ failed_terms = []
1318
+
1319
+ with console.status("[bold green]Importing terms...") as status:
1320
+ for i, term in enumerate(terms, 1):
1321
+ term_name = term.get("name", f"Term {i}")
1322
+ status.update(f"[bold green]Creating term {i}/{len(terms)}: {term_name}")
1323
+
1324
+ try:
1325
+ args = {
1326
+ "--name": [term.get("name", "")],
1327
+ "--description": [term.get("description", "")],
1328
+ "--governance-domain-id": [term.get("domain_id", "")],
1329
+ "--status": [term.get("status", "Draft")],
1330
+ }
1331
+
1332
+ if term.get("acronyms"):
1333
+ args["--acronym"] = term["acronyms"]
1334
+
1335
+ if term.get("owner_ids"):
1336
+ args["--owner-id"] = term["owner_ids"]
1337
+
1338
+ if term.get("resources"):
1339
+ args["--resource-name"] = [r.get("name", "") for r in term["resources"]]
1340
+ args["--resource-url"] = [r.get("url", "") for r in term["resources"]]
1341
+
1342
+ result = client.create_term(args)
1343
+
1344
+ # Check if result contains an ID (indicates successful creation)
1345
+ if result and isinstance(result, dict) and result.get("id"):
1346
+ success_count += 1
1347
+ term_id = result.get("id")
1348
+ console.print(f"[green]Created: {term_name} (ID: {term_id})[/green]")
1349
+ elif result and not (isinstance(result, dict) and "error" in result):
1350
+ # Got a response but no ID - might be an issue
1351
+ console.print(f"[yellow]WARNING: Response received for {term_name} but no ID returned[/yellow]")
1352
+ console.print(f"[dim]Response: {json.dumps(result, indent=2)[:200]}...[/dim]")
1353
+ failed_count += 1
1354
+ failed_terms.append({"name": term_name, "error": "No ID in response"})
1355
+ else:
1356
+ failed_count += 1
1357
+ error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else "No response"
1358
+ failed_terms.append({"name": term_name, "error": error_msg})
1359
+ console.print(f"[red]FAILED: {term_name} - {error_msg}[/red]")
1360
+
1361
+ except Exception as e:
1362
+ failed_count += 1
1363
+ failed_terms.append({"name": term_name, "error": str(e)})
1364
+ console.print(f"[red]FAILED: {term_name} - {str(e)}[/red]")
1365
+
1366
+ # Summary
1367
+ console.print("\n" + "="*60)
1368
+ console.print(f"[cyan]Import Summary:[/cyan]")
1369
+ console.print(f" Total terms: {len(terms)}")
1370
+ console.print(f" [green]Successfully created: {success_count}[/green]")
1371
+ console.print(f" [red]Failed: {failed_count}[/red]")
1372
+
1373
+ if failed_terms:
1374
+ console.print("\n[red]Failed Terms:[/red]")
1375
+ for ft in failed_terms:
1376
+ console.print(f" • {ft['name']}: {ft['error']}")
1377
+
1378
+ except Exception as e:
1379
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1380
+
1381
+
1382
+ @term.command(name="update-csv")
1383
+ @click.option("--csv-file", required=True, type=click.Path(exists=True), help="Path to CSV file with term updates")
1384
+ @click.option("--dry-run", is_flag=True, help="Preview updates without applying them")
1385
+ def update_terms_from_csv(csv_file, dry_run):
1386
+ """Bulk update glossary terms from a CSV file.
1387
+
1388
+ CSV Format:
1389
+ term_id,name,description,status,acronyms,owner_ids,add_acronyms,add_owner_ids
1390
+
1391
+ Required:
1392
+ - term_id: The ID of the term to update
1393
+
1394
+ Optional (leave empty to skip update):
1395
+ - name: New term name (replaces existing)
1396
+ - description: New description (replaces existing)
1397
+ - status: New status (Draft, Published, Archived)
1398
+ - acronyms: New acronyms separated by semicolons (replaces all existing)
1399
+ - owner_ids: New owner IDs separated by semicolons (replaces all existing)
1400
+ - add_acronyms: Acronyms to add separated by semicolons (preserves existing)
1401
+ - add_owner_ids: Owner IDs to add separated by semicolons (preserves existing)
1402
+
1403
+ Example CSV:
1404
+ term_id,name,description,status,add_acronyms,add_owner_ids
1405
+ abc-123,,Updated description,Published,API;REST,user1@company.com
1406
+ def-456,New Name,,,SQL,
1407
+ """
1408
+ import csv
1409
+
1410
+ try:
1411
+ # Read CSV file
1412
+ with open(csv_file, 'r', encoding='utf-8') as f:
1413
+ reader = csv.DictReader(f)
1414
+ updates = list(reader)
1415
+
1416
+ if not updates:
1417
+ console.print("[yellow]No updates found in CSV file.[/yellow]")
1418
+ return
1419
+
1420
+ console.print(f"Found {len(updates)} term(s) to update in CSV file")
1421
+
1422
+ # Dry run preview
1423
+ if dry_run:
1424
+ console.print("\n[cyan]DRY RUN - Preview of updates to be applied:[/cyan]\n")
1425
+
1426
+ table = Table(title="Terms to Update")
1427
+ table.add_column("#", style="cyan")
1428
+ table.add_column("Term ID", style="yellow")
1429
+ table.add_column("Updates", style="white")
1430
+
1431
+ for idx, update in enumerate(updates, 1):
1432
+ term_id = update.get('term_id', '').strip()
1433
+ if not term_id:
1434
+ continue
1435
+
1436
+ changes = []
1437
+ if update.get('name', '').strip():
1438
+ changes.append(f"name: {update['name']}")
1439
+ if update.get('description', '').strip():
1440
+ changes.append(f"desc: {update['description'][:50]}...")
1441
+ if update.get('status', '').strip():
1442
+ changes.append(f"status: {update['status']}")
1443
+ if update.get('acronyms', '').strip():
1444
+ changes.append(f"acronyms: {update['acronyms']}")
1445
+ if update.get('add_acronyms', '').strip():
1446
+ changes.append(f"add acronyms: {update['add_acronyms']}")
1447
+ if update.get('owner_ids', '').strip():
1448
+ changes.append(f"owners: {update['owner_ids']}")
1449
+ if update.get('add_owner_ids', '').strip():
1450
+ changes.append(f"add owners: {update['add_owner_ids']}")
1451
+
1452
+ table.add_row(str(idx), term_id[:36], ", ".join(changes) if changes else "No changes")
1453
+
1454
+ console.print(table)
1455
+ console.print(f"\n[yellow]Total terms to update: {len(updates)}[/yellow]")
1456
+ return
1457
+
1458
+ # Apply updates
1459
+ console.print("\n[cyan]Updating terms...[/cyan]\n")
1460
+
1461
+ client = UnifiedCatalogClient()
1462
+ success_count = 0
1463
+ failed_count = 0
1464
+ failed_terms = []
1465
+
1466
+ for idx, update in enumerate(updates, 1):
1467
+ term_id = update.get('term_id', '').strip()
1468
+ if not term_id:
1469
+ console.print(f"[yellow]Skipping row {idx}: Missing term_id[/yellow]")
1470
+ continue
1471
+
1472
+ # Build update arguments
1473
+ args = {"--term-id": [term_id]}
1474
+
1475
+ # Add replace operations
1476
+ if update.get('name', '').strip():
1477
+ args['--name'] = [update['name'].strip()]
1478
+ if update.get('description', '').strip():
1479
+ args['--description'] = [update['description'].strip()]
1480
+ if update.get('status', '').strip():
1481
+ args['--status'] = [update['status'].strip()]
1482
+ if update.get('acronyms', '').strip():
1483
+ args['--acronym'] = [a.strip() for a in update['acronyms'].split(';') if a.strip()]
1484
+ if update.get('owner_ids', '').strip():
1485
+ args['--owner-id'] = [o.strip() for o in update['owner_ids'].split(';') if o.strip()]
1486
+
1487
+ # Add "add" operations
1488
+ if update.get('add_acronyms', '').strip():
1489
+ args['--add-acronym'] = [a.strip() for a in update['add_acronyms'].split(';') if a.strip()]
1490
+ if update.get('add_owner_ids', '').strip():
1491
+ args['--add-owner-id'] = [o.strip() for o in update['add_owner_ids'].split(';') if o.strip()]
1492
+
1493
+ # Display progress
1494
+ display_name = update.get('name', term_id[:36])
1495
+ console.status(f"[{idx}/{len(updates)}] Updating: {display_name}...")
1496
+
1497
+ try:
1498
+ result = client.update_term(args)
1499
+ console.print(f"[green]SUCCESS:[/green] Updated term {idx}/{len(updates)}")
1500
+ success_count += 1
1501
+ except Exception as e:
1502
+ error_msg = str(e)
1503
+ console.print(f"[red]FAILED:[/red] {display_name}: {error_msg}")
1504
+ failed_terms.append({'term_id': term_id, 'name': display_name, 'error': error_msg})
1505
+ failed_count += 1
1506
+
1507
+ # Rate limiting
1508
+ time.sleep(0.2)
1509
+
1510
+ # Summary
1511
+ console.print("\n" + "="*60)
1512
+ console.print(f"[cyan]Update Summary:[/cyan]")
1513
+ console.print(f" Total terms: {len(updates)}")
1514
+ console.print(f" [green]Successfully updated: {success_count}[/green]")
1515
+ console.print(f" [red]Failed: {failed_count}[/red]")
1516
+
1517
+ if failed_terms:
1518
+ console.print("\n[red]Failed Updates:[/red]")
1519
+ for ft in failed_terms:
1520
+ console.print(f" • {ft['name']}: {ft['error']}")
1521
+
1522
+ except Exception as e:
1523
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1524
+
1525
+
1526
+ @term.command(name="update-json")
1527
+ @click.option("--json-file", required=True, type=click.Path(exists=True), help="Path to JSON file with term updates")
1528
+ @click.option("--dry-run", is_flag=True, help="Preview updates without applying them")
1529
+ def update_terms_from_json(json_file, dry_run):
1530
+ """Bulk update glossary terms from a JSON file.
1531
+
1532
+ JSON Format:
1533
+ {
1534
+ "updates": [
1535
+ {
1536
+ "term_id": "term-guid",
1537
+ "name": "New Name", // Optional: Replace name
1538
+ "description": "New description", // Optional: Replace description
1539
+ "status": "Published", // Optional: Change status
1540
+ "acronyms": ["API", "REST"], // Optional: Replace all acronyms
1541
+ "owner_ids": ["user@company.com"], // Optional: Replace all owners
1542
+ "add_acronyms": ["SQL"], // Optional: Add acronyms (preserves existing)
1543
+ "add_owner_ids": ["user2@company.com"] // Optional: Add owners (preserves existing)
1544
+ }
1545
+ ]
1546
+ }
1547
+
1548
+ Note: Leave fields empty or omit them to skip that update.
1549
+ """
1550
+ import json
1551
+
1552
+ try:
1553
+ # Read JSON file
1554
+ with open(json_file, 'r', encoding='utf-8') as f:
1555
+ data = json.load(f)
1556
+
1557
+ updates = data.get('updates', [])
1558
+
1559
+ if not updates:
1560
+ console.print("[yellow]No updates found in JSON file.[/yellow]")
1561
+ return
1562
+
1563
+ console.print(f"Found {len(updates)} term(s) to update in JSON file")
1564
+
1565
+ # Dry run preview
1566
+ if dry_run:
1567
+ console.print("\n[cyan]DRY RUN - Preview of updates to be applied:[/cyan]\n")
1568
+
1569
+ # Display updates in colored JSON
1570
+ from rich.syntax import Syntax
1571
+ json_str = json.dumps(data, indent=2)
1572
+ syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
1573
+ console.print(syntax)
1574
+
1575
+ console.print(f"\n[yellow]Total terms to update: {len(updates)}[/yellow]")
1576
+ return
1577
+
1578
+ # Apply updates
1579
+ console.print("\n[cyan]Updating terms...[/cyan]\n")
1580
+
1581
+ client = UnifiedCatalogClient()
1582
+ success_count = 0
1583
+ failed_count = 0
1584
+ failed_terms = []
1585
+
1586
+ for idx, update in enumerate(updates, 1):
1587
+ term_id = update.get('term_id', '').strip() if isinstance(update.get('term_id'), str) else ''
1588
+ if not term_id:
1589
+ console.print(f"[yellow]Skipping update {idx}: Missing term_id[/yellow]")
1590
+ continue
1591
+
1592
+ # Build update arguments
1593
+ args = {"--term-id": [term_id]}
1594
+
1595
+ # Add replace operations
1596
+ if update.get('name'):
1597
+ args['--name'] = [update['name']]
1598
+ if update.get('description'):
1599
+ args['--description'] = [update['description']]
1600
+ if update.get('status'):
1601
+ args['--status'] = [update['status']]
1602
+ if update.get('acronyms'):
1603
+ args['--acronym'] = update['acronyms'] if isinstance(update['acronyms'], list) else [update['acronyms']]
1604
+ if update.get('owner_ids'):
1605
+ args['--owner-id'] = update['owner_ids'] if isinstance(update['owner_ids'], list) else [update['owner_ids']]
1606
+
1607
+ # Add "add" operations
1608
+ if update.get('add_acronyms'):
1609
+ args['--add-acronym'] = update['add_acronyms'] if isinstance(update['add_acronyms'], list) else [update['add_acronyms']]
1610
+ if update.get('add_owner_ids'):
1611
+ args['--add-owner-id'] = update['add_owner_ids'] if isinstance(update['add_owner_ids'], list) else [update['add_owner_ids']]
1612
+
1613
+ # Display progress
1614
+ display_name = update.get('name', term_id[:36])
1615
+ console.status(f"[{idx}/{len(updates)}] Updating: {display_name}...")
1616
+
1617
+ try:
1618
+ result = client.update_term(args)
1619
+ console.print(f"[green]SUCCESS:[/green] Updated term {idx}/{len(updates)}")
1620
+ success_count += 1
1621
+ except Exception as e:
1622
+ error_msg = str(e)
1623
+ console.print(f"[red]FAILED:[/red] {display_name}: {error_msg}")
1624
+ failed_terms.append({'term_id': term_id, 'name': display_name, 'error': error_msg})
1625
+ failed_count += 1
1626
+
1627
+ # Rate limiting
1628
+ time.sleep(0.2)
1629
+
1630
+ # Summary
1631
+ console.print("\n" + "="*60)
1632
+ console.print(f"[cyan]Update Summary:[/cyan]")
1633
+ console.print(f" Total terms: {len(updates)}")
1634
+ console.print(f" [green]Successfully updated: {success_count}[/green]")
1635
+ console.print(f" [red]Failed: {failed_count}[/red]")
1636
+
1637
+ if failed_terms:
1638
+ console.print("\n[red]Failed Updates:[/red]")
1639
+ for ft in failed_terms:
1640
+ console.print(f" • {ft['name']}: {ft['error']}")
1641
+
1642
+ except Exception as e:
1643
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1644
+
1645
+
1646
+
1080
1647
  # ========================================
1081
1648
  # OBJECTIVES AND KEY RESULTS (OKRs)
1082
1649
  # ========================================
@@ -1132,7 +1699,7 @@ def create(definition, domain_id, status, owner_id, target_date):
1132
1699
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1133
1700
  return
1134
1701
 
1135
- console.print(f"[green] SUCCESS:[/green] Created objective")
1702
+ console.print(f"[green] SUCCESS:[/green] Created objective")
1136
1703
  console.print(json.dumps(result, indent=2))
1137
1704
 
1138
1705
  except Exception as e:
@@ -1275,7 +1842,7 @@ def create(name, description, domain_id, data_type, status, owner_id):
1275
1842
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1276
1843
  return
1277
1844
 
1278
- console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
1845
+ console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
1279
1846
  console.print(json.dumps(result, indent=2))
1280
1847
 
1281
1848
  except Exception as e:
@@ -1362,7 +1929,7 @@ def show(cde_id):
1362
1929
 
1363
1930
 
1364
1931
  # ========================================
1365
- # HEALTH MANAGEMENT - IMPLEMENTED!
1932
+ # HEALTH MANAGEMENT - IMPLEMENTED!
1366
1933
  # ========================================
1367
1934
 
1368
1935
  # Import and register health commands from dedicated module