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

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
 
@@ -796,6 +813,7 @@ def term():
796
813
  @click.option("--name", required=True, help="Name of the glossary term")
797
814
  @click.option("--description", required=False, default="", help="Rich text description of the term")
798
815
  @click.option("--domain-id", required=True, help="Governance domain ID")
816
+ @click.option("--parent-id", required=False, help="Parent term ID (for hierarchical terms)")
799
817
  @click.option(
800
818
  "--status",
801
819
  required=False,
@@ -817,7 +835,7 @@ def term():
817
835
  )
818
836
  @click.option("--resource-name", required=False, help="Resource name for additional reading (can be specified multiple times)", multiple=True)
819
837
  @click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times)", multiple=True)
820
- def create(name, description, domain_id, status, acronym, owner_id, resource_name, resource_url):
838
+ def create(name, description, domain_id, parent_id, status, acronym, owner_id, resource_name, resource_url):
821
839
  """Create a new Unified Catalog term (Governance Domain term)."""
822
840
  try:
823
841
  client = UnifiedCatalogClient()
@@ -830,6 +848,8 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
830
848
  "--status": [status],
831
849
  }
832
850
 
851
+ if parent_id:
852
+ args["--parent-id"] = [parent_id]
833
853
  if acronym:
834
854
  args["--acronym"] = list(acronym)
835
855
  if owner_id:
@@ -848,7 +868,7 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
848
868
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
849
869
  return
850
870
 
851
- console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
871
+ console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
852
872
  console.print(json.dumps(result, indent=2))
853
873
 
854
874
  except Exception as e:
@@ -857,9 +877,20 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
857
877
 
858
878
  @term.command(name="list")
859
879
  @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."""
880
+ @click.option(
881
+ "--output",
882
+ type=click.Choice(["table", "json", "jsonc"]),
883
+ default="table",
884
+ help="Output format: table (default, formatted), json (plain, parseable), jsonc (colored JSON)"
885
+ )
886
+ def list_terms(domain_id, output):
887
+ """List all Unified Catalog terms in a governance domain.
888
+
889
+ Output formats:
890
+ - table: Formatted table output with Rich (default)
891
+ - json: Plain JSON for scripting (use with PowerShell ConvertFrom-Json)
892
+ - jsonc: Colored JSON with syntax highlighting for viewing
893
+ """
863
894
  try:
864
895
  client = UnifiedCatalogClient()
865
896
  args = {"--governance-domain-id": [domain_id]}
@@ -884,8 +915,13 @@ def list_terms(domain_id, output_json):
884
915
  console.print("[yellow]No terms found.[/yellow]")
885
916
  return
886
917
 
887
- # Output in JSON format if requested
888
- if output_json:
918
+ # Handle output format
919
+ if output == "json":
920
+ # Plain JSON for scripting (PowerShell compatible)
921
+ print(json.dumps(all_terms, indent=2))
922
+ return
923
+ elif output == "jsonc":
924
+ # Colored JSON for viewing
889
925
  _format_json_output(all_terms)
890
926
  return
891
927
 
@@ -993,7 +1029,7 @@ def delete(term_id, force):
993
1029
  gclient = Glossary()
994
1030
  result = gclient.glossaryDeleteTerm({"--termGuid": term_id})
995
1031
 
996
- console.print(f"[green] SUCCESS:[/green] Deleted term with ID: {term_id}")
1032
+ console.print(f"[green] SUCCESS:[/green] Deleted term with ID: {term_id}")
997
1033
 
998
1034
  except Exception as e:
999
1035
  console.print(f"[red]ERROR:[/red] {str(e)}")
@@ -1004,6 +1040,7 @@ def delete(term_id, force):
1004
1040
  @click.option("--name", required=False, help="Name of the glossary term")
1005
1041
  @click.option("--description", required=False, help="Rich text description of the term")
1006
1042
  @click.option("--domain-id", required=False, help="Governance domain ID")
1043
+ @click.option("--parent-id", required=False, help="Parent term ID (for hierarchical terms)")
1007
1044
  @click.option(
1008
1045
  "--status",
1009
1046
  required=False,
@@ -1026,7 +1063,7 @@ def delete(term_id, force):
1026
1063
  @click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times, replaces existing)", multiple=True)
1027
1064
  @click.option("--add-acronym", required=False, help="Add acronym to existing ones (can be specified multiple times)", multiple=True)
1028
1065
  @click.option("--add-owner-id", required=False, help="Add owner to existing ones (can be specified multiple times)", multiple=True)
1029
- def update(term_id, name, description, domain_id, status, acronym, owner_id, resource_name, resource_url, add_acronym, add_owner_id):
1066
+ def update(term_id, name, description, domain_id, parent_id, status, acronym, owner_id, resource_name, resource_url, add_acronym, add_owner_id):
1030
1067
  """Update an existing Unified Catalog term."""
1031
1068
  try:
1032
1069
  client = UnifiedCatalogClient()
@@ -1040,6 +1077,8 @@ def update(term_id, name, description, domain_id, status, acronym, owner_id, res
1040
1077
  args["--description"] = [description]
1041
1078
  if domain_id:
1042
1079
  args["--governance-domain-id"] = [domain_id]
1080
+ if parent_id:
1081
+ args["--parent-id"] = [parent_id]
1043
1082
  if status:
1044
1083
  args["--status"] = [status]
1045
1084
 
@@ -1070,13 +1109,555 @@ def update(term_id, name, description, domain_id, status, acronym, owner_id, res
1070
1109
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1071
1110
  return
1072
1111
 
1073
- console.print(f"[green] SUCCESS:[/green] Updated glossary term '{term_id}'")
1112
+ console.print(f"[green] SUCCESS:[/green] Updated glossary term '{term_id}'")
1074
1113
  console.print(json.dumps(result, indent=2))
1075
1114
 
1076
1115
  except Exception as e:
1077
1116
  console.print(f"[red]ERROR:[/red] {str(e)}")
1078
1117
 
1079
1118
 
1119
+ @term.command(name="import-csv")
1120
+ @click.option("--csv-file", required=True, type=click.Path(exists=True), help="Path to CSV file with terms")
1121
+ @click.option("--domain-id", required=True, help="Governance domain ID for all terms")
1122
+ @click.option("--dry-run", is_flag=True, help="Preview terms without creating them")
1123
+ def import_terms_from_csv(csv_file, domain_id, dry_run):
1124
+ """Bulk import glossary terms from a CSV file.
1125
+
1126
+ CSV Format:
1127
+ name,description,status,acronyms,owner_ids,resource_name,resource_url
1128
+
1129
+ - name: Required term name
1130
+ - description: Optional description
1131
+ - status: Draft, Published, or Archived (default: Draft)
1132
+ - acronyms: Comma-separated list (e.g., "API,REST")
1133
+ - owner_ids: Comma-separated list of Entra Object IDs
1134
+ - resource_name: Name of related resource
1135
+ - resource_url: URL of related resource
1136
+
1137
+ Multiple resources can be specified by separating with semicolons.
1138
+ """
1139
+ try:
1140
+ client = UnifiedCatalogClient()
1141
+
1142
+ # Read and parse CSV
1143
+ terms = []
1144
+ with open(csv_file, 'r', encoding='utf-8') as f:
1145
+ reader = csv.DictReader(f)
1146
+ for row in reader:
1147
+ term = {
1148
+ "name": row.get("name", "").strip(),
1149
+ "description": row.get("description", "").strip(),
1150
+ "status": row.get("status", "Draft").strip(),
1151
+ "domain_id": domain_id,
1152
+ "acronyms": [],
1153
+ "owner_ids": [],
1154
+ "resources": []
1155
+ }
1156
+
1157
+ # Parse acronyms
1158
+ if row.get("acronyms"):
1159
+ term["acronyms"] = [a.strip() for a in row["acronyms"].split(",") if a.strip()]
1160
+
1161
+ # Parse owner IDs
1162
+ if row.get("owner_ids"):
1163
+ term["owner_ids"] = [o.strip() for o in row["owner_ids"].split(",") if o.strip()]
1164
+
1165
+ # Parse resources
1166
+ resource_names = row.get("resource_name", "").strip()
1167
+ resource_urls = row.get("resource_url", "").strip()
1168
+
1169
+ if resource_names and resource_urls:
1170
+ names = [n.strip() for n in resource_names.split(";") if n.strip()]
1171
+ urls = [u.strip() for u in resource_urls.split(";") if u.strip()]
1172
+ term["resources"] = [{"name": n, "url": u} for n, u in zip(names, urls)]
1173
+
1174
+ if term["name"]: # Only add if name is present
1175
+ terms.append(term)
1176
+
1177
+ if not terms:
1178
+ console.print("[yellow]No valid terms found in CSV file.[/yellow]")
1179
+ return
1180
+
1181
+ console.print(f"[cyan]Found {len(terms)} term(s) in CSV file[/cyan]")
1182
+
1183
+ if dry_run:
1184
+ console.print("\n[yellow]DRY RUN - Preview of terms to be created:[/yellow]\n")
1185
+ table = Table(title="Terms to Import")
1186
+ table.add_column("#", style="dim", width=4)
1187
+ table.add_column("Name", style="cyan")
1188
+ table.add_column("Status", style="yellow")
1189
+ table.add_column("Acronyms", style="magenta")
1190
+ table.add_column("Owners", style="green")
1191
+
1192
+ for i, term in enumerate(terms, 1):
1193
+ acronyms = ", ".join(term.get("acronyms", []))
1194
+ owners = ", ".join(term.get("owner_ids", []))
1195
+ table.add_row(
1196
+ str(i),
1197
+ term["name"],
1198
+ term["status"],
1199
+ acronyms or "-",
1200
+ owners or "-"
1201
+ )
1202
+
1203
+ console.print(table)
1204
+ console.print(f"\n[dim]Domain ID: {domain_id}[/dim]")
1205
+ return
1206
+
1207
+ # Import terms (one by one using single POST)
1208
+ success_count = 0
1209
+ failed_count = 0
1210
+ failed_terms = []
1211
+
1212
+ with console.status("[bold green]Importing terms...") as status:
1213
+ for i, term in enumerate(terms, 1):
1214
+ status.update(f"[bold green]Creating term {i}/{len(terms)}: {term['name']}")
1215
+
1216
+ try:
1217
+ # Create individual term
1218
+ args = {
1219
+ "--name": [term["name"]],
1220
+ "--description": [term.get("description", "")],
1221
+ "--governance-domain-id": [term["domain_id"]],
1222
+ "--status": [term.get("status", "Draft")],
1223
+ }
1224
+
1225
+ if term.get("acronyms"):
1226
+ args["--acronym"] = term["acronyms"]
1227
+
1228
+ if term.get("owner_ids"):
1229
+ args["--owner-id"] = term["owner_ids"]
1230
+
1231
+ if term.get("resources"):
1232
+ args["--resource-name"] = [r["name"] for r in term["resources"]]
1233
+ args["--resource-url"] = [r["url"] for r in term["resources"]]
1234
+
1235
+ result = client.create_term(args)
1236
+
1237
+ # Check if result contains an ID (indicates successful creation)
1238
+ if result and isinstance(result, dict) and result.get("id"):
1239
+ success_count += 1
1240
+ term_id = result.get("id")
1241
+ console.print(f"[green]Created: {term['name']} (ID: {term_id})[/green]")
1242
+ elif result and not (isinstance(result, dict) and "error" in result):
1243
+ # Got a response but no ID - might be an issue
1244
+ console.print(f"[yellow]WARNING: Response received for {term['name']} but no ID returned[/yellow]")
1245
+ console.print(f"[dim]Response: {json.dumps(result, indent=2)[:200]}...[/dim]")
1246
+ failed_count += 1
1247
+ failed_terms.append({"name": term["name"], "error": "No ID in response"})
1248
+ else:
1249
+ failed_count += 1
1250
+ error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else "No response"
1251
+ failed_terms.append({"name": term["name"], "error": error_msg})
1252
+ console.print(f"[red]FAILED: {term['name']} - {error_msg}[/red]")
1253
+
1254
+ except Exception as e:
1255
+ failed_count += 1
1256
+ failed_terms.append({"name": term["name"], "error": str(e)})
1257
+ console.print(f"[red]FAILED: {term['name']} - {str(e)}[/red]")
1258
+
1259
+ # Summary
1260
+ console.print("\n" + "="*60)
1261
+ console.print(f"[cyan]Import Summary:[/cyan]")
1262
+ console.print(f" Total terms: {len(terms)}")
1263
+ console.print(f" [green]Successfully created: {success_count}[/green]")
1264
+ console.print(f" [red]Failed: {failed_count}[/red]")
1265
+
1266
+ if failed_terms:
1267
+ console.print("\n[red]Failed Terms:[/red]")
1268
+ for ft in failed_terms:
1269
+ console.print(f" • {ft['name']}: {ft['error']}")
1270
+
1271
+ except Exception as e:
1272
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1273
+
1274
+
1275
+ @term.command(name="import-json")
1276
+ @click.option("--json-file", required=True, type=click.Path(exists=True), help="Path to JSON file with terms")
1277
+ @click.option("--dry-run", is_flag=True, help="Preview terms without creating them")
1278
+ def import_terms_from_json(json_file, dry_run):
1279
+ """Bulk import glossary terms from a JSON file.
1280
+
1281
+ JSON Format:
1282
+ [
1283
+ {
1284
+ "name": "Term Name",
1285
+ "description": "Description",
1286
+ "domain_id": "domain-guid",
1287
+ "status": "Draft",
1288
+ "acronyms": ["API", "REST"],
1289
+ "owner_ids": ["owner-guid-1"],
1290
+ "resources": [
1291
+ {"name": "Resource Name", "url": "https://example.com"}
1292
+ ]
1293
+ }
1294
+ ]
1295
+
1296
+ Each term must include domain_id.
1297
+ """
1298
+ try:
1299
+ client = UnifiedCatalogClient()
1300
+
1301
+ # Read and parse JSON
1302
+ with open(json_file, 'r', encoding='utf-8') as f:
1303
+ terms = json.load(f)
1304
+
1305
+ if not isinstance(terms, list):
1306
+ console.print("[red]ERROR:[/red] JSON file must contain an array of terms")
1307
+ return
1308
+
1309
+ if not terms:
1310
+ console.print("[yellow]No terms found in JSON file.[/yellow]")
1311
+ return
1312
+
1313
+ console.print(f"[cyan]Found {len(terms)} term(s) in JSON file[/cyan]")
1314
+
1315
+ if dry_run:
1316
+ console.print("\n[yellow]DRY RUN - Preview of terms to be created:[/yellow]\n")
1317
+ _format_json_output(terms)
1318
+ return
1319
+
1320
+ # Import terms
1321
+ success_count = 0
1322
+ failed_count = 0
1323
+ failed_terms = []
1324
+
1325
+ with console.status("[bold green]Importing terms...") as status:
1326
+ for i, term in enumerate(terms, 1):
1327
+ term_name = term.get("name", f"Term {i}")
1328
+ status.update(f"[bold green]Creating term {i}/{len(terms)}: {term_name}")
1329
+
1330
+ try:
1331
+ args = {
1332
+ "--name": [term.get("name", "")],
1333
+ "--description": [term.get("description", "")],
1334
+ "--governance-domain-id": [term.get("domain_id", "")],
1335
+ "--status": [term.get("status", "Draft")],
1336
+ }
1337
+
1338
+ if term.get("acronyms"):
1339
+ args["--acronym"] = term["acronyms"]
1340
+
1341
+ if term.get("owner_ids"):
1342
+ args["--owner-id"] = term["owner_ids"]
1343
+
1344
+ if term.get("resources"):
1345
+ args["--resource-name"] = [r.get("name", "") for r in term["resources"]]
1346
+ args["--resource-url"] = [r.get("url", "") for r in term["resources"]]
1347
+
1348
+ result = client.create_term(args)
1349
+
1350
+ # Check if result contains an ID (indicates successful creation)
1351
+ if result and isinstance(result, dict) and result.get("id"):
1352
+ success_count += 1
1353
+ term_id = result.get("id")
1354
+ console.print(f"[green]Created: {term_name} (ID: {term_id})[/green]")
1355
+ elif result and not (isinstance(result, dict) and "error" in result):
1356
+ # Got a response but no ID - might be an issue
1357
+ console.print(f"[yellow]WARNING: Response received for {term_name} but no ID returned[/yellow]")
1358
+ console.print(f"[dim]Response: {json.dumps(result, indent=2)[:200]}...[/dim]")
1359
+ failed_count += 1
1360
+ failed_terms.append({"name": term_name, "error": "No ID in response"})
1361
+ else:
1362
+ failed_count += 1
1363
+ error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else "No response"
1364
+ failed_terms.append({"name": term_name, "error": error_msg})
1365
+ console.print(f"[red]FAILED: {term_name} - {error_msg}[/red]")
1366
+
1367
+ except Exception as e:
1368
+ failed_count += 1
1369
+ failed_terms.append({"name": term_name, "error": str(e)})
1370
+ console.print(f"[red]FAILED: {term_name} - {str(e)}[/red]")
1371
+
1372
+ # Summary
1373
+ console.print("\n" + "="*60)
1374
+ console.print(f"[cyan]Import Summary:[/cyan]")
1375
+ console.print(f" Total terms: {len(terms)}")
1376
+ console.print(f" [green]Successfully created: {success_count}[/green]")
1377
+ console.print(f" [red]Failed: {failed_count}[/red]")
1378
+
1379
+ if failed_terms:
1380
+ console.print("\n[red]Failed Terms:[/red]")
1381
+ for ft in failed_terms:
1382
+ console.print(f" • {ft['name']}: {ft['error']}")
1383
+
1384
+ except Exception as e:
1385
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1386
+
1387
+
1388
+ @term.command(name="update-csv")
1389
+ @click.option("--csv-file", required=True, type=click.Path(exists=True), help="Path to CSV file with term updates")
1390
+ @click.option("--dry-run", is_flag=True, help="Preview updates without applying them")
1391
+ def update_terms_from_csv(csv_file, dry_run):
1392
+ """Bulk update glossary terms from a CSV file.
1393
+
1394
+ CSV Format:
1395
+ term_id,name,description,status,parent_id,acronyms,owner_ids,add_acronyms,add_owner_ids
1396
+
1397
+ Required:
1398
+ - term_id: The ID of the term to update
1399
+
1400
+ Optional (leave empty to skip update):
1401
+ - name: New term name (replaces existing)
1402
+ - description: New description (replaces existing)
1403
+ - status: New status (Draft, Published, Archived)
1404
+ - parent_id: Parent term ID for hierarchical relationships (replaces existing)
1405
+ - acronyms: New acronyms separated by semicolons (replaces all existing)
1406
+ - owner_ids: New owner IDs separated by semicolons (replaces all existing)
1407
+ - add_acronyms: Acronyms to add separated by semicolons (preserves existing)
1408
+ - add_owner_ids: Owner IDs to add separated by semicolons (preserves existing)
1409
+
1410
+ Example CSV:
1411
+ term_id,name,description,status,parent_id,add_acronyms,add_owner_ids
1412
+ abc-123,,Updated description,Published,parent-term-guid,API;REST,user1@company.com
1413
+ def-456,New Name,,,parent-term-guid,SQL,
1414
+ """
1415
+ import csv
1416
+
1417
+ try:
1418
+ # Read CSV file
1419
+ with open(csv_file, 'r', encoding='utf-8') as f:
1420
+ reader = csv.DictReader(f)
1421
+ updates = list(reader)
1422
+
1423
+ if not updates:
1424
+ console.print("[yellow]No updates found in CSV file.[/yellow]")
1425
+ return
1426
+
1427
+ console.print(f"Found {len(updates)} term(s) to update in CSV file")
1428
+
1429
+ # Dry run preview
1430
+ if dry_run:
1431
+ console.print("\n[cyan]DRY RUN - Preview of updates to be applied:[/cyan]\n")
1432
+
1433
+ table = Table(title="Terms to Update")
1434
+ table.add_column("#", style="cyan")
1435
+ table.add_column("Term ID", style="yellow")
1436
+ table.add_column("Updates", style="white")
1437
+
1438
+ for idx, update in enumerate(updates, 1):
1439
+ term_id = update.get('term_id', '').strip()
1440
+ if not term_id:
1441
+ continue
1442
+
1443
+ changes = []
1444
+ if update.get('name', '').strip():
1445
+ changes.append(f"name: {update['name']}")
1446
+ if update.get('description', '').strip():
1447
+ changes.append(f"desc: {update['description'][:50]}...")
1448
+ if update.get('status', '').strip():
1449
+ changes.append(f"status: {update['status']}")
1450
+ if update.get('parent_id', '').strip():
1451
+ changes.append(f"parent: {update['parent_id'][:20]}...")
1452
+ if update.get('acronyms', '').strip():
1453
+ changes.append(f"acronyms: {update['acronyms']}")
1454
+ if update.get('add_acronyms', '').strip():
1455
+ changes.append(f"add acronyms: {update['add_acronyms']}")
1456
+ if update.get('owner_ids', '').strip():
1457
+ changes.append(f"owners: {update['owner_ids']}")
1458
+ if update.get('add_owner_ids', '').strip():
1459
+ changes.append(f"add owners: {update['add_owner_ids']}")
1460
+
1461
+ table.add_row(str(idx), term_id[:36], ", ".join(changes) if changes else "No changes")
1462
+
1463
+ console.print(table)
1464
+ console.print(f"\n[yellow]Total terms to update: {len(updates)}[/yellow]")
1465
+ return
1466
+
1467
+ # Apply updates
1468
+ console.print("\n[cyan]Updating terms...[/cyan]\n")
1469
+
1470
+ client = UnifiedCatalogClient()
1471
+ success_count = 0
1472
+ failed_count = 0
1473
+ failed_terms = []
1474
+
1475
+ for idx, update in enumerate(updates, 1):
1476
+ term_id = update.get('term_id', '').strip()
1477
+ if not term_id:
1478
+ console.print(f"[yellow]Skipping row {idx}: Missing term_id[/yellow]")
1479
+ continue
1480
+
1481
+ # Build update arguments
1482
+ args = {"--term-id": [term_id]}
1483
+
1484
+ # Add replace operations
1485
+ if update.get('name', '').strip():
1486
+ args['--name'] = [update['name'].strip()]
1487
+ if update.get('description', '').strip():
1488
+ args['--description'] = [update['description'].strip()]
1489
+ if update.get('status', '').strip():
1490
+ args['--status'] = [update['status'].strip()]
1491
+ if update.get('parent_id', '').strip():
1492
+ args['--parent-id'] = [update['parent_id'].strip()]
1493
+ if update.get('acronyms', '').strip():
1494
+ args['--acronym'] = [a.strip() for a in update['acronyms'].split(';') if a.strip()]
1495
+ if update.get('owner_ids', '').strip():
1496
+ args['--owner-id'] = [o.strip() for o in update['owner_ids'].split(';') if o.strip()]
1497
+
1498
+ # Add "add" operations
1499
+ if update.get('add_acronyms', '').strip():
1500
+ args['--add-acronym'] = [a.strip() for a in update['add_acronyms'].split(';') if a.strip()]
1501
+ if update.get('add_owner_ids', '').strip():
1502
+ args['--add-owner-id'] = [o.strip() for o in update['add_owner_ids'].split(';') if o.strip()]
1503
+
1504
+ # Display progress
1505
+ display_name = update.get('name', term_id[:36])
1506
+ console.status(f"[{idx}/{len(updates)}] Updating: {display_name}...")
1507
+
1508
+ try:
1509
+ result = client.update_term(args)
1510
+ console.print(f"[green]SUCCESS:[/green] Updated term {idx}/{len(updates)}")
1511
+ success_count += 1
1512
+ except Exception as e:
1513
+ error_msg = str(e)
1514
+ console.print(f"[red]FAILED:[/red] {display_name}: {error_msg}")
1515
+ failed_terms.append({'term_id': term_id, 'name': display_name, 'error': error_msg})
1516
+ failed_count += 1
1517
+
1518
+ # Rate limiting
1519
+ time.sleep(0.2)
1520
+
1521
+ # Summary
1522
+ console.print("\n" + "="*60)
1523
+ console.print(f"[cyan]Update Summary:[/cyan]")
1524
+ console.print(f" Total terms: {len(updates)}")
1525
+ console.print(f" [green]Successfully updated: {success_count}[/green]")
1526
+ console.print(f" [red]Failed: {failed_count}[/red]")
1527
+
1528
+ if failed_terms:
1529
+ console.print("\n[red]Failed Updates:[/red]")
1530
+ for ft in failed_terms:
1531
+ console.print(f" • {ft['name']}: {ft['error']}")
1532
+
1533
+ except Exception as e:
1534
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1535
+
1536
+
1537
+ @term.command(name="update-json")
1538
+ @click.option("--json-file", required=True, type=click.Path(exists=True), help="Path to JSON file with term updates")
1539
+ @click.option("--dry-run", is_flag=True, help="Preview updates without applying them")
1540
+ def update_terms_from_json(json_file, dry_run):
1541
+ """Bulk update glossary terms from a JSON file.
1542
+
1543
+ JSON Format:
1544
+ {
1545
+ "updates": [
1546
+ {
1547
+ "term_id": "term-guid",
1548
+ "name": "New Name", // Optional: Replace name
1549
+ "description": "New description", // Optional: Replace description
1550
+ "status": "Published", // Optional: Change status
1551
+ "parent_id": "parent-term-guid", // Optional: Set parent term (hierarchical)
1552
+ "acronyms": ["API", "REST"], // Optional: Replace all acronyms
1553
+ "owner_ids": ["user@company.com"], // Optional: Replace all owners
1554
+ "add_acronyms": ["SQL"], // Optional: Add acronyms (preserves existing)
1555
+ "add_owner_ids": ["user2@company.com"] // Optional: Add owners (preserves existing)
1556
+ }
1557
+ ]
1558
+ }
1559
+
1560
+ Note: Leave fields empty or omit them to skip that update.
1561
+ """
1562
+ import json
1563
+
1564
+ try:
1565
+ # Read JSON file
1566
+ with open(json_file, 'r', encoding='utf-8') as f:
1567
+ data = json.load(f)
1568
+
1569
+ updates = data.get('updates', [])
1570
+
1571
+ if not updates:
1572
+ console.print("[yellow]No updates found in JSON file.[/yellow]")
1573
+ return
1574
+
1575
+ console.print(f"Found {len(updates)} term(s) to update in JSON file")
1576
+
1577
+ # Dry run preview
1578
+ if dry_run:
1579
+ console.print("\n[cyan]DRY RUN - Preview of updates to be applied:[/cyan]\n")
1580
+
1581
+ # Display updates in colored JSON
1582
+ from rich.syntax import Syntax
1583
+ json_str = json.dumps(data, indent=2)
1584
+ syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
1585
+ console.print(syntax)
1586
+
1587
+ console.print(f"\n[yellow]Total terms to update: {len(updates)}[/yellow]")
1588
+ return
1589
+
1590
+ # Apply updates
1591
+ console.print("\n[cyan]Updating terms...[/cyan]\n")
1592
+
1593
+ client = UnifiedCatalogClient()
1594
+ success_count = 0
1595
+ failed_count = 0
1596
+ failed_terms = []
1597
+
1598
+ for idx, update in enumerate(updates, 1):
1599
+ term_id = update.get('term_id', '').strip() if isinstance(update.get('term_id'), str) else ''
1600
+ if not term_id:
1601
+ console.print(f"[yellow]Skipping update {idx}: Missing term_id[/yellow]")
1602
+ continue
1603
+
1604
+ # Build update arguments
1605
+ args = {"--term-id": [term_id]}
1606
+
1607
+ # Add replace operations
1608
+ if update.get('name'):
1609
+ args['--name'] = [update['name']]
1610
+ if update.get('description'):
1611
+ args['--description'] = [update['description']]
1612
+ if update.get('status'):
1613
+ args['--status'] = [update['status']]
1614
+ if update.get('parent_id'):
1615
+ args['--parent-id'] = [update['parent_id']]
1616
+ if update.get('acronyms'):
1617
+ args['--acronym'] = update['acronyms'] if isinstance(update['acronyms'], list) else [update['acronyms']]
1618
+ if update.get('owner_ids'):
1619
+ args['--owner-id'] = update['owner_ids'] if isinstance(update['owner_ids'], list) else [update['owner_ids']]
1620
+
1621
+ # Add "add" operations
1622
+ if update.get('add_acronyms'):
1623
+ args['--add-acronym'] = update['add_acronyms'] if isinstance(update['add_acronyms'], list) else [update['add_acronyms']]
1624
+ if update.get('add_owner_ids'):
1625
+ args['--add-owner-id'] = update['add_owner_ids'] if isinstance(update['add_owner_ids'], list) else [update['add_owner_ids']]
1626
+
1627
+ # Display progress
1628
+ display_name = update.get('name', term_id[:36])
1629
+ console.status(f"[{idx}/{len(updates)}] Updating: {display_name}...")
1630
+
1631
+ try:
1632
+ result = client.update_term(args)
1633
+ console.print(f"[green]SUCCESS:[/green] Updated term {idx}/{len(updates)}")
1634
+ success_count += 1
1635
+ except Exception as e:
1636
+ error_msg = str(e)
1637
+ console.print(f"[red]FAILED:[/red] {display_name}: {error_msg}")
1638
+ failed_terms.append({'term_id': term_id, 'name': display_name, 'error': error_msg})
1639
+ failed_count += 1
1640
+
1641
+ # Rate limiting
1642
+ time.sleep(0.2)
1643
+
1644
+ # Summary
1645
+ console.print("\n" + "="*60)
1646
+ console.print(f"[cyan]Update Summary:[/cyan]")
1647
+ console.print(f" Total terms: {len(updates)}")
1648
+ console.print(f" [green]Successfully updated: {success_count}[/green]")
1649
+ console.print(f" [red]Failed: {failed_count}[/red]")
1650
+
1651
+ if failed_terms:
1652
+ console.print("\n[red]Failed Updates:[/red]")
1653
+ for ft in failed_terms:
1654
+ console.print(f" • {ft['name']}: {ft['error']}")
1655
+
1656
+ except Exception as e:
1657
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1658
+
1659
+
1660
+
1080
1661
  # ========================================
1081
1662
  # OBJECTIVES AND KEY RESULTS (OKRs)
1082
1663
  # ========================================
@@ -1132,7 +1713,7 @@ def create(definition, domain_id, status, owner_id, target_date):
1132
1713
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1133
1714
  return
1134
1715
 
1135
- console.print(f"[green] SUCCESS:[/green] Created objective")
1716
+ console.print(f"[green] SUCCESS:[/green] Created objective")
1136
1717
  console.print(json.dumps(result, indent=2))
1137
1718
 
1138
1719
  except Exception as e:
@@ -1275,7 +1856,7 @@ def create(name, description, domain_id, data_type, status, owner_id):
1275
1856
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1276
1857
  return
1277
1858
 
1278
- console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
1859
+ console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
1279
1860
  console.print(json.dumps(result, indent=2))
1280
1861
 
1281
1862
  except Exception as e:
@@ -1362,7 +1943,7 @@ def show(cde_id):
1362
1943
 
1363
1944
 
1364
1945
  # ========================================
1365
- # HEALTH MANAGEMENT - IMPLEMENTED!
1946
+ # HEALTH MANAGEMENT - IMPLEMENTED!
1366
1947
  # ========================================
1367
1948
 
1368
1949
  # Import and register health commands from dedicated module