pvw-cli 1.0.11__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
@@ -51,8 +52,7 @@ def domain():
51
52
  "--type",
52
53
  required=False,
53
54
  default="FunctionalUnit",
54
- type=click.Choice(["FunctionalUnit", "BusinessUnit", "Department"]),
55
- help="Type of governance domain",
55
+ help="Type of governance domain (default: FunctionalUnit). Note: UC API currently only accepts 'FunctionalUnit'.",
56
56
  )
57
57
  @click.option(
58
58
  "--owner-id",
@@ -67,19 +67,46 @@ def domain():
67
67
  type=click.Choice(["Draft", "Published", "Archived"]),
68
68
  help="Status of the governance domain",
69
69
  )
70
- def create(name, description, type, owner_id, status):
70
+ @click.option(
71
+ "--parent-id",
72
+ required=False,
73
+ help="Parent governance domain ID (create as subdomain under this domain)",
74
+ )
75
+ @click.option(
76
+ "--payload-file",
77
+ required=False,
78
+ type=click.Path(exists=True),
79
+ help="Optional JSON payload file to use for creating the domain (overrides flags if provided)",
80
+ )
81
+ def create(name, description, type, owner_id, status, parent_id, payload_file):
71
82
  """Create a new governance domain."""
72
83
  try:
73
84
  client = UnifiedCatalogClient()
74
85
 
75
86
  # Build args dictionary in Purview CLI format
76
- args = {
77
- "--name": [name],
78
- "--description": [description],
79
- "--type": [type],
80
- "--status": [status],
81
- }
82
-
87
+ # If payload-file is provided we will let the client read the file directly
88
+ # otherwise build args from individual flags.
89
+ args = {}
90
+ # Note: click will pass None for owner_id if not provided, but multiple=True returns ()
91
+ # We'll only include values if payload-file not used.
92
+ if locals().get('payload_file'):
93
+ args = {"--payloadFile": locals().get('payload_file')}
94
+ else:
95
+ args = {
96
+ "--name": [name],
97
+ "--description": [description],
98
+ "--type": [type],
99
+ "--status": [status],
100
+ }
101
+ if owner_id:
102
+ args["--owner-id"] = list(owner_id)
103
+ # include parent id if provided
104
+ parent_id = locals().get('parent_id')
105
+ if parent_id:
106
+ # use a consistent arg name for client lookup
107
+ args["--parent-domain-id"] = [parent_id]
108
+
109
+ # Call the client to create the governance domain
83
110
  result = client.create_governance_domain(args)
84
111
 
85
112
  if not result:
@@ -89,7 +116,7 @@ def create(name, description, type, owner_id, status):
89
116
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
90
117
  return
91
118
 
92
- console.print(f"[green] SUCCESS:[/green] Created governance domain '{name}'")
119
+ console.print(f"[green] SUCCESS:[/green] Created governance domain '{name}'")
93
120
  console.print(json.dumps(result, indent=2))
94
121
 
95
122
  except Exception as e:
@@ -97,9 +124,20 @@ def create(name, description, type, owner_id, status):
97
124
 
98
125
 
99
126
  @domain.command(name="list")
100
- @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
101
- def list_domains(output_json):
102
- """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
+ """
103
141
  try:
104
142
  client = UnifiedCatalogClient()
105
143
  args = {} # No arguments needed for list operation
@@ -121,8 +159,13 @@ def list_domains(output_json):
121
159
  console.print("[yellow]No governance domains found.[/yellow]")
122
160
  return
123
161
 
124
- # Output in JSON format if requested
125
- 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
126
169
  _format_json_output(domains)
127
170
  return
128
171
 
@@ -248,7 +291,7 @@ def create(
248
291
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
249
292
  return
250
293
 
251
- console.print(f"[green] SUCCESS:[/green] Created data product '{name}'")
294
+ console.print(f"[green] SUCCESS:[/green] Created data product '{name}'")
252
295
  console.print(json.dumps(result, indent=2))
253
296
 
254
297
  except Exception as e:
@@ -418,7 +461,7 @@ def update(
418
461
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
419
462
  return
420
463
 
421
- console.print(f"[green] SUCCESS:[/green] Updated data product '{product_id}'")
464
+ console.print(f"[green] SUCCESS:[/green] Updated data product '{product_id}'")
422
465
  console.print(json.dumps(result, indent=2))
423
466
 
424
467
  except Exception as e:
@@ -446,11 +489,11 @@ def delete(product_id, yes):
446
489
 
447
490
  # DELETE operations may return empty response on success
448
491
  if result is None or (isinstance(result, dict) and not result.get("error")):
449
- console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
492
+ console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
450
493
  elif isinstance(result, dict) and "error" in result:
451
494
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
452
495
  else:
453
- console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
496
+ console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
454
497
  if result:
455
498
  console.print(json.dumps(result, indent=2))
456
499
 
@@ -553,7 +596,7 @@ def create_glossary(name, description, domain_id):
553
596
  return
554
597
 
555
598
  guid = result.get("guid") if isinstance(result, dict) else None
556
- console.print(f"[green] SUCCESS:[/green] Created glossary '{name}'")
599
+ console.print(f"[green] SUCCESS:[/green] Created glossary '{name}'")
557
600
  if guid:
558
601
  console.print(f"[cyan]GUID:[/cyan] {guid}")
559
602
  console.print(f"\n[dim]Use this GUID: --glossary-guid {guid}[/dim]")
@@ -638,13 +681,13 @@ def create_glossaries_for_domains():
638
681
  guid = result.get("guid") if isinstance(result, dict) else None
639
682
 
640
683
  if guid:
641
- console.print(f"[green] Created:[/green] {glossary_name} (GUID: {guid})")
684
+ console.print(f"[green] Created:[/green] {glossary_name} (GUID: {guid})")
642
685
  created_count += 1
643
686
  else:
644
- 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]")
645
688
 
646
689
  except Exception as e:
647
- 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)}")
648
691
 
649
692
  console.print(f"\n[cyan]Created {created_count} new glossaries[/cyan]")
650
693
  console.print("[dim]Run 'pvw uc glossary list' to see all glossaries[/dim]")
@@ -730,7 +773,7 @@ def verify_glossary_links():
730
773
  domain_id[:8] + "...",
731
774
  glossary_info["name"],
732
775
  glossary_info["guid"][:8] + "...",
733
- "[green] Linked[/green]"
776
+ "[green] Linked[/green]"
734
777
  )
735
778
  linked_count += 1
736
779
  else:
@@ -739,7 +782,7 @@ def verify_glossary_links():
739
782
  domain_id[:8] + "...",
740
783
  "[dim]No glossary[/dim]",
741
784
  "[dim]N/A[/dim]",
742
- "[yellow] Not Linked[/yellow]"
785
+ "[yellow] Not Linked[/yellow]"
743
786
  )
744
787
  unlinked_count += 1
745
788
 
@@ -822,7 +865,7 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
822
865
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
823
866
  return
824
867
 
825
- console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
868
+ console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
826
869
  console.print(json.dumps(result, indent=2))
827
870
 
828
871
  except Exception as e:
@@ -831,9 +874,20 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
831
874
 
832
875
  @term.command(name="list")
833
876
  @click.option("--domain-id", required=True, help="Governance domain ID to list terms from")
834
- @click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
835
- def list_terms(domain_id, output_json):
836
- """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
+ """
837
891
  try:
838
892
  client = UnifiedCatalogClient()
839
893
  args = {"--governance-domain-id": [domain_id]}
@@ -858,8 +912,13 @@ def list_terms(domain_id, output_json):
858
912
  console.print("[yellow]No terms found.[/yellow]")
859
913
  return
860
914
 
861
- # Output in JSON format if requested
862
- 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
863
922
  _format_json_output(all_terms)
864
923
  return
865
924
 
@@ -967,12 +1026,624 @@ def delete(term_id, force):
967
1026
  gclient = Glossary()
968
1027
  result = gclient.glossaryDeleteTerm({"--termGuid": term_id})
969
1028
 
970
- 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}")
1030
+
1031
+ except Exception as e:
1032
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1033
+
1034
+
1035
+ @term.command()
1036
+ @click.option("--term-id", required=True, help="ID of the glossary term to update")
1037
+ @click.option("--name", required=False, help="Name of the glossary term")
1038
+ @click.option("--description", required=False, help="Rich text description of the term")
1039
+ @click.option("--domain-id", required=False, help="Governance domain ID")
1040
+ @click.option(
1041
+ "--status",
1042
+ required=False,
1043
+ type=click.Choice(["Draft", "Published", "Archived"]),
1044
+ help="Status of the term",
1045
+ )
1046
+ @click.option(
1047
+ "--acronym",
1048
+ required=False,
1049
+ help="Acronyms for the term (can be specified multiple times, replaces existing)",
1050
+ multiple=True,
1051
+ )
1052
+ @click.option(
1053
+ "--owner-id",
1054
+ required=False,
1055
+ help="Owner Entra ID (can be specified multiple times, replaces existing)",
1056
+ multiple=True,
1057
+ )
1058
+ @click.option("--resource-name", required=False, help="Resource name for additional reading (can be specified multiple times, replaces existing)", multiple=True)
1059
+ @click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times, replaces existing)", multiple=True)
1060
+ @click.option("--add-acronym", required=False, help="Add acronym to existing ones (can be specified multiple times)", multiple=True)
1061
+ @click.option("--add-owner-id", required=False, help="Add owner to existing ones (can be specified multiple times)", multiple=True)
1062
+ def update(term_id, name, description, domain_id, status, acronym, owner_id, resource_name, resource_url, add_acronym, add_owner_id):
1063
+ """Update an existing Unified Catalog term."""
1064
+ try:
1065
+ client = UnifiedCatalogClient()
1066
+
1067
+ # Build args dictionary - only include provided values
1068
+ args = {"--term-id": [term_id]}
1069
+
1070
+ if name:
1071
+ args["--name"] = [name]
1072
+ if description is not None: # Allow empty string
1073
+ args["--description"] = [description]
1074
+ if domain_id:
1075
+ args["--governance-domain-id"] = [domain_id]
1076
+ if status:
1077
+ args["--status"] = [status]
1078
+
1079
+ # Handle acronyms - either replace or add
1080
+ if acronym:
1081
+ args["--acronym"] = list(acronym)
1082
+ elif add_acronym:
1083
+ args["--add-acronym"] = list(add_acronym)
1084
+
1085
+ # Handle owners - either replace or add
1086
+ if owner_id:
1087
+ args["--owner-id"] = list(owner_id)
1088
+ elif add_owner_id:
1089
+ args["--add-owner-id"] = list(add_owner_id)
1090
+
1091
+ # Handle resources
1092
+ if resource_name:
1093
+ args["--resource-name"] = list(resource_name)
1094
+ if resource_url:
1095
+ args["--resource-url"] = list(resource_url)
1096
+
1097
+ result = client.update_term(args)
1098
+
1099
+ if not result:
1100
+ console.print("[red]ERROR:[/red] No response received")
1101
+ return
1102
+ if isinstance(result, dict) and "error" in result:
1103
+ console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1104
+ return
1105
+
1106
+ console.print(f"[green] SUCCESS:[/green] Updated glossary term '{term_id}'")
1107
+ console.print(json.dumps(result, indent=2))
1108
+
1109
+ except Exception as e:
1110
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1111
+
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']}")
971
1641
 
972
1642
  except Exception as e:
973
1643
  console.print(f"[red]ERROR:[/red] {str(e)}")
974
1644
 
975
1645
 
1646
+
976
1647
  # ========================================
977
1648
  # OBJECTIVES AND KEY RESULTS (OKRs)
978
1649
  # ========================================
@@ -1028,7 +1699,7 @@ def create(definition, domain_id, status, owner_id, target_date):
1028
1699
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1029
1700
  return
1030
1701
 
1031
- console.print(f"[green] SUCCESS:[/green] Created objective")
1702
+ console.print(f"[green] SUCCESS:[/green] Created objective")
1032
1703
  console.print(json.dumps(result, indent=2))
1033
1704
 
1034
1705
  except Exception as e:
@@ -1171,7 +1842,7 @@ def create(name, description, domain_id, data_type, status, owner_id):
1171
1842
  console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1172
1843
  return
1173
1844
 
1174
- console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
1845
+ console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
1175
1846
  console.print(json.dumps(result, indent=2))
1176
1847
 
1177
1848
  except Exception as e:
@@ -1258,7 +1929,7 @@ def show(cde_id):
1258
1929
 
1259
1930
 
1260
1931
  # ========================================
1261
- # HEALTH MANAGEMENT - IMPLEMENTED!
1932
+ # HEALTH MANAGEMENT - IMPLEMENTED!
1262
1933
  # ========================================
1263
1934
 
1264
1935
  # Import and register health commands from dedicated module