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.
- purviewcli/__init__.py +2 -2
- purviewcli/cli/account.py +24 -24
- purviewcli/cli/cli.py +16 -16
- purviewcli/cli/collections.py +24 -24
- purviewcli/cli/domain.py +8 -8
- purviewcli/cli/entity.py +193 -193
- purviewcli/cli/glossary.py +29 -29
- purviewcli/cli/health.py +2 -2
- purviewcli/cli/insight.py +7 -7
- purviewcli/cli/lineage.py +11 -11
- purviewcli/cli/unified_catalog.py +594 -27
- purviewcli/cli/workflow.py +43 -43
- {pvw_cli-1.0.12.dist-info → pvw_cli-1.0.14.dist-info}/METADATA +378 -58
- {pvw_cli-1.0.12.dist-info → pvw_cli-1.0.14.dist-info}/RECORD +17 -17
- {pvw_cli-1.0.12.dist-info → pvw_cli-1.0.14.dist-info}/WHEEL +0 -0
- {pvw_cli-1.0.12.dist-info → pvw_cli-1.0.14.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.0.12.dist-info → pvw_cli-1.0.14.dist-info}/top_level.txt +0 -0
|
@@ -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]
|
|
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(
|
|
127
|
-
|
|
128
|
-
"""
|
|
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
|
-
#
|
|
151
|
-
if
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
684
|
+
console.print(f"[green] Created:[/green] {glossary_name} (GUID: {guid})")
|
|
668
685
|
created_count += 1
|
|
669
686
|
else:
|
|
670
|
-
console.print(f"[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]
|
|
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]
|
|
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]
|
|
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]
|
|
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(
|
|
861
|
-
|
|
862
|
-
"""
|
|
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
|
-
#
|
|
888
|
-
if
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|