pvw-cli 1.2.8__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 +27 -0
- purviewcli/__main__.py +15 -0
- purviewcli/cli/__init__.py +5 -0
- purviewcli/cli/account.py +199 -0
- purviewcli/cli/cli.py +170 -0
- purviewcli/cli/collections.py +502 -0
- purviewcli/cli/domain.py +361 -0
- purviewcli/cli/entity.py +2436 -0
- purviewcli/cli/glossary.py +533 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/insight.py +113 -0
- purviewcli/cli/lineage.py +1103 -0
- purviewcli/cli/management.py +141 -0
- purviewcli/cli/policystore.py +103 -0
- purviewcli/cli/relationship.py +75 -0
- purviewcli/cli/scan.py +357 -0
- purviewcli/cli/search.py +527 -0
- purviewcli/cli/share.py +478 -0
- purviewcli/cli/types.py +831 -0
- purviewcli/cli/unified_catalog.py +3540 -0
- purviewcli/cli/workflow.py +402 -0
- purviewcli/client/__init__.py +21 -0
- purviewcli/client/_account.py +1877 -0
- purviewcli/client/_collections.py +1761 -0
- purviewcli/client/_domain.py +414 -0
- purviewcli/client/_entity.py +3545 -0
- purviewcli/client/_glossary.py +3233 -0
- purviewcli/client/_health.py +501 -0
- purviewcli/client/_insight.py +2873 -0
- purviewcli/client/_lineage.py +2138 -0
- purviewcli/client/_management.py +2202 -0
- purviewcli/client/_policystore.py +2915 -0
- purviewcli/client/_relationship.py +1351 -0
- purviewcli/client/_scan.py +2607 -0
- purviewcli/client/_search.py +1472 -0
- purviewcli/client/_share.py +272 -0
- purviewcli/client/_types.py +2708 -0
- purviewcli/client/_unified_catalog.py +5112 -0
- purviewcli/client/_workflow.py +2734 -0
- purviewcli/client/api_client.py +1295 -0
- purviewcli/client/business_rules.py +675 -0
- purviewcli/client/config.py +231 -0
- purviewcli/client/data_quality.py +433 -0
- purviewcli/client/endpoint.py +123 -0
- purviewcli/client/endpoints.py +554 -0
- purviewcli/client/exceptions.py +38 -0
- purviewcli/client/lineage_visualization.py +797 -0
- purviewcli/client/monitoring_dashboard.py +712 -0
- purviewcli/client/rate_limiter.py +30 -0
- purviewcli/client/retry_handler.py +125 -0
- purviewcli/client/scanning_operations.py +523 -0
- purviewcli/client/settings.py +1 -0
- purviewcli/client/sync_client.py +250 -0
- purviewcli/plugins/__init__.py +1 -0
- purviewcli/plugins/plugin_system.py +709 -0
- pvw_cli-1.2.8.dist-info/METADATA +1618 -0
- pvw_cli-1.2.8.dist-info/RECORD +60 -0
- pvw_cli-1.2.8.dist-info/WHEEL +5 -0
- pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
- pvw_cli-1.2.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,3540 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Purview Unified Catalog CLI Commands
|
|
3
|
+
Replaces data_product functionality with comprehensive Unified Catalog operations
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import tempfile
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
from purviewcli.client._unified_catalog import UnifiedCatalogClient
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _format_json_output(data):
|
|
22
|
+
"""Format JSON output with syntax highlighting using Rich"""
|
|
23
|
+
# Pretty print JSON with syntax highlighting
|
|
24
|
+
json_str = json.dumps(data, indent=2)
|
|
25
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
|
|
26
|
+
console.print(syntax)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
def uc():
|
|
31
|
+
"""Manage Unified Catalog in Microsoft Purview (domains, terms, data products, OKRs, CDEs)."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ========================================
|
|
36
|
+
# GOVERNANCE DOMAINS
|
|
37
|
+
# ========================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@uc.group()
|
|
41
|
+
def domain():
|
|
42
|
+
"""Manage governance domains."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@domain.command()
|
|
47
|
+
@click.option("--name", required=True, help="Name of the governance domain")
|
|
48
|
+
@click.option(
|
|
49
|
+
"--description", required=False, default="", help="Description of the governance domain"
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--type",
|
|
53
|
+
required=False,
|
|
54
|
+
default="FunctionalUnit",
|
|
55
|
+
help="Type of governance domain (default: FunctionalUnit). Note: UC API currently only accepts 'FunctionalUnit'.",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--owner-id",
|
|
59
|
+
required=False,
|
|
60
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
61
|
+
multiple=True,
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--status",
|
|
65
|
+
required=False,
|
|
66
|
+
default="Draft",
|
|
67
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
68
|
+
help="Status of the governance domain",
|
|
69
|
+
)
|
|
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):
|
|
82
|
+
"""Create a new governance domain."""
|
|
83
|
+
try:
|
|
84
|
+
client = UnifiedCatalogClient()
|
|
85
|
+
|
|
86
|
+
# Build args dictionary in Purview CLI format
|
|
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
|
|
110
|
+
result = client.create_governance_domain(args)
|
|
111
|
+
|
|
112
|
+
if not result:
|
|
113
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
114
|
+
return
|
|
115
|
+
if isinstance(result, dict) and "error" in result:
|
|
116
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
console.print(f"[green] SUCCESS:[/green] Created governance domain '{name}'")
|
|
120
|
+
console.print(json.dumps(result, indent=2))
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@domain.command(name="list")
|
|
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
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
client = UnifiedCatalogClient()
|
|
143
|
+
args = {} # No arguments needed for list operation
|
|
144
|
+
result = client.get_governance_domains(args)
|
|
145
|
+
|
|
146
|
+
if not result:
|
|
147
|
+
console.print("[yellow]No governance domains found.[/yellow]")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Handle both list and dict responses
|
|
151
|
+
if isinstance(result, (list, tuple)):
|
|
152
|
+
domains = result
|
|
153
|
+
elif isinstance(result, dict):
|
|
154
|
+
domains = result.get("value", [])
|
|
155
|
+
else:
|
|
156
|
+
domains = []
|
|
157
|
+
|
|
158
|
+
if not domains:
|
|
159
|
+
console.print("[yellow]No governance domains found.[/yellow]")
|
|
160
|
+
return
|
|
161
|
+
|
|
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
|
|
169
|
+
_format_json_output(domains)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
table = Table(title="Governance Domains")
|
|
173
|
+
table.add_column("ID", style="cyan")
|
|
174
|
+
table.add_column("Name", style="green")
|
|
175
|
+
table.add_column("Type", style="blue")
|
|
176
|
+
table.add_column("Status", style="yellow")
|
|
177
|
+
table.add_column("Owners", style="magenta")
|
|
178
|
+
|
|
179
|
+
for domain in domains:
|
|
180
|
+
owners = ", ".join(
|
|
181
|
+
[o.get("name", o.get("id", "Unknown")) for o in domain.get("owners", [])]
|
|
182
|
+
)
|
|
183
|
+
table.add_row(
|
|
184
|
+
domain.get("id", "N/A"),
|
|
185
|
+
domain.get("name", "N/A"),
|
|
186
|
+
domain.get("type", "N/A"),
|
|
187
|
+
domain.get("status", "N/A"),
|
|
188
|
+
owners or "None",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
console.print(table)
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@domain.command()
|
|
198
|
+
@click.option("--domain-id", required=True, help="ID of the governance domain")
|
|
199
|
+
def show(domain_id):
|
|
200
|
+
"""Show details of a governance domain."""
|
|
201
|
+
try:
|
|
202
|
+
client = UnifiedCatalogClient()
|
|
203
|
+
args = {"--domain-id": [domain_id]}
|
|
204
|
+
result = client.get_governance_domain_by_id(args)
|
|
205
|
+
|
|
206
|
+
if not result:
|
|
207
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
208
|
+
return
|
|
209
|
+
if isinstance(result, dict) and result.get("error"):
|
|
210
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Domain not found')}")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
console.print(json.dumps(result, indent=2))
|
|
214
|
+
except Exception as e:
|
|
215
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ========================================
|
|
219
|
+
# DATA PRODUCTS (for backwards compatibility)
|
|
220
|
+
# ========================================
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@uc.group()
|
|
224
|
+
def dataproduct():
|
|
225
|
+
"""Manage data products."""
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataproduct.command()
|
|
230
|
+
@click.option("--name", required=True, help="Name of the data product")
|
|
231
|
+
@click.option("--description", required=False, default="", help="Description of the data product")
|
|
232
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
233
|
+
@click.option(
|
|
234
|
+
"--type",
|
|
235
|
+
required=False,
|
|
236
|
+
default="Operational",
|
|
237
|
+
type=click.Choice(["Operational", "Analytical", "Reference"]),
|
|
238
|
+
help="Type of data product",
|
|
239
|
+
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--owner-id",
|
|
242
|
+
required=False,
|
|
243
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
244
|
+
multiple=True,
|
|
245
|
+
)
|
|
246
|
+
@click.option("--business-use", required=False, default="", help="Business use description")
|
|
247
|
+
@click.option(
|
|
248
|
+
"--update-frequency",
|
|
249
|
+
required=False,
|
|
250
|
+
default="Weekly",
|
|
251
|
+
type=click.Choice(["Daily", "Weekly", "Monthly", "Quarterly", "Annually"]),
|
|
252
|
+
help="Update frequency",
|
|
253
|
+
)
|
|
254
|
+
@click.option("--endorsed", is_flag=True, help="Mark as endorsed")
|
|
255
|
+
@click.option(
|
|
256
|
+
"--status",
|
|
257
|
+
required=False,
|
|
258
|
+
default="Draft",
|
|
259
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
260
|
+
help="Status of the data product",
|
|
261
|
+
)
|
|
262
|
+
def create(
|
|
263
|
+
name, description, domain_id, type, owner_id, business_use, update_frequency, endorsed, status
|
|
264
|
+
):
|
|
265
|
+
"""Create a new data product."""
|
|
266
|
+
try:
|
|
267
|
+
client = UnifiedCatalogClient()
|
|
268
|
+
owners = [{"id": oid} for oid in owner_id] if owner_id else []
|
|
269
|
+
|
|
270
|
+
# Build args dictionary in Purview CLI format
|
|
271
|
+
args = {
|
|
272
|
+
"--governance-domain-id": [domain_id],
|
|
273
|
+
"--name": [name],
|
|
274
|
+
"--description": [description],
|
|
275
|
+
"--type": [type],
|
|
276
|
+
"--status": [status],
|
|
277
|
+
"--business-use": [business_use],
|
|
278
|
+
"--update-frequency": [update_frequency],
|
|
279
|
+
}
|
|
280
|
+
if endorsed:
|
|
281
|
+
args["--endorsed"] = ["true"]
|
|
282
|
+
if owners:
|
|
283
|
+
args["--owner-id"] = [owner["id"] for owner in owners]
|
|
284
|
+
|
|
285
|
+
result = client.create_data_product(args)
|
|
286
|
+
|
|
287
|
+
if not result:
|
|
288
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
289
|
+
return
|
|
290
|
+
if isinstance(result, dict) and "error" in result:
|
|
291
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
console.print(f"[green] SUCCESS:[/green] Created data product '{name}'")
|
|
295
|
+
console.print(json.dumps(result, indent=2))
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataproduct.command(name="list")
|
|
302
|
+
@click.option("--domain-id", required=False, help="Governance domain ID (optional filter)")
|
|
303
|
+
@click.option("--status", required=False, help="Status filter (Draft, Published, Archived)")
|
|
304
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
305
|
+
def list_data_products(domain_id, status, output_json):
|
|
306
|
+
"""List all data products (optionally filtered by domain or status)."""
|
|
307
|
+
try:
|
|
308
|
+
client = UnifiedCatalogClient()
|
|
309
|
+
|
|
310
|
+
# Build args dictionary in Purview CLI format
|
|
311
|
+
args = {}
|
|
312
|
+
if domain_id:
|
|
313
|
+
args["--domain-id"] = [domain_id]
|
|
314
|
+
if status:
|
|
315
|
+
args["--status"] = [status]
|
|
316
|
+
|
|
317
|
+
result = client.get_data_products(args)
|
|
318
|
+
|
|
319
|
+
# Handle both list and dict responses
|
|
320
|
+
if isinstance(result, (list, tuple)):
|
|
321
|
+
products = result
|
|
322
|
+
elif isinstance(result, dict):
|
|
323
|
+
products = result.get("value", [])
|
|
324
|
+
else:
|
|
325
|
+
products = []
|
|
326
|
+
|
|
327
|
+
if not products:
|
|
328
|
+
filter_msg = ""
|
|
329
|
+
if domain_id:
|
|
330
|
+
filter_msg += f" in domain '{domain_id}'"
|
|
331
|
+
if status:
|
|
332
|
+
filter_msg += f" with status '{status}'"
|
|
333
|
+
console.print(f"[yellow]No data products found{filter_msg}.[/yellow]")
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
# Output in JSON format if requested
|
|
337
|
+
if output_json:
|
|
338
|
+
_format_json_output(products)
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
table = Table(title="Data Products")
|
|
342
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
343
|
+
table.add_column("Name", style="green")
|
|
344
|
+
table.add_column("Domain ID", style="blue", no_wrap=True)
|
|
345
|
+
table.add_column("Status", style="yellow")
|
|
346
|
+
table.add_column("Description", style="white", max_width=50)
|
|
347
|
+
|
|
348
|
+
for product in products:
|
|
349
|
+
# Get domain ID and handle "N/A" display
|
|
350
|
+
domain_id = product.get("domain") or product.get("domainId", "")
|
|
351
|
+
domain_display = domain_id if domain_id else "N/A"
|
|
352
|
+
|
|
353
|
+
# Clean HTML tags from description
|
|
354
|
+
description = product.get("description", "")
|
|
355
|
+
import re
|
|
356
|
+
description = re.sub(r'<[^>]+>', '', description)
|
|
357
|
+
description = description.strip()
|
|
358
|
+
|
|
359
|
+
table.add_row(
|
|
360
|
+
product.get("id", "N/A"),
|
|
361
|
+
product.get("name", "N/A"),
|
|
362
|
+
domain_display,
|
|
363
|
+
product.get("status", "N/A"),
|
|
364
|
+
(description[:50] + "...") if len(description) > 50 else description,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
console.print(table)
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@dataproduct.command()
|
|
374
|
+
@click.option("--product-id", required=True, help="ID of the data product")
|
|
375
|
+
def show(product_id):
|
|
376
|
+
"""Show details of a data product."""
|
|
377
|
+
try:
|
|
378
|
+
client = UnifiedCatalogClient()
|
|
379
|
+
args = {"--product-id": [product_id]}
|
|
380
|
+
result = client.get_data_product_by_id(args)
|
|
381
|
+
|
|
382
|
+
if not result:
|
|
383
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
384
|
+
return
|
|
385
|
+
if isinstance(result, dict) and "error" in result:
|
|
386
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Data product not found')}")
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
console.print(json.dumps(result, indent=2))
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@dataproduct.command()
|
|
396
|
+
@click.option("--product-id", required=True, help="ID of the data product to update")
|
|
397
|
+
@click.option("--name", required=False, help="Name of the data product")
|
|
398
|
+
@click.option("--description", required=False, help="Description of the data product")
|
|
399
|
+
@click.option("--domain-id", required=False, help="Governance domain ID")
|
|
400
|
+
@click.option(
|
|
401
|
+
"--type",
|
|
402
|
+
required=False,
|
|
403
|
+
type=click.Choice(["Operational", "Analytical", "Reference"]),
|
|
404
|
+
help="Type of data product",
|
|
405
|
+
)
|
|
406
|
+
@click.option(
|
|
407
|
+
"--owner-id",
|
|
408
|
+
required=False,
|
|
409
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
410
|
+
multiple=True,
|
|
411
|
+
)
|
|
412
|
+
@click.option("--business-use", required=False, help="Business use description")
|
|
413
|
+
@click.option(
|
|
414
|
+
"--update-frequency",
|
|
415
|
+
required=False,
|
|
416
|
+
type=click.Choice(["Daily", "Weekly", "Monthly", "Quarterly", "Annually"]),
|
|
417
|
+
help="Update frequency",
|
|
418
|
+
)
|
|
419
|
+
@click.option("--endorsed", is_flag=True, help="Mark as endorsed")
|
|
420
|
+
@click.option(
|
|
421
|
+
"--status",
|
|
422
|
+
required=False,
|
|
423
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
424
|
+
help="Status of the data product",
|
|
425
|
+
)
|
|
426
|
+
def update(
|
|
427
|
+
product_id, name, description, domain_id, type, owner_id, business_use, update_frequency, endorsed, status
|
|
428
|
+
):
|
|
429
|
+
"""Update an existing data product."""
|
|
430
|
+
try:
|
|
431
|
+
client = UnifiedCatalogClient()
|
|
432
|
+
|
|
433
|
+
# Build args dictionary - only include provided values
|
|
434
|
+
args = {"--product-id": [product_id]}
|
|
435
|
+
|
|
436
|
+
if name:
|
|
437
|
+
args["--name"] = [name]
|
|
438
|
+
if description is not None: # Allow empty string
|
|
439
|
+
args["--description"] = [description]
|
|
440
|
+
if domain_id:
|
|
441
|
+
args["--domain-id"] = [domain_id]
|
|
442
|
+
if type:
|
|
443
|
+
args["--type"] = [type]
|
|
444
|
+
if status:
|
|
445
|
+
args["--status"] = [status]
|
|
446
|
+
if business_use is not None:
|
|
447
|
+
args["--business-use"] = [business_use]
|
|
448
|
+
if update_frequency:
|
|
449
|
+
args["--update-frequency"] = [update_frequency]
|
|
450
|
+
if endorsed:
|
|
451
|
+
args["--endorsed"] = ["true"]
|
|
452
|
+
if owner_id:
|
|
453
|
+
args["--owner-id"] = list(owner_id)
|
|
454
|
+
|
|
455
|
+
result = client.update_data_product(args)
|
|
456
|
+
|
|
457
|
+
if not result:
|
|
458
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
459
|
+
return
|
|
460
|
+
if isinstance(result, dict) and "error" in result:
|
|
461
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
console.print(f"[green] SUCCESS:[/green] Updated data product '{product_id}'")
|
|
465
|
+
console.print(json.dumps(result, indent=2))
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@dataproduct.command()
|
|
472
|
+
@click.option("--product-id", required=True, help="ID of the data product to delete")
|
|
473
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
474
|
+
def delete(product_id, yes):
|
|
475
|
+
"""Delete a data product."""
|
|
476
|
+
try:
|
|
477
|
+
if not yes:
|
|
478
|
+
confirm = click.confirm(
|
|
479
|
+
f"Are you sure you want to delete data product '{product_id}'?",
|
|
480
|
+
default=False
|
|
481
|
+
)
|
|
482
|
+
if not confirm:
|
|
483
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
client = UnifiedCatalogClient()
|
|
487
|
+
args = {"--product-id": [product_id]}
|
|
488
|
+
result = client.delete_data_product(args)
|
|
489
|
+
|
|
490
|
+
# DELETE operations may return empty response on success
|
|
491
|
+
if result is None or (isinstance(result, dict) and not result.get("error")):
|
|
492
|
+
console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
|
|
493
|
+
elif isinstance(result, dict) and "error" in result:
|
|
494
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
495
|
+
else:
|
|
496
|
+
console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
|
|
497
|
+
if result:
|
|
498
|
+
console.print(json.dumps(result, indent=2))
|
|
499
|
+
|
|
500
|
+
except Exception as e:
|
|
501
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@dataproduct.command(name="add-relationship")
|
|
505
|
+
@click.option("--product-id", required=True, help="Data product ID (GUID)")
|
|
506
|
+
@click.option("--entity-type", required=True,
|
|
507
|
+
type=click.Choice(["CRITICALDATACOLUMN", "TERM", "DATAASSET", "CRITICALDATAELEMENT"], case_sensitive=False),
|
|
508
|
+
help="Type of entity to relate to")
|
|
509
|
+
@click.option("--entity-id", required=True, help="Entity ID (GUID) to relate to")
|
|
510
|
+
@click.option("--asset-id", help="Asset ID (GUID) - defaults to entity-id if not provided")
|
|
511
|
+
@click.option("--relationship-type", default="Related", help="Relationship type (default: Related)")
|
|
512
|
+
@click.option("--description", default="", help="Description of the relationship")
|
|
513
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
514
|
+
def add_relationship(product_id, entity_type, entity_id, asset_id, relationship_type, description, output):
|
|
515
|
+
"""Create a relationship for a data product.
|
|
516
|
+
|
|
517
|
+
Links a data product to another entity like a critical data column, term, or asset.
|
|
518
|
+
|
|
519
|
+
Examples:
|
|
520
|
+
pvw uc dataproduct add-relationship --product-id <id> --entity-type CRITICALDATACOLUMN --entity-id <col-id>
|
|
521
|
+
pvw uc dataproduct add-relationship --product-id <id> --entity-type TERM --entity-id <term-id> --description "Primary term"
|
|
522
|
+
"""
|
|
523
|
+
try:
|
|
524
|
+
client = UnifiedCatalogClient()
|
|
525
|
+
args = {
|
|
526
|
+
"--product-id": [product_id],
|
|
527
|
+
"--entity-type": [entity_type],
|
|
528
|
+
"--entity-id": [entity_id],
|
|
529
|
+
"--relationship-type": [relationship_type],
|
|
530
|
+
"--description": [description]
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if asset_id:
|
|
534
|
+
args["--asset-id"] = [asset_id]
|
|
535
|
+
|
|
536
|
+
result = client.create_data_product_relationship(args)
|
|
537
|
+
|
|
538
|
+
if output == "json":
|
|
539
|
+
console.print_json(data=result)
|
|
540
|
+
else:
|
|
541
|
+
if result and isinstance(result, dict):
|
|
542
|
+
console.print("[green]SUCCESS:[/green] Created relationship")
|
|
543
|
+
table = Table(title="Data Product Relationship", show_header=True)
|
|
544
|
+
table.add_column("Property", style="cyan")
|
|
545
|
+
table.add_column("Value", style="white")
|
|
546
|
+
|
|
547
|
+
table.add_row("Entity ID", result.get("entityId", "N/A"))
|
|
548
|
+
table.add_row("Relationship Type", result.get("relationshipType", "N/A"))
|
|
549
|
+
table.add_row("Description", result.get("description", "N/A"))
|
|
550
|
+
|
|
551
|
+
if "systemData" in result:
|
|
552
|
+
sys_data = result["systemData"]
|
|
553
|
+
table.add_row("Created By", sys_data.get("createdBy", "N/A"))
|
|
554
|
+
table.add_row("Created At", sys_data.get("createdAt", "N/A"))
|
|
555
|
+
|
|
556
|
+
console.print(table)
|
|
557
|
+
else:
|
|
558
|
+
console.print("[green]SUCCESS:[/green] Created relationship")
|
|
559
|
+
|
|
560
|
+
except Exception as e:
|
|
561
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@dataproduct.command(name="list-relationships")
|
|
565
|
+
@click.option("--product-id", required=True, help="Data product ID (GUID)")
|
|
566
|
+
@click.option("--entity-type",
|
|
567
|
+
type=click.Choice(["CRITICALDATACOLUMN", "TERM", "DATAASSET", "CRITICALDATAELEMENT"], case_sensitive=False),
|
|
568
|
+
help="Filter by entity type (optional)")
|
|
569
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
570
|
+
def list_relationships(product_id, entity_type, output):
|
|
571
|
+
"""List relationships for a data product.
|
|
572
|
+
|
|
573
|
+
Shows all entities linked to this data product, optionally filtered by type.
|
|
574
|
+
|
|
575
|
+
Examples:
|
|
576
|
+
pvw uc dataproduct list-relationships --product-id <id>
|
|
577
|
+
pvw uc dataproduct list-relationships --product-id <id> --entity-type CRITICALDATACOLUMN
|
|
578
|
+
"""
|
|
579
|
+
try:
|
|
580
|
+
client = UnifiedCatalogClient()
|
|
581
|
+
args = {"--product-id": [product_id]}
|
|
582
|
+
|
|
583
|
+
if entity_type:
|
|
584
|
+
args["--entity-type"] = [entity_type]
|
|
585
|
+
|
|
586
|
+
result = client.get_data_product_relationships(args)
|
|
587
|
+
|
|
588
|
+
if output == "json":
|
|
589
|
+
console.print_json(data=result)
|
|
590
|
+
else:
|
|
591
|
+
relationships = result.get("value", []) if result else []
|
|
592
|
+
|
|
593
|
+
if not relationships:
|
|
594
|
+
console.print(f"[yellow]No relationships found for data product '{product_id}'[/yellow]")
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
table = Table(title=f"Data Product Relationships ({len(relationships)} found)", show_header=True)
|
|
598
|
+
table.add_column("Entity ID", style="cyan")
|
|
599
|
+
table.add_column("Relationship Type", style="white")
|
|
600
|
+
table.add_column("Description", style="white")
|
|
601
|
+
table.add_column("Created", style="dim")
|
|
602
|
+
|
|
603
|
+
for rel in relationships:
|
|
604
|
+
table.add_row(
|
|
605
|
+
rel.get("entityId", "N/A"),
|
|
606
|
+
rel.get("relationshipType", "N/A"),
|
|
607
|
+
rel.get("description", "")[:50] + ("..." if len(rel.get("description", "")) > 50 else ""),
|
|
608
|
+
rel.get("systemData", {}).get("createdAt", "N/A")[:10]
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
console.print(table)
|
|
612
|
+
|
|
613
|
+
except Exception as e:
|
|
614
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@dataproduct.command(name="remove-relationship")
|
|
618
|
+
@click.option("--product-id", required=True, help="Data product ID (GUID)")
|
|
619
|
+
@click.option("--entity-type", required=True,
|
|
620
|
+
type=click.Choice(["CRITICALDATACOLUMN", "TERM", "DATAASSET", "CRITICALDATAELEMENT"], case_sensitive=False),
|
|
621
|
+
help="Type of entity to unlink")
|
|
622
|
+
@click.option("--entity-id", required=True, help="Entity ID (GUID) to unlink")
|
|
623
|
+
@click.option("--confirm/--no-confirm", default=True, help="Ask for confirmation before deleting")
|
|
624
|
+
def remove_relationship(product_id, entity_type, entity_id, confirm):
|
|
625
|
+
"""Delete a relationship between a data product and an entity.
|
|
626
|
+
|
|
627
|
+
Removes the link between a data product and a specific entity.
|
|
628
|
+
|
|
629
|
+
Examples:
|
|
630
|
+
pvw uc dataproduct remove-relationship --product-id <id> --entity-type CRITICALDATACOLUMN --entity-id <col-id>
|
|
631
|
+
pvw uc dataproduct remove-relationship --product-id <id> --entity-type TERM --entity-id <term-id> --no-confirm
|
|
632
|
+
"""
|
|
633
|
+
try:
|
|
634
|
+
if confirm:
|
|
635
|
+
confirm = click.confirm(
|
|
636
|
+
f"Are you sure you want to delete relationship to {entity_type} '{entity_id}'?",
|
|
637
|
+
default=False
|
|
638
|
+
)
|
|
639
|
+
if not confirm:
|
|
640
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
client = UnifiedCatalogClient()
|
|
644
|
+
args = {
|
|
645
|
+
"--product-id": [product_id],
|
|
646
|
+
"--entity-type": [entity_type],
|
|
647
|
+
"--entity-id": [entity_id]
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
result = client.delete_data_product_relationship(args)
|
|
651
|
+
|
|
652
|
+
# DELETE returns 204 No Content on success
|
|
653
|
+
if result is None or (isinstance(result, dict) and not result.get("error")):
|
|
654
|
+
console.print(f"[green]SUCCESS:[/green] Deleted relationship to {entity_type} '{entity_id}'")
|
|
655
|
+
elif isinstance(result, dict) and "error" in result:
|
|
656
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
657
|
+
else:
|
|
658
|
+
console.print(f"[green]SUCCESS:[/green] Deleted relationship")
|
|
659
|
+
|
|
660
|
+
except Exception as e:
|
|
661
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@dataproduct.command(name="query")
|
|
665
|
+
@click.option("--ids", multiple=True, help="Filter by specific product IDs (GUIDs)")
|
|
666
|
+
@click.option("--domain-ids", multiple=True, help="Filter by domain IDs (GUIDs)")
|
|
667
|
+
@click.option("--name-keyword", help="Filter by name keyword (partial match)")
|
|
668
|
+
@click.option("--owners", multiple=True, help="Filter by owner IDs (GUIDs)")
|
|
669
|
+
@click.option("--status", type=click.Choice(["Draft", "Published", "Expired"], case_sensitive=False),
|
|
670
|
+
help="Filter by status")
|
|
671
|
+
@click.option("--multi-status", multiple=True,
|
|
672
|
+
type=click.Choice(["Draft", "Published", "Expired"], case_sensitive=False),
|
|
673
|
+
help="Filter by multiple statuses")
|
|
674
|
+
@click.option("--type", help="Filter by data product type (e.g., Master, Operational)")
|
|
675
|
+
@click.option("--types", multiple=True, help="Filter by multiple data product types")
|
|
676
|
+
@click.option("--skip", type=int, default=0, help="Number of items to skip (pagination)")
|
|
677
|
+
@click.option("--top", type=int, default=100, help="Number of items to return (max 1000)")
|
|
678
|
+
@click.option("--order-by-field", help="Field to sort by (e.g., 'name', 'status')")
|
|
679
|
+
@click.option("--order-by-direction", type=click.Choice(["asc", "desc"]), default="asc",
|
|
680
|
+
help="Sort direction")
|
|
681
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
682
|
+
def query_data_products(ids, domain_ids, name_keyword, owners, status, multi_status, type, types,
|
|
683
|
+
skip, top, order_by_field, order_by_direction, output):
|
|
684
|
+
"""Query data products with advanced filters.
|
|
685
|
+
|
|
686
|
+
Perform complex searches across data products using multiple filter criteria.
|
|
687
|
+
Supports pagination and custom sorting.
|
|
688
|
+
|
|
689
|
+
Examples:
|
|
690
|
+
# Find all data products in a specific domain
|
|
691
|
+
pvw uc dataproduct query --domain-ids <domain-guid>
|
|
692
|
+
|
|
693
|
+
# Search by keyword
|
|
694
|
+
pvw uc dataproduct query --name-keyword "customer"
|
|
695
|
+
|
|
696
|
+
# Filter by owner and status
|
|
697
|
+
pvw uc dataproduct query --owners <user-guid> --status Published
|
|
698
|
+
|
|
699
|
+
# Pagination example
|
|
700
|
+
pvw uc dataproduct query --skip 0 --top 50 --order-by-field name
|
|
701
|
+
|
|
702
|
+
# Multiple filters
|
|
703
|
+
pvw uc dataproduct query --domain-ids <guid1> <guid2> --status Published --type Master
|
|
704
|
+
"""
|
|
705
|
+
try:
|
|
706
|
+
client = UnifiedCatalogClient()
|
|
707
|
+
args = {}
|
|
708
|
+
|
|
709
|
+
# Build args dict from parameters
|
|
710
|
+
if ids:
|
|
711
|
+
args["--ids"] = list(ids)
|
|
712
|
+
if domain_ids:
|
|
713
|
+
args["--domain-ids"] = list(domain_ids)
|
|
714
|
+
if name_keyword:
|
|
715
|
+
args["--name-keyword"] = [name_keyword]
|
|
716
|
+
if owners:
|
|
717
|
+
args["--owners"] = list(owners)
|
|
718
|
+
if status:
|
|
719
|
+
args["--status"] = [status]
|
|
720
|
+
if multi_status:
|
|
721
|
+
args["--multi-status"] = list(multi_status)
|
|
722
|
+
if type:
|
|
723
|
+
args["--type"] = [type]
|
|
724
|
+
if types:
|
|
725
|
+
args["--types"] = list(types)
|
|
726
|
+
if skip:
|
|
727
|
+
args["--skip"] = [str(skip)]
|
|
728
|
+
if top:
|
|
729
|
+
args["--top"] = [str(top)]
|
|
730
|
+
if order_by_field:
|
|
731
|
+
args["--order-by-field"] = [order_by_field]
|
|
732
|
+
args["--order-by-direction"] = [order_by_direction]
|
|
733
|
+
|
|
734
|
+
result = client.query_data_products(args)
|
|
735
|
+
|
|
736
|
+
if output == "json":
|
|
737
|
+
console.print_json(data=result)
|
|
738
|
+
else:
|
|
739
|
+
products = result.get("value", []) if result else []
|
|
740
|
+
|
|
741
|
+
if not products:
|
|
742
|
+
console.print("[yellow]No data products found matching the query.[/yellow]")
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
# Check for pagination
|
|
746
|
+
next_link = result.get("nextLink")
|
|
747
|
+
if next_link:
|
|
748
|
+
console.print(f"[dim]Note: More results available (nextLink provided)[/dim]\n")
|
|
749
|
+
|
|
750
|
+
table = Table(title=f"Query Results ({len(products)} found)", show_header=True)
|
|
751
|
+
table.add_column("Name", style="cyan")
|
|
752
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
753
|
+
table.add_column("Domain", style="yellow", no_wrap=True)
|
|
754
|
+
table.add_column("Type", style="green")
|
|
755
|
+
table.add_column("Status", style="white")
|
|
756
|
+
table.add_column("Owner", style="magenta")
|
|
757
|
+
|
|
758
|
+
for product in products:
|
|
759
|
+
# Extract owner info
|
|
760
|
+
contacts = product.get("contacts", {})
|
|
761
|
+
owners_list = contacts.get("owner", [])
|
|
762
|
+
owner_display = owners_list[0].get("id", "N/A")[:8] if owners_list else "N/A"
|
|
763
|
+
|
|
764
|
+
table.add_row(
|
|
765
|
+
product.get("name", "N/A"),
|
|
766
|
+
product.get("id", "N/A")[:13] + "...",
|
|
767
|
+
product.get("domain", "N/A")[:13] + "...",
|
|
768
|
+
product.get("type", "N/A"),
|
|
769
|
+
product.get("status", "N/A"),
|
|
770
|
+
owner_display + "..."
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
console.print(table)
|
|
774
|
+
|
|
775
|
+
# Show pagination info
|
|
776
|
+
if skip > 0 or next_link:
|
|
777
|
+
console.print(f"\n[dim]Showing items {skip + 1} to {skip + len(products)}[/dim]")
|
|
778
|
+
|
|
779
|
+
except Exception as e:
|
|
780
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
# ========================================
|
|
784
|
+
# GLOSSARIES
|
|
785
|
+
# ========================================
|
|
786
|
+
@uc.group()
|
|
787
|
+
def glossary():
|
|
788
|
+
"""Manage glossaries (for finding glossary GUIDs)."""
|
|
789
|
+
pass
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@glossary.command(name="list")
|
|
793
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
794
|
+
def list_glossaries(output_json):
|
|
795
|
+
"""List all glossaries with their GUIDs."""
|
|
796
|
+
try:
|
|
797
|
+
from purviewcli.client._glossary import Glossary
|
|
798
|
+
|
|
799
|
+
client = Glossary()
|
|
800
|
+
result = client.glossaryRead({})
|
|
801
|
+
|
|
802
|
+
# Normalize response
|
|
803
|
+
if isinstance(result, dict):
|
|
804
|
+
glossaries = result.get("value", []) or []
|
|
805
|
+
elif isinstance(result, (list, tuple)):
|
|
806
|
+
glossaries = result
|
|
807
|
+
else:
|
|
808
|
+
glossaries = []
|
|
809
|
+
|
|
810
|
+
if not glossaries:
|
|
811
|
+
console.print("[yellow]No glossaries found.[/yellow]")
|
|
812
|
+
return
|
|
813
|
+
|
|
814
|
+
# Output in JSON format if requested
|
|
815
|
+
if output_json:
|
|
816
|
+
_format_json_output(glossaries)
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
table = Table(title="Glossaries")
|
|
820
|
+
table.add_column("GUID", style="cyan", no_wrap=True)
|
|
821
|
+
table.add_column("Name", style="green")
|
|
822
|
+
table.add_column("Qualified Name", style="yellow")
|
|
823
|
+
table.add_column("Description", style="white")
|
|
824
|
+
|
|
825
|
+
for g in glossaries:
|
|
826
|
+
if not isinstance(g, dict):
|
|
827
|
+
continue
|
|
828
|
+
table.add_row(
|
|
829
|
+
g.get("guid", "N/A"),
|
|
830
|
+
g.get("name", "N/A"),
|
|
831
|
+
g.get("qualifiedName", "N/A"),
|
|
832
|
+
(g.get("shortDescription", "")[:60] + "...") if len(g.get("shortDescription", "")) > 60 else g.get("shortDescription", ""),
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
console.print(table)
|
|
836
|
+
console.print("\n[dim]Tip: Use the GUID with --glossary-guid option when listing/creating terms[/dim]")
|
|
837
|
+
|
|
838
|
+
except Exception as e:
|
|
839
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
@glossary.command(name="create")
|
|
843
|
+
@click.option("--name", required=True, help="Name of the glossary")
|
|
844
|
+
@click.option("--description", required=False, default="", help="Description of the glossary")
|
|
845
|
+
@click.option("--domain-id", required=False, help="Associate with governance domain ID (optional)")
|
|
846
|
+
def create_glossary(name, description, domain_id):
|
|
847
|
+
"""Create a new glossary."""
|
|
848
|
+
try:
|
|
849
|
+
from purviewcli.client._glossary import Glossary
|
|
850
|
+
|
|
851
|
+
client = Glossary()
|
|
852
|
+
|
|
853
|
+
# Build qualified name - include domain_id if provided
|
|
854
|
+
if domain_id:
|
|
855
|
+
qualified_name = f"{name}@{domain_id}"
|
|
856
|
+
else:
|
|
857
|
+
qualified_name = name
|
|
858
|
+
|
|
859
|
+
payload = {
|
|
860
|
+
"name": name,
|
|
861
|
+
"qualifiedName": qualified_name,
|
|
862
|
+
"shortDescription": description,
|
|
863
|
+
"longDescription": description,
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
result = client.glossaryCreate({"--payloadFile": payload})
|
|
867
|
+
|
|
868
|
+
if not result:
|
|
869
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
870
|
+
return
|
|
871
|
+
if isinstance(result, dict) and "error" in result:
|
|
872
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
873
|
+
return
|
|
874
|
+
|
|
875
|
+
guid = result.get("guid") if isinstance(result, dict) else None
|
|
876
|
+
console.print(f"[green] SUCCESS:[/green] Created glossary '{name}'")
|
|
877
|
+
if guid:
|
|
878
|
+
console.print(f"[cyan]GUID:[/cyan] {guid}")
|
|
879
|
+
console.print(f"\n[dim]Use this GUID: --glossary-guid {guid}[/dim]")
|
|
880
|
+
console.print(json.dumps(result, indent=2))
|
|
881
|
+
|
|
882
|
+
except Exception as e:
|
|
883
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@glossary.command(name="create-for-domains")
|
|
887
|
+
def create_glossaries_for_domains():
|
|
888
|
+
"""Create glossaries for all governance domains that don't have one."""
|
|
889
|
+
try:
|
|
890
|
+
from purviewcli.client._glossary import Glossary
|
|
891
|
+
|
|
892
|
+
uc_client = UnifiedCatalogClient()
|
|
893
|
+
glossary_client = Glossary()
|
|
894
|
+
|
|
895
|
+
# Get all domains
|
|
896
|
+
domains_result = uc_client.get_governance_domains({})
|
|
897
|
+
if isinstance(domains_result, dict):
|
|
898
|
+
domains = domains_result.get("value", [])
|
|
899
|
+
elif isinstance(domains_result, (list, tuple)):
|
|
900
|
+
domains = domains_result
|
|
901
|
+
else:
|
|
902
|
+
domains = []
|
|
903
|
+
|
|
904
|
+
if not domains:
|
|
905
|
+
console.print("[yellow]No governance domains found.[/yellow]")
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
# Get existing glossaries
|
|
909
|
+
glossaries_result = glossary_client.glossaryRead({})
|
|
910
|
+
if isinstance(glossaries_result, dict):
|
|
911
|
+
existing_glossaries = glossaries_result.get("value", [])
|
|
912
|
+
elif isinstance(glossaries_result, (list, tuple)):
|
|
913
|
+
existing_glossaries = glossaries_result
|
|
914
|
+
else:
|
|
915
|
+
existing_glossaries = []
|
|
916
|
+
|
|
917
|
+
# Build set of domain IDs that already have glossaries (check qualifiedName)
|
|
918
|
+
existing_domain_ids = set()
|
|
919
|
+
for g in existing_glossaries:
|
|
920
|
+
if isinstance(g, dict):
|
|
921
|
+
qn = g.get("qualifiedName", "")
|
|
922
|
+
# Extract domain_id from qualifiedName if it contains @domain_id pattern
|
|
923
|
+
if "@" in qn:
|
|
924
|
+
domain_id_part = qn.split("@")[-1]
|
|
925
|
+
existing_domain_ids.add(domain_id_part)
|
|
926
|
+
|
|
927
|
+
console.print(f"[cyan]Found {len(domains)} governance domains and {len(existing_glossaries)} existing glossaries[/cyan]\n")
|
|
928
|
+
|
|
929
|
+
created_count = 0
|
|
930
|
+
for domain in domains:
|
|
931
|
+
if not isinstance(domain, dict):
|
|
932
|
+
continue
|
|
933
|
+
|
|
934
|
+
domain_id = domain.get("id")
|
|
935
|
+
domain_name = domain.get("name")
|
|
936
|
+
|
|
937
|
+
if not domain_id or not domain_name:
|
|
938
|
+
continue
|
|
939
|
+
|
|
940
|
+
# Check if glossary already exists for this domain
|
|
941
|
+
if domain_id in existing_domain_ids:
|
|
942
|
+
console.print(f"[dim]⏭ Skipping {domain_name} - glossary already exists[/dim]")
|
|
943
|
+
continue
|
|
944
|
+
|
|
945
|
+
# Create glossary for this domain
|
|
946
|
+
glossary_name = f"{domain_name} Glossary"
|
|
947
|
+
qualified_name = f"{glossary_name}@{domain_id}"
|
|
948
|
+
|
|
949
|
+
payload = {
|
|
950
|
+
"name": glossary_name,
|
|
951
|
+
"qualifiedName": qualified_name,
|
|
952
|
+
"shortDescription": f"Glossary for {domain_name} domain",
|
|
953
|
+
"longDescription": f"This glossary contains business terms for the {domain_name} governance domain.",
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
result = glossary_client.glossaryCreate({"--payloadFile": payload})
|
|
958
|
+
guid = result.get("guid") if isinstance(result, dict) else None
|
|
959
|
+
|
|
960
|
+
if guid:
|
|
961
|
+
console.print(f"[green] Created:[/green] {glossary_name} (GUID: {guid})")
|
|
962
|
+
created_count += 1
|
|
963
|
+
else:
|
|
964
|
+
console.print(f"[yellow] Created {glossary_name} but no GUID returned[/yellow]")
|
|
965
|
+
|
|
966
|
+
except Exception as e:
|
|
967
|
+
console.print(f"[red] Failed to create {glossary_name}:[/red] {str(e)}")
|
|
968
|
+
|
|
969
|
+
console.print(f"\n[cyan]Created {created_count} new glossaries[/cyan]")
|
|
970
|
+
console.print("[dim]Run 'pvw uc glossary list' to see all glossaries[/dim]")
|
|
971
|
+
|
|
972
|
+
except Exception as e:
|
|
973
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@glossary.command(name="verify-links")
|
|
977
|
+
def verify_glossary_links():
|
|
978
|
+
"""Verify which domains have properly linked glossaries."""
|
|
979
|
+
try:
|
|
980
|
+
from purviewcli.client._glossary import Glossary
|
|
981
|
+
|
|
982
|
+
uc_client = UnifiedCatalogClient()
|
|
983
|
+
glossary_client = Glossary()
|
|
984
|
+
|
|
985
|
+
# Get all domains
|
|
986
|
+
domains_result = uc_client.get_governance_domains({})
|
|
987
|
+
if isinstance(domains_result, dict):
|
|
988
|
+
domains = domains_result.get("value", [])
|
|
989
|
+
elif isinstance(domains_result, (list, tuple)):
|
|
990
|
+
domains = domains_result
|
|
991
|
+
else:
|
|
992
|
+
domains = []
|
|
993
|
+
|
|
994
|
+
# Get all glossaries
|
|
995
|
+
glossaries_result = glossary_client.glossaryRead({})
|
|
996
|
+
if isinstance(glossaries_result, dict):
|
|
997
|
+
glossaries = glossaries_result.get("value", [])
|
|
998
|
+
elif isinstance(glossaries_result, (list, tuple)):
|
|
999
|
+
glossaries = glossaries_result
|
|
1000
|
+
else:
|
|
1001
|
+
glossaries = []
|
|
1002
|
+
|
|
1003
|
+
console.print(f"[bold cyan]Governance Domain → Glossary Link Verification[/bold cyan]\n")
|
|
1004
|
+
|
|
1005
|
+
table = Table(title="Domain-Glossary Associations")
|
|
1006
|
+
table.add_column("Domain Name", style="green")
|
|
1007
|
+
table.add_column("Domain ID", style="cyan", no_wrap=True)
|
|
1008
|
+
table.add_column("Linked Glossary", style="yellow")
|
|
1009
|
+
table.add_column("Glossary GUID", style="magenta", no_wrap=True)
|
|
1010
|
+
table.add_column("Status", style="white")
|
|
1011
|
+
|
|
1012
|
+
# Build a map of domain_id -> glossary info
|
|
1013
|
+
domain_glossary_map = {}
|
|
1014
|
+
for g in glossaries:
|
|
1015
|
+
if not isinstance(g, dict):
|
|
1016
|
+
continue
|
|
1017
|
+
qn = g.get("qualifiedName", "")
|
|
1018
|
+
# Check if qualifiedName contains @domain_id pattern
|
|
1019
|
+
if "@" in qn:
|
|
1020
|
+
domain_id_part = qn.split("@")[-1]
|
|
1021
|
+
domain_glossary_map[domain_id_part] = {
|
|
1022
|
+
"name": g.get("name"),
|
|
1023
|
+
"guid": g.get("guid"),
|
|
1024
|
+
"qualifiedName": qn,
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
linked_count = 0
|
|
1028
|
+
unlinked_count = 0
|
|
1029
|
+
|
|
1030
|
+
for domain in domains:
|
|
1031
|
+
if not isinstance(domain, dict):
|
|
1032
|
+
continue
|
|
1033
|
+
|
|
1034
|
+
domain_id = domain.get("id")
|
|
1035
|
+
domain_name = domain.get("name", "N/A")
|
|
1036
|
+
parent_id = domain.get("parentDomainId")
|
|
1037
|
+
|
|
1038
|
+
# Skip if no domain_id
|
|
1039
|
+
if not domain_id:
|
|
1040
|
+
continue
|
|
1041
|
+
|
|
1042
|
+
# Show if it's a nested domain
|
|
1043
|
+
nested_indicator = " (nested)" if parent_id else ""
|
|
1044
|
+
domain_display = f"{domain_name}{nested_indicator}"
|
|
1045
|
+
|
|
1046
|
+
if domain_id in domain_glossary_map:
|
|
1047
|
+
glossary_info = domain_glossary_map[domain_id]
|
|
1048
|
+
table.add_row(
|
|
1049
|
+
domain_display,
|
|
1050
|
+
domain_id[:8] + "...",
|
|
1051
|
+
glossary_info["name"],
|
|
1052
|
+
glossary_info["guid"][:8] + "...",
|
|
1053
|
+
"[green] Linked[/green]"
|
|
1054
|
+
)
|
|
1055
|
+
linked_count += 1
|
|
1056
|
+
else:
|
|
1057
|
+
table.add_row(
|
|
1058
|
+
domain_display,
|
|
1059
|
+
domain_id[:8] + "...",
|
|
1060
|
+
"[dim]No glossary[/dim]",
|
|
1061
|
+
"[dim]N/A[/dim]",
|
|
1062
|
+
"[yellow] Not Linked[/yellow]"
|
|
1063
|
+
)
|
|
1064
|
+
unlinked_count += 1
|
|
1065
|
+
|
|
1066
|
+
console.print(table)
|
|
1067
|
+
console.print(f"\n[cyan]Summary:[/cyan]")
|
|
1068
|
+
console.print(f" • Linked domains: [green]{linked_count}[/green]")
|
|
1069
|
+
console.print(f" • Unlinked domains: [yellow]{unlinked_count}[/yellow]")
|
|
1070
|
+
|
|
1071
|
+
if unlinked_count > 0:
|
|
1072
|
+
console.print(f"\n[dim]💡 Tip: Run 'pvw uc glossary create-for-domains' to create glossaries for unlinked domains[/dim]")
|
|
1073
|
+
|
|
1074
|
+
except Exception as e:
|
|
1075
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
# ========================================
|
|
1079
|
+
# GLOSSARY TERMS
|
|
1080
|
+
# ========================================
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
@uc.group()
|
|
1084
|
+
def term():
|
|
1085
|
+
"""Manage glossary terms."""
|
|
1086
|
+
pass
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
@term.command()
|
|
1090
|
+
@click.option("--name", required=True, help="Name of the glossary term")
|
|
1091
|
+
@click.option("--description", required=False, default="", help="Rich text description of the term")
|
|
1092
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
1093
|
+
@click.option("--parent-id", required=False, help="Parent term ID (for hierarchical terms)")
|
|
1094
|
+
@click.option(
|
|
1095
|
+
"--status",
|
|
1096
|
+
required=False,
|
|
1097
|
+
default="Draft",
|
|
1098
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
1099
|
+
help="Status of the term",
|
|
1100
|
+
)
|
|
1101
|
+
@click.option(
|
|
1102
|
+
"--acronym",
|
|
1103
|
+
required=False,
|
|
1104
|
+
help="Acronyms for the term (can be specified multiple times)",
|
|
1105
|
+
multiple=True,
|
|
1106
|
+
)
|
|
1107
|
+
@click.option(
|
|
1108
|
+
"--owner-id",
|
|
1109
|
+
required=False,
|
|
1110
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
1111
|
+
multiple=True,
|
|
1112
|
+
)
|
|
1113
|
+
@click.option("--resource-name", required=False, help="Resource name for additional reading (can be specified multiple times)", multiple=True)
|
|
1114
|
+
@click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times)", multiple=True)
|
|
1115
|
+
def create(name, description, domain_id, parent_id, status, acronym, owner_id, resource_name, resource_url):
|
|
1116
|
+
"""Create a new Unified Catalog term (Governance Domain term)."""
|
|
1117
|
+
try:
|
|
1118
|
+
client = UnifiedCatalogClient()
|
|
1119
|
+
|
|
1120
|
+
# Build args dictionary
|
|
1121
|
+
args = {
|
|
1122
|
+
"--name": [name],
|
|
1123
|
+
"--description": [description],
|
|
1124
|
+
"--governance-domain-id": [domain_id],
|
|
1125
|
+
"--status": [status],
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if parent_id:
|
|
1129
|
+
args["--parent-id"] = [parent_id]
|
|
1130
|
+
if acronym:
|
|
1131
|
+
args["--acronym"] = list(acronym)
|
|
1132
|
+
if owner_id:
|
|
1133
|
+
args["--owner-id"] = list(owner_id)
|
|
1134
|
+
if resource_name:
|
|
1135
|
+
args["--resource-name"] = list(resource_name)
|
|
1136
|
+
if resource_url:
|
|
1137
|
+
args["--resource-url"] = list(resource_url)
|
|
1138
|
+
|
|
1139
|
+
result = client.create_term(args)
|
|
1140
|
+
|
|
1141
|
+
if not result:
|
|
1142
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
1143
|
+
return
|
|
1144
|
+
if isinstance(result, dict) and "error" in result:
|
|
1145
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
1146
|
+
return
|
|
1147
|
+
|
|
1148
|
+
console.print(f"[green] SUCCESS:[/green] Created glossary term '{name}'")
|
|
1149
|
+
console.print(json.dumps(result, indent=2))
|
|
1150
|
+
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
@term.command(name="list")
|
|
1156
|
+
@click.option("--domain-id", required=True, help="Governance domain ID to list terms from")
|
|
1157
|
+
@click.option(
|
|
1158
|
+
"--output",
|
|
1159
|
+
type=click.Choice(["table", "json", "jsonc"]),
|
|
1160
|
+
default="table",
|
|
1161
|
+
help="Output format: table (default, formatted), json (plain, parseable), jsonc (colored JSON)"
|
|
1162
|
+
)
|
|
1163
|
+
def list_terms(domain_id, output):
|
|
1164
|
+
"""List all Unified Catalog terms in a governance domain.
|
|
1165
|
+
|
|
1166
|
+
Output formats:
|
|
1167
|
+
- table: Formatted table output with Rich (default)
|
|
1168
|
+
- json: Plain JSON for scripting (use with PowerShell ConvertFrom-Json)
|
|
1169
|
+
- jsonc: Colored JSON with syntax highlighting for viewing
|
|
1170
|
+
"""
|
|
1171
|
+
try:
|
|
1172
|
+
client = UnifiedCatalogClient()
|
|
1173
|
+
args = {"--governance-domain-id": [domain_id]}
|
|
1174
|
+
result = client.get_terms(args)
|
|
1175
|
+
|
|
1176
|
+
if not result:
|
|
1177
|
+
console.print("[yellow]No terms found.[/yellow]")
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
# Unified Catalog API returns terms directly in value array
|
|
1181
|
+
all_terms = []
|
|
1182
|
+
|
|
1183
|
+
if isinstance(result, dict):
|
|
1184
|
+
all_terms = result.get("value", [])
|
|
1185
|
+
elif isinstance(result, (list, tuple)):
|
|
1186
|
+
all_terms = result
|
|
1187
|
+
else:
|
|
1188
|
+
console.print("[yellow]Unexpected response format.[/yellow]")
|
|
1189
|
+
return
|
|
1190
|
+
|
|
1191
|
+
if not all_terms:
|
|
1192
|
+
console.print("[yellow]No terms found.[/yellow]")
|
|
1193
|
+
return
|
|
1194
|
+
|
|
1195
|
+
# Handle output format
|
|
1196
|
+
if output == "json":
|
|
1197
|
+
# Plain JSON for scripting (PowerShell compatible)
|
|
1198
|
+
print(json.dumps(all_terms, indent=2))
|
|
1199
|
+
return
|
|
1200
|
+
elif output == "jsonc":
|
|
1201
|
+
# Colored JSON for viewing
|
|
1202
|
+
_format_json_output(all_terms)
|
|
1203
|
+
return
|
|
1204
|
+
|
|
1205
|
+
table = Table(title="Unified Catalog Terms")
|
|
1206
|
+
table.add_column("Term ID", style="cyan", no_wrap=False)
|
|
1207
|
+
table.add_column("Name", style="green")
|
|
1208
|
+
table.add_column("Status", style="yellow")
|
|
1209
|
+
table.add_column("Description", style="white")
|
|
1210
|
+
|
|
1211
|
+
for term in all_terms:
|
|
1212
|
+
description = term.get("description", "")
|
|
1213
|
+
# Strip HTML tags from description
|
|
1214
|
+
import re
|
|
1215
|
+
description = re.sub(r'<[^>]+>', '', description)
|
|
1216
|
+
# Truncate long descriptions
|
|
1217
|
+
if len(description) > 50:
|
|
1218
|
+
description = description[:50] + "..."
|
|
1219
|
+
|
|
1220
|
+
table.add_row(
|
|
1221
|
+
term.get("id", "N/A"),
|
|
1222
|
+
term.get("name", "N/A"),
|
|
1223
|
+
term.get("status", "N/A"),
|
|
1224
|
+
description.strip(),
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
console.print(table)
|
|
1228
|
+
console.print(f"\n[dim]Found {len(all_terms)} term(s)[/dim]")
|
|
1229
|
+
|
|
1230
|
+
except Exception as e:
|
|
1231
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
@term.command()
|
|
1235
|
+
@click.option("--term-id", required=True, help="ID of the glossary term")
|
|
1236
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
1237
|
+
def show(term_id, output_json):
|
|
1238
|
+
"""Show details of a glossary term."""
|
|
1239
|
+
try:
|
|
1240
|
+
client = UnifiedCatalogClient()
|
|
1241
|
+
args = {"--term-id": [term_id]}
|
|
1242
|
+
result = client.get_term_by_id(args)
|
|
1243
|
+
|
|
1244
|
+
if not result:
|
|
1245
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
1246
|
+
return
|
|
1247
|
+
if isinstance(result, dict) and "error" in result:
|
|
1248
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Term not found')}")
|
|
1249
|
+
return
|
|
1250
|
+
|
|
1251
|
+
if output_json:
|
|
1252
|
+
_format_json_output(result)
|
|
1253
|
+
else:
|
|
1254
|
+
# Display key information in a readable format
|
|
1255
|
+
if isinstance(result, dict):
|
|
1256
|
+
console.print(f"[cyan]Term Name:[/cyan] {result.get('name', 'N/A')}")
|
|
1257
|
+
console.print(f"[cyan]GUID:[/cyan] {result.get('guid', 'N/A')}")
|
|
1258
|
+
console.print(f"[cyan]Status:[/cyan] {result.get('status', 'N/A')}")
|
|
1259
|
+
console.print(f"[cyan]Qualified Name:[/cyan] {result.get('qualifiedName', 'N/A')}")
|
|
1260
|
+
|
|
1261
|
+
# Show glossary info
|
|
1262
|
+
anchor = result.get('anchor', {})
|
|
1263
|
+
if anchor:
|
|
1264
|
+
console.print(f"[cyan]Glossary GUID:[/cyan] {anchor.get('glossaryGuid', 'N/A')}")
|
|
1265
|
+
|
|
1266
|
+
# Show description
|
|
1267
|
+
desc = result.get('shortDescription') or result.get('longDescription', '')
|
|
1268
|
+
if desc:
|
|
1269
|
+
console.print(f"[cyan]Description:[/cyan] {desc}")
|
|
1270
|
+
|
|
1271
|
+
# Show full JSON if needed
|
|
1272
|
+
console.print(f"\n[dim]Full details (JSON):[/dim]")
|
|
1273
|
+
console.print(json.dumps(result, indent=2))
|
|
1274
|
+
else:
|
|
1275
|
+
console.print(json.dumps(result, indent=2))
|
|
1276
|
+
|
|
1277
|
+
except Exception as e:
|
|
1278
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
@term.command()
|
|
1282
|
+
@click.option("--term-id", required=True, help="ID of the glossary term to delete")
|
|
1283
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
1284
|
+
def delete(term_id, force):
|
|
1285
|
+
"""Delete a glossary term."""
|
|
1286
|
+
try:
|
|
1287
|
+
if not force:
|
|
1288
|
+
# Show term details first
|
|
1289
|
+
client = UnifiedCatalogClient()
|
|
1290
|
+
term_info = client.get_term_by_id({"--term-id": [term_id]})
|
|
1291
|
+
|
|
1292
|
+
if isinstance(term_info, dict) and term_info.get('name'):
|
|
1293
|
+
console.print(f"[yellow]About to delete term:[/yellow]")
|
|
1294
|
+
console.print(f" Name: {term_info.get('name')}")
|
|
1295
|
+
console.print(f" GUID: {term_info.get('guid')}")
|
|
1296
|
+
console.print(f" Status: {term_info.get('status')}")
|
|
1297
|
+
|
|
1298
|
+
confirm = click.confirm("Are you sure you want to delete this term?", default=False)
|
|
1299
|
+
if not confirm:
|
|
1300
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
1301
|
+
return
|
|
1302
|
+
|
|
1303
|
+
# Import glossary client to delete term
|
|
1304
|
+
from purviewcli.client._glossary import Glossary
|
|
1305
|
+
|
|
1306
|
+
gclient = Glossary()
|
|
1307
|
+
result = gclient.glossaryDeleteTerm({"--termGuid": term_id})
|
|
1308
|
+
|
|
1309
|
+
console.print(f"[green] SUCCESS:[/green] Deleted term with ID: {term_id}")
|
|
1310
|
+
|
|
1311
|
+
except Exception as e:
|
|
1312
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
@term.command()
|
|
1316
|
+
@click.option("--term-id", required=True, help="ID of the glossary term to update")
|
|
1317
|
+
@click.option("--name", required=False, help="Name of the glossary term")
|
|
1318
|
+
@click.option("--description", required=False, help="Rich text description of the term")
|
|
1319
|
+
@click.option("--domain-id", required=False, help="Governance domain ID")
|
|
1320
|
+
@click.option("--parent-id", required=False, help="Parent term ID (for hierarchical terms)")
|
|
1321
|
+
@click.option(
|
|
1322
|
+
"--status",
|
|
1323
|
+
required=False,
|
|
1324
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
1325
|
+
help="Status of the term",
|
|
1326
|
+
)
|
|
1327
|
+
@click.option(
|
|
1328
|
+
"--acronym",
|
|
1329
|
+
required=False,
|
|
1330
|
+
help="Acronyms for the term (can be specified multiple times, replaces existing)",
|
|
1331
|
+
multiple=True,
|
|
1332
|
+
)
|
|
1333
|
+
@click.option(
|
|
1334
|
+
"--owner-id",
|
|
1335
|
+
required=False,
|
|
1336
|
+
help="Owner Entra ID (can be specified multiple times, replaces existing)",
|
|
1337
|
+
multiple=True,
|
|
1338
|
+
)
|
|
1339
|
+
@click.option("--resource-name", required=False, help="Resource name for additional reading (can be specified multiple times, replaces existing)", multiple=True)
|
|
1340
|
+
@click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times, replaces existing)", multiple=True)
|
|
1341
|
+
@click.option("--add-acronym", required=False, help="Add acronym to existing ones (can be specified multiple times)", multiple=True)
|
|
1342
|
+
@click.option("--add-owner-id", required=False, help="Add owner to existing ones (can be specified multiple times)", multiple=True)
|
|
1343
|
+
def update(term_id, name, description, domain_id, parent_id, status, acronym, owner_id, resource_name, resource_url, add_acronym, add_owner_id):
|
|
1344
|
+
"""Update an existing Unified Catalog term."""
|
|
1345
|
+
try:
|
|
1346
|
+
client = UnifiedCatalogClient()
|
|
1347
|
+
|
|
1348
|
+
# Build args dictionary - only include provided values
|
|
1349
|
+
args = {"--term-id": [term_id]}
|
|
1350
|
+
|
|
1351
|
+
if name:
|
|
1352
|
+
args["--name"] = [name]
|
|
1353
|
+
if description is not None: # Allow empty string
|
|
1354
|
+
args["--description"] = [description]
|
|
1355
|
+
if domain_id:
|
|
1356
|
+
args["--governance-domain-id"] = [domain_id]
|
|
1357
|
+
if parent_id:
|
|
1358
|
+
args["--parent-id"] = [parent_id]
|
|
1359
|
+
if status:
|
|
1360
|
+
args["--status"] = [status]
|
|
1361
|
+
|
|
1362
|
+
# Handle acronyms - either replace or add
|
|
1363
|
+
if acronym:
|
|
1364
|
+
args["--acronym"] = list(acronym)
|
|
1365
|
+
elif add_acronym:
|
|
1366
|
+
args["--add-acronym"] = list(add_acronym)
|
|
1367
|
+
|
|
1368
|
+
# Handle owners - either replace or add
|
|
1369
|
+
if owner_id:
|
|
1370
|
+
args["--owner-id"] = list(owner_id)
|
|
1371
|
+
elif add_owner_id:
|
|
1372
|
+
args["--add-owner-id"] = list(add_owner_id)
|
|
1373
|
+
|
|
1374
|
+
# Handle resources
|
|
1375
|
+
if resource_name:
|
|
1376
|
+
args["--resource-name"] = list(resource_name)
|
|
1377
|
+
if resource_url:
|
|
1378
|
+
args["--resource-url"] = list(resource_url)
|
|
1379
|
+
|
|
1380
|
+
result = client.update_term(args)
|
|
1381
|
+
|
|
1382
|
+
if not result:
|
|
1383
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
1384
|
+
return
|
|
1385
|
+
if isinstance(result, dict) and "error" in result:
|
|
1386
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
1387
|
+
return
|
|
1388
|
+
|
|
1389
|
+
console.print(f"[green] SUCCESS:[/green] Updated glossary term '{term_id}'")
|
|
1390
|
+
console.print(json.dumps(result, indent=2))
|
|
1391
|
+
|
|
1392
|
+
except Exception as e:
|
|
1393
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
@term.command(name="import-csv")
|
|
1397
|
+
@click.option("--csv-file", required=True, type=click.Path(exists=True), help="Path to CSV file with terms")
|
|
1398
|
+
@click.option("--domain-id", required=True, help="Governance domain ID for all terms")
|
|
1399
|
+
@click.option("--dry-run", is_flag=True, help="Preview terms without creating them")
|
|
1400
|
+
def import_terms_from_csv(csv_file, domain_id, dry_run):
|
|
1401
|
+
"""Bulk import glossary terms from a CSV file.
|
|
1402
|
+
|
|
1403
|
+
CSV Format:
|
|
1404
|
+
name,description,status,acronyms,owner_ids,resource_name,resource_url
|
|
1405
|
+
|
|
1406
|
+
- name: Required term name
|
|
1407
|
+
- description: Optional description
|
|
1408
|
+
- status: Draft, Published, or Archived (default: Draft)
|
|
1409
|
+
- acronyms: Comma-separated list (e.g., "API,REST")
|
|
1410
|
+
- owner_ids: Comma-separated list of Entra Object IDs
|
|
1411
|
+
- resource_name: Name of related resource
|
|
1412
|
+
- resource_url: URL of related resource
|
|
1413
|
+
|
|
1414
|
+
Multiple resources can be specified by separating with semicolons.
|
|
1415
|
+
"""
|
|
1416
|
+
try:
|
|
1417
|
+
client = UnifiedCatalogClient()
|
|
1418
|
+
|
|
1419
|
+
# Read and parse CSV
|
|
1420
|
+
terms = []
|
|
1421
|
+
with open(csv_file, 'r', encoding='utf-8') as f:
|
|
1422
|
+
reader = csv.DictReader(f)
|
|
1423
|
+
for row in reader:
|
|
1424
|
+
term = {
|
|
1425
|
+
"name": row.get("name", "").strip(),
|
|
1426
|
+
"description": row.get("description", "").strip(),
|
|
1427
|
+
"status": row.get("status", "Draft").strip(),
|
|
1428
|
+
"domain_id": domain_id,
|
|
1429
|
+
"acronyms": [],
|
|
1430
|
+
"owner_ids": [],
|
|
1431
|
+
"resources": []
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
# Parse acronyms
|
|
1435
|
+
if row.get("acronyms"):
|
|
1436
|
+
term["acronyms"] = [a.strip() for a in row["acronyms"].split(",") if a.strip()]
|
|
1437
|
+
|
|
1438
|
+
# Parse owner IDs
|
|
1439
|
+
if row.get("owner_ids"):
|
|
1440
|
+
term["owner_ids"] = [o.strip() for o in row["owner_ids"].split(",") if o.strip()]
|
|
1441
|
+
|
|
1442
|
+
# Parse resources
|
|
1443
|
+
resource_names = row.get("resource_name", "").strip()
|
|
1444
|
+
resource_urls = row.get("resource_url", "").strip()
|
|
1445
|
+
|
|
1446
|
+
if resource_names and resource_urls:
|
|
1447
|
+
names = [n.strip() for n in resource_names.split(";") if n.strip()]
|
|
1448
|
+
urls = [u.strip() for u in resource_urls.split(";") if u.strip()]
|
|
1449
|
+
term["resources"] = [{"name": n, "url": u} for n, u in zip(names, urls)]
|
|
1450
|
+
|
|
1451
|
+
if term["name"]: # Only add if name is present
|
|
1452
|
+
terms.append(term)
|
|
1453
|
+
|
|
1454
|
+
if not terms:
|
|
1455
|
+
console.print("[yellow]No valid terms found in CSV file.[/yellow]")
|
|
1456
|
+
return
|
|
1457
|
+
|
|
1458
|
+
console.print(f"[cyan]Found {len(terms)} term(s) in CSV file[/cyan]")
|
|
1459
|
+
|
|
1460
|
+
if dry_run:
|
|
1461
|
+
console.print("\n[yellow]DRY RUN - Preview of terms to be created:[/yellow]\n")
|
|
1462
|
+
table = Table(title="Terms to Import")
|
|
1463
|
+
table.add_column("#", style="dim", width=4)
|
|
1464
|
+
table.add_column("Name", style="cyan")
|
|
1465
|
+
table.add_column("Status", style="yellow")
|
|
1466
|
+
table.add_column("Acronyms", style="magenta")
|
|
1467
|
+
table.add_column("Owners", style="green")
|
|
1468
|
+
|
|
1469
|
+
for i, term in enumerate(terms, 1):
|
|
1470
|
+
acronyms = ", ".join(term.get("acronyms", []))
|
|
1471
|
+
owners = ", ".join(term.get("owner_ids", []))
|
|
1472
|
+
table.add_row(
|
|
1473
|
+
str(i),
|
|
1474
|
+
term["name"],
|
|
1475
|
+
term["status"],
|
|
1476
|
+
acronyms or "-",
|
|
1477
|
+
owners or "-"
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
console.print(table)
|
|
1481
|
+
console.print(f"\n[dim]Domain ID: {domain_id}[/dim]")
|
|
1482
|
+
return
|
|
1483
|
+
|
|
1484
|
+
# Import terms (one by one using single POST)
|
|
1485
|
+
success_count = 0
|
|
1486
|
+
failed_count = 0
|
|
1487
|
+
failed_terms = []
|
|
1488
|
+
|
|
1489
|
+
with console.status("[bold green]Importing terms...") as status:
|
|
1490
|
+
for i, term in enumerate(terms, 1):
|
|
1491
|
+
status.update(f"[bold green]Creating term {i}/{len(terms)}: {term['name']}")
|
|
1492
|
+
|
|
1493
|
+
try:
|
|
1494
|
+
# Create individual term
|
|
1495
|
+
args = {
|
|
1496
|
+
"--name": [term["name"]],
|
|
1497
|
+
"--description": [term.get("description", "")],
|
|
1498
|
+
"--governance-domain-id": [term["domain_id"]],
|
|
1499
|
+
"--status": [term.get("status", "Draft")],
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if term.get("acronyms"):
|
|
1503
|
+
args["--acronym"] = term["acronyms"]
|
|
1504
|
+
|
|
1505
|
+
if term.get("owner_ids"):
|
|
1506
|
+
args["--owner-id"] = term["owner_ids"]
|
|
1507
|
+
|
|
1508
|
+
if term.get("resources"):
|
|
1509
|
+
args["--resource-name"] = [r["name"] for r in term["resources"]]
|
|
1510
|
+
args["--resource-url"] = [r["url"] for r in term["resources"]]
|
|
1511
|
+
|
|
1512
|
+
result = client.create_term(args)
|
|
1513
|
+
|
|
1514
|
+
# Check if result contains an ID (indicates successful creation)
|
|
1515
|
+
if result and isinstance(result, dict) and result.get("id"):
|
|
1516
|
+
success_count += 1
|
|
1517
|
+
term_id = result.get("id")
|
|
1518
|
+
console.print(f"[green]Created: {term['name']} (ID: {term_id})[/green]")
|
|
1519
|
+
elif result and not (isinstance(result, dict) and "error" in result):
|
|
1520
|
+
# Got a response but no ID - might be an issue
|
|
1521
|
+
console.print(f"[yellow]WARNING: Response received for {term['name']} but no ID returned[/yellow]")
|
|
1522
|
+
console.print(f"[dim]Response: {json.dumps(result, indent=2)[:200]}...[/dim]")
|
|
1523
|
+
failed_count += 1
|
|
1524
|
+
failed_terms.append({"name": term["name"], "error": "No ID in response"})
|
|
1525
|
+
else:
|
|
1526
|
+
failed_count += 1
|
|
1527
|
+
error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else "No response"
|
|
1528
|
+
failed_terms.append({"name": term["name"], "error": error_msg})
|
|
1529
|
+
console.print(f"[red]FAILED: {term['name']} - {error_msg}[/red]")
|
|
1530
|
+
|
|
1531
|
+
except Exception as e:
|
|
1532
|
+
failed_count += 1
|
|
1533
|
+
failed_terms.append({"name": term["name"], "error": str(e)})
|
|
1534
|
+
console.print(f"[red]FAILED: {term['name']} - {str(e)}[/red]")
|
|
1535
|
+
|
|
1536
|
+
# Summary
|
|
1537
|
+
console.print("\n" + "="*60)
|
|
1538
|
+
console.print(f"[cyan]Import Summary:[/cyan]")
|
|
1539
|
+
console.print(f" Total terms: {len(terms)}")
|
|
1540
|
+
console.print(f" [green]Successfully created: {success_count}[/green]")
|
|
1541
|
+
console.print(f" [red]Failed: {failed_count}[/red]")
|
|
1542
|
+
|
|
1543
|
+
if failed_terms:
|
|
1544
|
+
console.print("\n[red]Failed Terms:[/red]")
|
|
1545
|
+
for ft in failed_terms:
|
|
1546
|
+
console.print(f" • {ft['name']}: {ft['error']}")
|
|
1547
|
+
|
|
1548
|
+
except Exception as e:
|
|
1549
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
@term.command(name="import-json")
|
|
1553
|
+
@click.option("--json-file", required=True, type=click.Path(exists=True), help="Path to JSON file with terms")
|
|
1554
|
+
@click.option("--dry-run", is_flag=True, help="Preview terms without creating them")
|
|
1555
|
+
def import_terms_from_json(json_file, dry_run):
|
|
1556
|
+
"""Bulk import glossary terms from a JSON file.
|
|
1557
|
+
|
|
1558
|
+
JSON Format:
|
|
1559
|
+
[
|
|
1560
|
+
{
|
|
1561
|
+
"name": "Term Name",
|
|
1562
|
+
"description": "Description",
|
|
1563
|
+
"domain_id": "domain-guid",
|
|
1564
|
+
"status": "Draft",
|
|
1565
|
+
"acronyms": ["API", "REST"],
|
|
1566
|
+
"owner_ids": ["owner-guid-1"],
|
|
1567
|
+
"resources": [
|
|
1568
|
+
{"name": "Resource Name", "url": "https://example.com"}
|
|
1569
|
+
]
|
|
1570
|
+
}
|
|
1571
|
+
]
|
|
1572
|
+
|
|
1573
|
+
Each term must include domain_id.
|
|
1574
|
+
"""
|
|
1575
|
+
try:
|
|
1576
|
+
client = UnifiedCatalogClient()
|
|
1577
|
+
|
|
1578
|
+
# Read and parse JSON
|
|
1579
|
+
with open(json_file, 'r', encoding='utf-8') as f:
|
|
1580
|
+
terms = json.load(f)
|
|
1581
|
+
|
|
1582
|
+
if not isinstance(terms, list):
|
|
1583
|
+
console.print("[red]ERROR:[/red] JSON file must contain an array of terms")
|
|
1584
|
+
return
|
|
1585
|
+
|
|
1586
|
+
if not terms:
|
|
1587
|
+
console.print("[yellow]No terms found in JSON file.[/yellow]")
|
|
1588
|
+
return
|
|
1589
|
+
|
|
1590
|
+
console.print(f"[cyan]Found {len(terms)} term(s) in JSON file[/cyan]")
|
|
1591
|
+
|
|
1592
|
+
if dry_run:
|
|
1593
|
+
console.print("\n[yellow]DRY RUN - Preview of terms to be created:[/yellow]\n")
|
|
1594
|
+
_format_json_output(terms)
|
|
1595
|
+
return
|
|
1596
|
+
|
|
1597
|
+
# Import terms
|
|
1598
|
+
success_count = 0
|
|
1599
|
+
failed_count = 0
|
|
1600
|
+
failed_terms = []
|
|
1601
|
+
|
|
1602
|
+
with console.status("[bold green]Importing terms...") as status:
|
|
1603
|
+
for i, term in enumerate(terms, 1):
|
|
1604
|
+
term_name = term.get("name", f"Term {i}")
|
|
1605
|
+
status.update(f"[bold green]Creating term {i}/{len(terms)}: {term_name}")
|
|
1606
|
+
|
|
1607
|
+
try:
|
|
1608
|
+
args = {
|
|
1609
|
+
"--name": [term.get("name", "")],
|
|
1610
|
+
"--description": [term.get("description", "")],
|
|
1611
|
+
"--governance-domain-id": [term.get("domain_id", "")],
|
|
1612
|
+
"--status": [term.get("status", "Draft")],
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if term.get("acronyms"):
|
|
1616
|
+
args["--acronym"] = term["acronyms"]
|
|
1617
|
+
|
|
1618
|
+
if term.get("owner_ids"):
|
|
1619
|
+
args["--owner-id"] = term["owner_ids"]
|
|
1620
|
+
|
|
1621
|
+
if term.get("resources"):
|
|
1622
|
+
args["--resource-name"] = [r.get("name", "") for r in term["resources"]]
|
|
1623
|
+
args["--resource-url"] = [r.get("url", "") for r in term["resources"]]
|
|
1624
|
+
|
|
1625
|
+
result = client.create_term(args)
|
|
1626
|
+
|
|
1627
|
+
# Check if result contains an ID (indicates successful creation)
|
|
1628
|
+
if result and isinstance(result, dict) and result.get("id"):
|
|
1629
|
+
success_count += 1
|
|
1630
|
+
term_id = result.get("id")
|
|
1631
|
+
console.print(f"[green]Created: {term_name} (ID: {term_id})[/green]")
|
|
1632
|
+
elif result and not (isinstance(result, dict) and "error" in result):
|
|
1633
|
+
# Got a response but no ID - might be an issue
|
|
1634
|
+
console.print(f"[yellow]WARNING: Response received for {term_name} but no ID returned[/yellow]")
|
|
1635
|
+
console.print(f"[dim]Response: {json.dumps(result, indent=2)[:200]}...[/dim]")
|
|
1636
|
+
failed_count += 1
|
|
1637
|
+
failed_terms.append({"name": term_name, "error": "No ID in response"})
|
|
1638
|
+
else:
|
|
1639
|
+
failed_count += 1
|
|
1640
|
+
error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else "No response"
|
|
1641
|
+
failed_terms.append({"name": term_name, "error": error_msg})
|
|
1642
|
+
console.print(f"[red]FAILED: {term_name} - {error_msg}[/red]")
|
|
1643
|
+
|
|
1644
|
+
except Exception as e:
|
|
1645
|
+
failed_count += 1
|
|
1646
|
+
failed_terms.append({"name": term_name, "error": str(e)})
|
|
1647
|
+
console.print(f"[red]FAILED: {term_name} - {str(e)}[/red]")
|
|
1648
|
+
|
|
1649
|
+
# Summary
|
|
1650
|
+
console.print("\n" + "="*60)
|
|
1651
|
+
console.print(f"[cyan]Import Summary:[/cyan]")
|
|
1652
|
+
console.print(f" Total terms: {len(terms)}")
|
|
1653
|
+
console.print(f" [green]Successfully created: {success_count}[/green]")
|
|
1654
|
+
console.print(f" [red]Failed: {failed_count}[/red]")
|
|
1655
|
+
|
|
1656
|
+
if failed_terms:
|
|
1657
|
+
console.print("\n[red]Failed Terms:[/red]")
|
|
1658
|
+
for ft in failed_terms:
|
|
1659
|
+
console.print(f" • {ft['name']}: {ft['error']}")
|
|
1660
|
+
|
|
1661
|
+
except Exception as e:
|
|
1662
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
@term.command(name="update-csv")
|
|
1666
|
+
@click.option("--csv-file", required=True, type=click.Path(exists=True), help="Path to CSV file with term updates")
|
|
1667
|
+
@click.option("--dry-run", is_flag=True, help="Preview updates without applying them")
|
|
1668
|
+
def update_terms_from_csv(csv_file, dry_run):
|
|
1669
|
+
"""Bulk update glossary terms from a CSV file.
|
|
1670
|
+
|
|
1671
|
+
CSV Format:
|
|
1672
|
+
term_id,name,description,status,parent_id,acronyms,owner_ids,add_acronyms,add_owner_ids
|
|
1673
|
+
|
|
1674
|
+
Required:
|
|
1675
|
+
- term_id: The ID of the term to update
|
|
1676
|
+
|
|
1677
|
+
Optional (leave empty to skip update):
|
|
1678
|
+
- name: New term name (replaces existing)
|
|
1679
|
+
- description: New description (replaces existing)
|
|
1680
|
+
- status: New status (Draft, Published, Archived)
|
|
1681
|
+
- parent_id: Parent term ID for hierarchical relationships (replaces existing)
|
|
1682
|
+
- acronyms: New acronyms separated by semicolons (replaces all existing)
|
|
1683
|
+
- owner_ids: New owner IDs separated by semicolons (replaces all existing)
|
|
1684
|
+
- add_acronyms: Acronyms to add separated by semicolons (preserves existing)
|
|
1685
|
+
- add_owner_ids: Owner IDs to add separated by semicolons (preserves existing)
|
|
1686
|
+
|
|
1687
|
+
Example CSV:
|
|
1688
|
+
term_id,name,description,status,parent_id,add_acronyms,add_owner_ids
|
|
1689
|
+
abc-123,,Updated description,Published,parent-term-guid,API;REST,user1@company.com
|
|
1690
|
+
def-456,New Name,,,parent-term-guid,SQL,
|
|
1691
|
+
"""
|
|
1692
|
+
import csv
|
|
1693
|
+
|
|
1694
|
+
try:
|
|
1695
|
+
# Read CSV file
|
|
1696
|
+
with open(csv_file, 'r', encoding='utf-8') as f:
|
|
1697
|
+
reader = csv.DictReader(f)
|
|
1698
|
+
updates = list(reader)
|
|
1699
|
+
|
|
1700
|
+
if not updates:
|
|
1701
|
+
console.print("[yellow]No updates found in CSV file.[/yellow]")
|
|
1702
|
+
return
|
|
1703
|
+
|
|
1704
|
+
console.print(f"Found {len(updates)} term(s) to update in CSV file")
|
|
1705
|
+
|
|
1706
|
+
# Dry run preview
|
|
1707
|
+
if dry_run:
|
|
1708
|
+
console.print("\n[cyan]DRY RUN - Preview of updates to be applied:[/cyan]\n")
|
|
1709
|
+
|
|
1710
|
+
table = Table(title="Terms to Update")
|
|
1711
|
+
table.add_column("#", style="cyan")
|
|
1712
|
+
table.add_column("Term ID", style="yellow")
|
|
1713
|
+
table.add_column("Updates", style="white")
|
|
1714
|
+
|
|
1715
|
+
for idx, update in enumerate(updates, 1):
|
|
1716
|
+
term_id = update.get('term_id', '').strip()
|
|
1717
|
+
if not term_id:
|
|
1718
|
+
continue
|
|
1719
|
+
|
|
1720
|
+
changes = []
|
|
1721
|
+
if update.get('name', '').strip():
|
|
1722
|
+
changes.append(f"name: {update['name']}")
|
|
1723
|
+
if update.get('description', '').strip():
|
|
1724
|
+
changes.append(f"desc: {update['description'][:50]}...")
|
|
1725
|
+
if update.get('status', '').strip():
|
|
1726
|
+
changes.append(f"status: {update['status']}")
|
|
1727
|
+
if update.get('parent_id', '').strip():
|
|
1728
|
+
changes.append(f"parent: {update['parent_id'][:20]}...")
|
|
1729
|
+
if update.get('acronyms', '').strip():
|
|
1730
|
+
changes.append(f"acronyms: {update['acronyms']}")
|
|
1731
|
+
if update.get('add_acronyms', '').strip():
|
|
1732
|
+
changes.append(f"add acronyms: {update['add_acronyms']}")
|
|
1733
|
+
if update.get('owner_ids', '').strip():
|
|
1734
|
+
changes.append(f"owners: {update['owner_ids']}")
|
|
1735
|
+
if update.get('add_owner_ids', '').strip():
|
|
1736
|
+
changes.append(f"add owners: {update['add_owner_ids']}")
|
|
1737
|
+
|
|
1738
|
+
table.add_row(str(idx), term_id[:36], ", ".join(changes) if changes else "No changes")
|
|
1739
|
+
|
|
1740
|
+
console.print(table)
|
|
1741
|
+
console.print(f"\n[yellow]Total terms to update: {len(updates)}[/yellow]")
|
|
1742
|
+
return
|
|
1743
|
+
|
|
1744
|
+
# Apply updates
|
|
1745
|
+
console.print("\n[cyan]Updating terms...[/cyan]\n")
|
|
1746
|
+
|
|
1747
|
+
client = UnifiedCatalogClient()
|
|
1748
|
+
success_count = 0
|
|
1749
|
+
failed_count = 0
|
|
1750
|
+
failed_terms = []
|
|
1751
|
+
|
|
1752
|
+
for idx, update in enumerate(updates, 1):
|
|
1753
|
+
term_id = update.get('term_id', '').strip()
|
|
1754
|
+
if not term_id:
|
|
1755
|
+
console.print(f"[yellow]Skipping row {idx}: Missing term_id[/yellow]")
|
|
1756
|
+
continue
|
|
1757
|
+
|
|
1758
|
+
# Build update arguments
|
|
1759
|
+
args = {"--term-id": [term_id]}
|
|
1760
|
+
|
|
1761
|
+
# Add replace operations
|
|
1762
|
+
if update.get('name', '').strip():
|
|
1763
|
+
args['--name'] = [update['name'].strip()]
|
|
1764
|
+
if update.get('description', '').strip():
|
|
1765
|
+
args['--description'] = [update['description'].strip()]
|
|
1766
|
+
if update.get('status', '').strip():
|
|
1767
|
+
args['--status'] = [update['status'].strip()]
|
|
1768
|
+
if update.get('parent_id', '').strip():
|
|
1769
|
+
args['--parent-id'] = [update['parent_id'].strip()]
|
|
1770
|
+
if update.get('acronyms', '').strip():
|
|
1771
|
+
args['--acronym'] = [a.strip() for a in update['acronyms'].split(';') if a.strip()]
|
|
1772
|
+
if update.get('owner_ids', '').strip():
|
|
1773
|
+
args['--owner-id'] = [o.strip() for o in update['owner_ids'].split(';') if o.strip()]
|
|
1774
|
+
|
|
1775
|
+
# Add "add" operations
|
|
1776
|
+
if update.get('add_acronyms', '').strip():
|
|
1777
|
+
args['--add-acronym'] = [a.strip() for a in update['add_acronyms'].split(';') if a.strip()]
|
|
1778
|
+
if update.get('add_owner_ids', '').strip():
|
|
1779
|
+
args['--add-owner-id'] = [o.strip() for o in update['add_owner_ids'].split(';') if o.strip()]
|
|
1780
|
+
|
|
1781
|
+
# Display progress
|
|
1782
|
+
display_name = update.get('name', term_id[:36])
|
|
1783
|
+
console.status(f"[{idx}/{len(updates)}] Updating: {display_name}...")
|
|
1784
|
+
|
|
1785
|
+
try:
|
|
1786
|
+
result = client.update_term(args)
|
|
1787
|
+
console.print(f"[green]SUCCESS:[/green] Updated term {idx}/{len(updates)}")
|
|
1788
|
+
success_count += 1
|
|
1789
|
+
except Exception as e:
|
|
1790
|
+
error_msg = str(e)
|
|
1791
|
+
console.print(f"[red]FAILED:[/red] {display_name}: {error_msg}")
|
|
1792
|
+
failed_terms.append({'term_id': term_id, 'name': display_name, 'error': error_msg})
|
|
1793
|
+
failed_count += 1
|
|
1794
|
+
|
|
1795
|
+
# Rate limiting
|
|
1796
|
+
time.sleep(0.2)
|
|
1797
|
+
|
|
1798
|
+
# Summary
|
|
1799
|
+
console.print("\n" + "="*60)
|
|
1800
|
+
console.print(f"[cyan]Update Summary:[/cyan]")
|
|
1801
|
+
console.print(f" Total terms: {len(updates)}")
|
|
1802
|
+
console.print(f" [green]Successfully updated: {success_count}[/green]")
|
|
1803
|
+
console.print(f" [red]Failed: {failed_count}[/red]")
|
|
1804
|
+
|
|
1805
|
+
if failed_terms:
|
|
1806
|
+
console.print("\n[red]Failed Updates:[/red]")
|
|
1807
|
+
for ft in failed_terms:
|
|
1808
|
+
console.print(f" • {ft['name']}: {ft['error']}")
|
|
1809
|
+
|
|
1810
|
+
except Exception as e:
|
|
1811
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
@term.command(name="update-json")
|
|
1815
|
+
@click.option("--json-file", required=True, type=click.Path(exists=True), help="Path to JSON file with term updates")
|
|
1816
|
+
@click.option("--dry-run", is_flag=True, help="Preview updates without applying them")
|
|
1817
|
+
def update_terms_from_json(json_file, dry_run):
|
|
1818
|
+
"""Bulk update glossary terms from a JSON file.
|
|
1819
|
+
|
|
1820
|
+
JSON Format:
|
|
1821
|
+
{
|
|
1822
|
+
"updates": [
|
|
1823
|
+
{
|
|
1824
|
+
"term_id": "term-guid",
|
|
1825
|
+
"name": "New Name", // Optional: Replace name
|
|
1826
|
+
"description": "New description", // Optional: Replace description
|
|
1827
|
+
"status": "Published", // Optional: Change status
|
|
1828
|
+
"parent_id": "parent-term-guid", // Optional: Set parent term (hierarchical)
|
|
1829
|
+
"acronyms": ["API", "REST"], // Optional: Replace all acronyms
|
|
1830
|
+
"owner_ids": ["user@company.com"], // Optional: Replace all owners
|
|
1831
|
+
"add_acronyms": ["SQL"], // Optional: Add acronyms (preserves existing)
|
|
1832
|
+
"add_owner_ids": ["user2@company.com"] // Optional: Add owners (preserves existing)
|
|
1833
|
+
}
|
|
1834
|
+
]
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
Note: Leave fields empty or omit them to skip that update.
|
|
1838
|
+
"""
|
|
1839
|
+
import json
|
|
1840
|
+
|
|
1841
|
+
try:
|
|
1842
|
+
# Read JSON file
|
|
1843
|
+
with open(json_file, 'r', encoding='utf-8') as f:
|
|
1844
|
+
data = json.load(f)
|
|
1845
|
+
|
|
1846
|
+
updates = data.get('updates', [])
|
|
1847
|
+
|
|
1848
|
+
if not updates:
|
|
1849
|
+
console.print("[yellow]No updates found in JSON file.[/yellow]")
|
|
1850
|
+
return
|
|
1851
|
+
|
|
1852
|
+
console.print(f"Found {len(updates)} term(s) to update in JSON file")
|
|
1853
|
+
|
|
1854
|
+
# Dry run preview
|
|
1855
|
+
if dry_run:
|
|
1856
|
+
console.print("\n[cyan]DRY RUN - Preview of updates to be applied:[/cyan]\n")
|
|
1857
|
+
|
|
1858
|
+
# Display updates in colored JSON
|
|
1859
|
+
from rich.syntax import Syntax
|
|
1860
|
+
json_str = json.dumps(data, indent=2)
|
|
1861
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
|
|
1862
|
+
console.print(syntax)
|
|
1863
|
+
|
|
1864
|
+
console.print(f"\n[yellow]Total terms to update: {len(updates)}[/yellow]")
|
|
1865
|
+
return
|
|
1866
|
+
|
|
1867
|
+
# Apply updates
|
|
1868
|
+
console.print("\n[cyan]Updating terms...[/cyan]\n")
|
|
1869
|
+
|
|
1870
|
+
client = UnifiedCatalogClient()
|
|
1871
|
+
success_count = 0
|
|
1872
|
+
failed_count = 0
|
|
1873
|
+
failed_terms = []
|
|
1874
|
+
|
|
1875
|
+
for idx, update in enumerate(updates, 1):
|
|
1876
|
+
term_id = update.get('term_id', '').strip() if isinstance(update.get('term_id'), str) else ''
|
|
1877
|
+
if not term_id:
|
|
1878
|
+
console.print(f"[yellow]Skipping update {idx}: Missing term_id[/yellow]")
|
|
1879
|
+
continue
|
|
1880
|
+
|
|
1881
|
+
# Build update arguments
|
|
1882
|
+
args = {"--term-id": [term_id]}
|
|
1883
|
+
|
|
1884
|
+
# Add replace operations
|
|
1885
|
+
if update.get('name'):
|
|
1886
|
+
args['--name'] = [update['name']]
|
|
1887
|
+
if update.get('description'):
|
|
1888
|
+
args['--description'] = [update['description']]
|
|
1889
|
+
if update.get('status'):
|
|
1890
|
+
args['--status'] = [update['status']]
|
|
1891
|
+
if update.get('parent_id'):
|
|
1892
|
+
args['--parent-id'] = [update['parent_id']]
|
|
1893
|
+
if update.get('acronyms'):
|
|
1894
|
+
args['--acronym'] = update['acronyms'] if isinstance(update['acronyms'], list) else [update['acronyms']]
|
|
1895
|
+
if update.get('owner_ids'):
|
|
1896
|
+
args['--owner-id'] = update['owner_ids'] if isinstance(update['owner_ids'], list) else [update['owner_ids']]
|
|
1897
|
+
|
|
1898
|
+
# Add "add" operations
|
|
1899
|
+
if update.get('add_acronyms'):
|
|
1900
|
+
args['--add-acronym'] = update['add_acronyms'] if isinstance(update['add_acronyms'], list) else [update['add_acronyms']]
|
|
1901
|
+
if update.get('add_owner_ids'):
|
|
1902
|
+
args['--add-owner-id'] = update['add_owner_ids'] if isinstance(update['add_owner_ids'], list) else [update['add_owner_ids']]
|
|
1903
|
+
|
|
1904
|
+
# Display progress
|
|
1905
|
+
display_name = update.get('name', term_id[:36])
|
|
1906
|
+
console.status(f"[{idx}/{len(updates)}] Updating: {display_name}...")
|
|
1907
|
+
|
|
1908
|
+
try:
|
|
1909
|
+
result = client.update_term(args)
|
|
1910
|
+
console.print(f"[green]SUCCESS:[/green] Updated term {idx}/{len(updates)}")
|
|
1911
|
+
success_count += 1
|
|
1912
|
+
except Exception as e:
|
|
1913
|
+
error_msg = str(e)
|
|
1914
|
+
console.print(f"[red]FAILED:[/red] {display_name}: {error_msg}")
|
|
1915
|
+
failed_terms.append({'term_id': term_id, 'name': display_name, 'error': error_msg})
|
|
1916
|
+
failed_count += 1
|
|
1917
|
+
|
|
1918
|
+
# Rate limiting
|
|
1919
|
+
time.sleep(0.2)
|
|
1920
|
+
|
|
1921
|
+
# Summary
|
|
1922
|
+
console.print("\n" + "="*60)
|
|
1923
|
+
console.print(f"[cyan]Update Summary:[/cyan]")
|
|
1924
|
+
console.print(f" Total terms: {len(updates)}")
|
|
1925
|
+
console.print(f" [green]Successfully updated: {success_count}[/green]")
|
|
1926
|
+
console.print(f" [red]Failed: {failed_count}[/red]")
|
|
1927
|
+
|
|
1928
|
+
if failed_terms:
|
|
1929
|
+
console.print("\n[red]Failed Updates:[/red]")
|
|
1930
|
+
for ft in failed_terms:
|
|
1931
|
+
console.print(f" • {ft['name']}: {ft['error']}")
|
|
1932
|
+
|
|
1933
|
+
except Exception as e:
|
|
1934
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
@term.command(name="query")
|
|
1938
|
+
@click.option("--ids", multiple=True, help="Filter by specific term IDs (GUIDs)")
|
|
1939
|
+
@click.option("--domain-ids", multiple=True, help="Filter by domain IDs (GUIDs)")
|
|
1940
|
+
@click.option("--name-keyword", help="Filter by name keyword (partial match)")
|
|
1941
|
+
@click.option("--acronyms", multiple=True, help="Filter by acronyms")
|
|
1942
|
+
@click.option("--owners", multiple=True, help="Filter by owner IDs (GUIDs)")
|
|
1943
|
+
@click.option("--status", type=click.Choice(["DRAFT", "PUBLISHED", "EXPIRED"], case_sensitive=False),
|
|
1944
|
+
help="Filter by status")
|
|
1945
|
+
@click.option("--multi-status", multiple=True,
|
|
1946
|
+
type=click.Choice(["DRAFT", "PUBLISHED", "EXPIRED"], case_sensitive=False),
|
|
1947
|
+
help="Filter by multiple statuses")
|
|
1948
|
+
@click.option("--skip", type=int, default=0, help="Number of items to skip (pagination)")
|
|
1949
|
+
@click.option("--top", type=int, default=100, help="Number of items to return (max 1000)")
|
|
1950
|
+
@click.option("--order-by-field", help="Field to sort by (e.g., 'name', 'status')")
|
|
1951
|
+
@click.option("--order-by-direction", type=click.Choice(["asc", "desc"]), default="asc",
|
|
1952
|
+
help="Sort direction")
|
|
1953
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
1954
|
+
def query_terms(ids, domain_ids, name_keyword, acronyms, owners, status, multi_status,
|
|
1955
|
+
skip, top, order_by_field, order_by_direction, output):
|
|
1956
|
+
"""Query terms with advanced filters.
|
|
1957
|
+
|
|
1958
|
+
Perform complex searches across glossary terms using multiple filter criteria.
|
|
1959
|
+
Supports pagination and custom sorting.
|
|
1960
|
+
|
|
1961
|
+
Examples:
|
|
1962
|
+
# Find all terms in a specific domain
|
|
1963
|
+
pvw uc term query --domain-ids <domain-guid>
|
|
1964
|
+
|
|
1965
|
+
# Search by keyword
|
|
1966
|
+
pvw uc term query --name-keyword "customer"
|
|
1967
|
+
|
|
1968
|
+
# Filter by acronym
|
|
1969
|
+
pvw uc term query --acronyms "PII" "GDPR"
|
|
1970
|
+
|
|
1971
|
+
# Filter by owner and status
|
|
1972
|
+
pvw uc term query --owners <user-guid> --status PUBLISHED
|
|
1973
|
+
|
|
1974
|
+
# Pagination example
|
|
1975
|
+
pvw uc term query --skip 0 --top 50 --order-by-field name --order-by-direction desc
|
|
1976
|
+
"""
|
|
1977
|
+
try:
|
|
1978
|
+
client = UnifiedCatalogClient()
|
|
1979
|
+
args = {}
|
|
1980
|
+
|
|
1981
|
+
# Build args dict from parameters
|
|
1982
|
+
if ids:
|
|
1983
|
+
args["--ids"] = list(ids)
|
|
1984
|
+
if domain_ids:
|
|
1985
|
+
args["--domain-ids"] = list(domain_ids)
|
|
1986
|
+
if name_keyword:
|
|
1987
|
+
args["--name-keyword"] = [name_keyword]
|
|
1988
|
+
if acronyms:
|
|
1989
|
+
args["--acronyms"] = list(acronyms)
|
|
1990
|
+
if owners:
|
|
1991
|
+
args["--owners"] = list(owners)
|
|
1992
|
+
if status:
|
|
1993
|
+
args["--status"] = [status]
|
|
1994
|
+
if multi_status:
|
|
1995
|
+
args["--multi-status"] = list(multi_status)
|
|
1996
|
+
if skip:
|
|
1997
|
+
args["--skip"] = [str(skip)]
|
|
1998
|
+
if top:
|
|
1999
|
+
args["--top"] = [str(top)]
|
|
2000
|
+
if order_by_field:
|
|
2001
|
+
args["--order-by-field"] = [order_by_field]
|
|
2002
|
+
args["--order-by-direction"] = [order_by_direction]
|
|
2003
|
+
|
|
2004
|
+
result = client.query_terms(args)
|
|
2005
|
+
|
|
2006
|
+
if output == "json":
|
|
2007
|
+
console.print_json(data=result)
|
|
2008
|
+
else:
|
|
2009
|
+
terms = result.get("value", []) if result else []
|
|
2010
|
+
|
|
2011
|
+
if not terms:
|
|
2012
|
+
console.print("[yellow]No terms found matching the query.[/yellow]")
|
|
2013
|
+
return
|
|
2014
|
+
|
|
2015
|
+
# Check for pagination
|
|
2016
|
+
next_link = result.get("nextLink")
|
|
2017
|
+
if next_link:
|
|
2018
|
+
console.print(f"[dim]Note: More results available (nextLink provided)[/dim]\n")
|
|
2019
|
+
|
|
2020
|
+
table = Table(title=f"Query Results ({len(terms)} found)", show_header=True)
|
|
2021
|
+
table.add_column("Name", style="cyan")
|
|
2022
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
2023
|
+
table.add_column("Domain", style="yellow", no_wrap=True)
|
|
2024
|
+
table.add_column("Status", style="white")
|
|
2025
|
+
table.add_column("Acronyms", style="green")
|
|
2026
|
+
|
|
2027
|
+
for term in terms:
|
|
2028
|
+
acronyms_list = term.get("acronyms", [])
|
|
2029
|
+
acronyms_display = ", ".join(acronyms_list[:2]) if acronyms_list else "N/A"
|
|
2030
|
+
if len(acronyms_list) > 2:
|
|
2031
|
+
acronyms_display += f" +{len(acronyms_list) - 2}"
|
|
2032
|
+
|
|
2033
|
+
table.add_row(
|
|
2034
|
+
term.get("name", "N/A"),
|
|
2035
|
+
term.get("id", "N/A")[:13] + "...",
|
|
2036
|
+
term.get("domain", "N/A")[:13] + "...",
|
|
2037
|
+
term.get("status", "N/A"),
|
|
2038
|
+
acronyms_display
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
console.print(table)
|
|
2042
|
+
|
|
2043
|
+
# Show pagination info
|
|
2044
|
+
if skip > 0 or next_link:
|
|
2045
|
+
console.print(f"\n[dim]Showing items {skip + 1} to {skip + len(terms)}[/dim]")
|
|
2046
|
+
|
|
2047
|
+
except Exception as e:
|
|
2048
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
# ========================================
|
|
2052
|
+
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
2053
|
+
# ========================================
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
@uc.group()
|
|
2057
|
+
def objective():
|
|
2058
|
+
"""Manage objectives and key results (OKRs)."""
|
|
2059
|
+
pass
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
@objective.command()
|
|
2063
|
+
@click.option("--definition", required=True, help="Definition of the objective")
|
|
2064
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
2065
|
+
@click.option(
|
|
2066
|
+
"--status",
|
|
2067
|
+
required=False,
|
|
2068
|
+
default="Draft",
|
|
2069
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
2070
|
+
help="Status of the objective",
|
|
2071
|
+
)
|
|
2072
|
+
@click.option(
|
|
2073
|
+
"--owner-id",
|
|
2074
|
+
required=False,
|
|
2075
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
2076
|
+
multiple=True,
|
|
2077
|
+
)
|
|
2078
|
+
@click.option(
|
|
2079
|
+
"--target-date", required=False, help="Target date (ISO format: 2025-12-30T14:00:00.000Z)"
|
|
2080
|
+
)
|
|
2081
|
+
def create(definition, domain_id, status, owner_id, target_date):
|
|
2082
|
+
"""Create a new objective."""
|
|
2083
|
+
try:
|
|
2084
|
+
client = UnifiedCatalogClient()
|
|
2085
|
+
|
|
2086
|
+
args = {
|
|
2087
|
+
"--definition": [definition],
|
|
2088
|
+
"--governance-domain-id": [domain_id],
|
|
2089
|
+
"--status": [status],
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
if owner_id:
|
|
2093
|
+
args["--owner-id"] = list(owner_id)
|
|
2094
|
+
if target_date:
|
|
2095
|
+
args["--target-date"] = [target_date]
|
|
2096
|
+
|
|
2097
|
+
result = client.create_objective(args)
|
|
2098
|
+
|
|
2099
|
+
if not result:
|
|
2100
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2101
|
+
return
|
|
2102
|
+
if isinstance(result, dict) and "error" in result:
|
|
2103
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
2104
|
+
return
|
|
2105
|
+
|
|
2106
|
+
console.print(f"[green] SUCCESS:[/green] Created objective")
|
|
2107
|
+
console.print(json.dumps(result, indent=2))
|
|
2108
|
+
|
|
2109
|
+
except Exception as e:
|
|
2110
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2111
|
+
|
|
2112
|
+
|
|
2113
|
+
@objective.command(name="list")
|
|
2114
|
+
@click.option("--domain-id", required=True, help="Governance domain ID to list objectives from")
|
|
2115
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
2116
|
+
def list_objectives(domain_id, output_json):
|
|
2117
|
+
"""List all objectives in a governance domain."""
|
|
2118
|
+
try:
|
|
2119
|
+
client = UnifiedCatalogClient()
|
|
2120
|
+
args = {"--governance-domain-id": [domain_id]}
|
|
2121
|
+
result = client.get_objectives(args)
|
|
2122
|
+
|
|
2123
|
+
if not result:
|
|
2124
|
+
console.print("[yellow]No objectives found.[/yellow]")
|
|
2125
|
+
return
|
|
2126
|
+
|
|
2127
|
+
# Handle response format
|
|
2128
|
+
if isinstance(result, (list, tuple)):
|
|
2129
|
+
objectives = result
|
|
2130
|
+
elif isinstance(result, dict):
|
|
2131
|
+
objectives = result.get("value", [])
|
|
2132
|
+
else:
|
|
2133
|
+
objectives = []
|
|
2134
|
+
|
|
2135
|
+
if not objectives:
|
|
2136
|
+
console.print("[yellow]No objectives found.[/yellow]")
|
|
2137
|
+
return
|
|
2138
|
+
|
|
2139
|
+
# Output in JSON format if requested
|
|
2140
|
+
if output_json:
|
|
2141
|
+
_format_json_output(objectives)
|
|
2142
|
+
return
|
|
2143
|
+
|
|
2144
|
+
table = Table(title="Objectives")
|
|
2145
|
+
table.add_column("ID", style="cyan")
|
|
2146
|
+
table.add_column("Definition", style="green")
|
|
2147
|
+
table.add_column("Status", style="yellow")
|
|
2148
|
+
table.add_column("Target Date", style="blue")
|
|
2149
|
+
|
|
2150
|
+
for obj in objectives:
|
|
2151
|
+
definition = obj.get("definition", "")
|
|
2152
|
+
if len(definition) > 50:
|
|
2153
|
+
definition = definition[:50] + "..."
|
|
2154
|
+
|
|
2155
|
+
table.add_row(
|
|
2156
|
+
obj.get("id", "N/A"),
|
|
2157
|
+
definition,
|
|
2158
|
+
obj.get("status", "N/A"),
|
|
2159
|
+
obj.get("targetDate", "N/A"),
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2162
|
+
console.print(table)
|
|
2163
|
+
|
|
2164
|
+
except Exception as e:
|
|
2165
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
@objective.command()
|
|
2169
|
+
@click.option("--objective-id", required=True, help="ID of the objective")
|
|
2170
|
+
def show(objective_id):
|
|
2171
|
+
"""Show details of an objective."""
|
|
2172
|
+
try:
|
|
2173
|
+
client = UnifiedCatalogClient()
|
|
2174
|
+
args = {"--objective-id": [objective_id]}
|
|
2175
|
+
result = client.get_objective_by_id(args)
|
|
2176
|
+
|
|
2177
|
+
if not result:
|
|
2178
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2179
|
+
return
|
|
2180
|
+
if isinstance(result, dict) and "error" in result:
|
|
2181
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Objective not found')}")
|
|
2182
|
+
return
|
|
2183
|
+
|
|
2184
|
+
console.print(json.dumps(result, indent=2))
|
|
2185
|
+
|
|
2186
|
+
except Exception as e:
|
|
2187
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2188
|
+
|
|
2189
|
+
|
|
2190
|
+
@objective.command(name="query")
|
|
2191
|
+
@click.option("--ids", multiple=True, help="Filter by specific objective IDs (GUIDs)")
|
|
2192
|
+
@click.option("--domain-ids", multiple=True, help="Filter by domain IDs (GUIDs)")
|
|
2193
|
+
@click.option("--definition", help="Filter by definition text (partial match)")
|
|
2194
|
+
@click.option("--owners", multiple=True, help="Filter by owner IDs (GUIDs)")
|
|
2195
|
+
@click.option("--status", type=click.Choice(["DRAFT", "ACTIVE", "COMPLETED", "ARCHIVED"], case_sensitive=False),
|
|
2196
|
+
help="Filter by status")
|
|
2197
|
+
@click.option("--multi-status", multiple=True,
|
|
2198
|
+
type=click.Choice(["DRAFT", "ACTIVE", "COMPLETED", "ARCHIVED"], case_sensitive=False),
|
|
2199
|
+
help="Filter by multiple statuses")
|
|
2200
|
+
@click.option("--skip", type=int, default=0, help="Number of items to skip (pagination)")
|
|
2201
|
+
@click.option("--top", type=int, default=100, help="Number of items to return (max 1000)")
|
|
2202
|
+
@click.option("--order-by-field", help="Field to sort by (e.g., 'name', 'status')")
|
|
2203
|
+
@click.option("--order-by-direction", type=click.Choice(["asc", "desc"]), default="asc",
|
|
2204
|
+
help="Sort direction")
|
|
2205
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
2206
|
+
def query_objectives(ids, domain_ids, definition, owners, status, multi_status,
|
|
2207
|
+
skip, top, order_by_field, order_by_direction, output):
|
|
2208
|
+
"""Query objectives with advanced filters.
|
|
2209
|
+
|
|
2210
|
+
Perform complex searches across OKR objectives using multiple filter criteria.
|
|
2211
|
+
Supports pagination and custom sorting.
|
|
2212
|
+
|
|
2213
|
+
Examples:
|
|
2214
|
+
# Find all objectives in a specific domain
|
|
2215
|
+
pvw uc objective query --domain-ids <domain-guid>
|
|
2216
|
+
|
|
2217
|
+
# Search by definition text
|
|
2218
|
+
pvw uc objective query --definition "customer satisfaction"
|
|
2219
|
+
|
|
2220
|
+
# Filter by owner and status
|
|
2221
|
+
pvw uc objective query --owners <user-guid> --status ACTIVE
|
|
2222
|
+
|
|
2223
|
+
# Find all completed objectives
|
|
2224
|
+
pvw uc objective query --multi-status COMPLETED ARCHIVED
|
|
2225
|
+
|
|
2226
|
+
# Pagination example
|
|
2227
|
+
pvw uc objective query --skip 0 --top 50 --order-by-field name --order-by-direction asc
|
|
2228
|
+
"""
|
|
2229
|
+
try:
|
|
2230
|
+
client = UnifiedCatalogClient()
|
|
2231
|
+
args = {}
|
|
2232
|
+
|
|
2233
|
+
# Build args dict from parameters
|
|
2234
|
+
if ids:
|
|
2235
|
+
args["--ids"] = list(ids)
|
|
2236
|
+
if domain_ids:
|
|
2237
|
+
args["--domain-ids"] = list(domain_ids)
|
|
2238
|
+
if definition:
|
|
2239
|
+
args["--definition"] = [definition]
|
|
2240
|
+
if owners:
|
|
2241
|
+
args["--owners"] = list(owners)
|
|
2242
|
+
if status:
|
|
2243
|
+
args["--status"] = [status]
|
|
2244
|
+
if multi_status:
|
|
2245
|
+
args["--multi-status"] = list(multi_status)
|
|
2246
|
+
if skip:
|
|
2247
|
+
args["--skip"] = [str(skip)]
|
|
2248
|
+
if top:
|
|
2249
|
+
args["--top"] = [str(top)]
|
|
2250
|
+
if order_by_field:
|
|
2251
|
+
args["--order-by-field"] = [order_by_field]
|
|
2252
|
+
args["--order-by-direction"] = [order_by_direction]
|
|
2253
|
+
|
|
2254
|
+
result = client.query_objectives(args)
|
|
2255
|
+
|
|
2256
|
+
if output == "json":
|
|
2257
|
+
console.print_json(data=result)
|
|
2258
|
+
else:
|
|
2259
|
+
objectives = result.get("value", []) if result else []
|
|
2260
|
+
|
|
2261
|
+
if not objectives:
|
|
2262
|
+
console.print("[yellow]No objectives found matching the query.[/yellow]")
|
|
2263
|
+
return
|
|
2264
|
+
|
|
2265
|
+
# Check for pagination
|
|
2266
|
+
next_link = result.get("nextLink")
|
|
2267
|
+
if next_link:
|
|
2268
|
+
console.print(f"[dim]Note: More results available (nextLink provided)[/dim]\n")
|
|
2269
|
+
|
|
2270
|
+
table = Table(title=f"Query Results ({len(objectives)} found)", show_header=True)
|
|
2271
|
+
table.add_column("Name", style="cyan")
|
|
2272
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
2273
|
+
table.add_column("Domain", style="yellow", no_wrap=True)
|
|
2274
|
+
table.add_column("Status", style="white")
|
|
2275
|
+
table.add_column("Owner", style="green", no_wrap=True)
|
|
2276
|
+
|
|
2277
|
+
for obj in objectives:
|
|
2278
|
+
owner_display = "N/A"
|
|
2279
|
+
if obj.get("owner"):
|
|
2280
|
+
owner_display = obj["owner"].get("id", "N/A")[:13] + "..."
|
|
2281
|
+
|
|
2282
|
+
table.add_row(
|
|
2283
|
+
obj.get("name", "N/A"),
|
|
2284
|
+
obj.get("id", "N/A")[:13] + "...",
|
|
2285
|
+
obj.get("domain", "N/A")[:13] + "...",
|
|
2286
|
+
obj.get("status", "N/A"),
|
|
2287
|
+
owner_display
|
|
2288
|
+
)
|
|
2289
|
+
|
|
2290
|
+
console.print(table)
|
|
2291
|
+
|
|
2292
|
+
# Show pagination info
|
|
2293
|
+
if skip > 0 or next_link:
|
|
2294
|
+
console.print(f"\n[dim]Showing items {skip + 1} to {skip + len(objectives)}[/dim]")
|
|
2295
|
+
|
|
2296
|
+
except Exception as e:
|
|
2297
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2298
|
+
|
|
2299
|
+
|
|
2300
|
+
# ========================================
|
|
2301
|
+
# CRITICAL DATA ELEMENTS (CDEs)
|
|
2302
|
+
# ========================================
|
|
2303
|
+
|
|
2304
|
+
|
|
2305
|
+
@uc.group()
|
|
2306
|
+
def cde():
|
|
2307
|
+
"""Manage critical data elements."""
|
|
2308
|
+
pass
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
@cde.command()
|
|
2312
|
+
@click.option("--name", required=True, help="Name of the critical data element")
|
|
2313
|
+
@click.option("--description", required=False, default="", help="Description of the CDE")
|
|
2314
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
2315
|
+
@click.option(
|
|
2316
|
+
"--data-type",
|
|
2317
|
+
required=True,
|
|
2318
|
+
type=click.Choice(["String", "Number", "Boolean", "Date", "DateTime"]),
|
|
2319
|
+
help="Data type of the CDE",
|
|
2320
|
+
)
|
|
2321
|
+
@click.option(
|
|
2322
|
+
"--status",
|
|
2323
|
+
required=False,
|
|
2324
|
+
default="Draft",
|
|
2325
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
2326
|
+
help="Status of the CDE",
|
|
2327
|
+
)
|
|
2328
|
+
@click.option(
|
|
2329
|
+
"--owner-id",
|
|
2330
|
+
required=False,
|
|
2331
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
2332
|
+
multiple=True,
|
|
2333
|
+
)
|
|
2334
|
+
def create(name, description, domain_id, data_type, status, owner_id):
|
|
2335
|
+
"""Create a new critical data element."""
|
|
2336
|
+
try:
|
|
2337
|
+
client = UnifiedCatalogClient()
|
|
2338
|
+
|
|
2339
|
+
args = {
|
|
2340
|
+
"--name": [name],
|
|
2341
|
+
"--description": [description],
|
|
2342
|
+
"--governance-domain-id": [domain_id],
|
|
2343
|
+
"--data-type": [data_type],
|
|
2344
|
+
"--status": [status],
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if owner_id:
|
|
2348
|
+
args["--owner-id"] = list(owner_id)
|
|
2349
|
+
|
|
2350
|
+
result = client.create_critical_data_element(args)
|
|
2351
|
+
|
|
2352
|
+
if not result:
|
|
2353
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2354
|
+
return
|
|
2355
|
+
if isinstance(result, dict) and "error" in result:
|
|
2356
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
2357
|
+
return
|
|
2358
|
+
|
|
2359
|
+
console.print(f"[green] SUCCESS:[/green] Created critical data element '{name}'")
|
|
2360
|
+
console.print(json.dumps(result, indent=2))
|
|
2361
|
+
|
|
2362
|
+
except Exception as e:
|
|
2363
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
@cde.command(name="list")
|
|
2367
|
+
@click.option("--domain-id", required=True, help="Governance domain ID to list CDEs from")
|
|
2368
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
2369
|
+
def list_cdes(domain_id, output_json):
|
|
2370
|
+
"""List all critical data elements in a governance domain."""
|
|
2371
|
+
try:
|
|
2372
|
+
client = UnifiedCatalogClient()
|
|
2373
|
+
args = {"--governance-domain-id": [domain_id]}
|
|
2374
|
+
result = client.get_critical_data_elements(args)
|
|
2375
|
+
|
|
2376
|
+
if not result:
|
|
2377
|
+
console.print("[yellow]No critical data elements found.[/yellow]")
|
|
2378
|
+
return
|
|
2379
|
+
|
|
2380
|
+
# Handle response format
|
|
2381
|
+
if isinstance(result, (list, tuple)):
|
|
2382
|
+
cdes = result
|
|
2383
|
+
elif isinstance(result, dict):
|
|
2384
|
+
cdes = result.get("value", [])
|
|
2385
|
+
else:
|
|
2386
|
+
cdes = []
|
|
2387
|
+
|
|
2388
|
+
if not cdes:
|
|
2389
|
+
console.print("[yellow]No critical data elements found.[/yellow]")
|
|
2390
|
+
return
|
|
2391
|
+
|
|
2392
|
+
# Output in JSON format if requested
|
|
2393
|
+
if output_json:
|
|
2394
|
+
_format_json_output(cdes)
|
|
2395
|
+
return
|
|
2396
|
+
|
|
2397
|
+
table = Table(title="Critical Data Elements")
|
|
2398
|
+
table.add_column("ID", style="cyan")
|
|
2399
|
+
table.add_column("Name", style="green")
|
|
2400
|
+
table.add_column("Data Type", style="blue")
|
|
2401
|
+
table.add_column("Status", style="yellow")
|
|
2402
|
+
table.add_column("Description", style="white")
|
|
2403
|
+
|
|
2404
|
+
for cde_item in cdes:
|
|
2405
|
+
desc = cde_item.get("description", "")
|
|
2406
|
+
if len(desc) > 30:
|
|
2407
|
+
desc = desc[:30] + "..."
|
|
2408
|
+
|
|
2409
|
+
table.add_row(
|
|
2410
|
+
cde_item.get("id", "N/A"),
|
|
2411
|
+
cde_item.get("name", "N/A"),
|
|
2412
|
+
cde_item.get("dataType", "N/A"),
|
|
2413
|
+
cde_item.get("status", "N/A"),
|
|
2414
|
+
desc,
|
|
2415
|
+
)
|
|
2416
|
+
|
|
2417
|
+
console.print(table)
|
|
2418
|
+
|
|
2419
|
+
except Exception as e:
|
|
2420
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
@cde.command()
|
|
2424
|
+
@click.option("--cde-id", required=True, help="ID of the critical data element")
|
|
2425
|
+
def show(cde_id):
|
|
2426
|
+
"""Show details of a critical data element."""
|
|
2427
|
+
try:
|
|
2428
|
+
client = UnifiedCatalogClient()
|
|
2429
|
+
args = {"--cde-id": [cde_id]}
|
|
2430
|
+
result = client.get_critical_data_element_by_id(args)
|
|
2431
|
+
|
|
2432
|
+
if not result:
|
|
2433
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2434
|
+
return
|
|
2435
|
+
if isinstance(result, dict) and "error" in result:
|
|
2436
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'CDE not found')}")
|
|
2437
|
+
return
|
|
2438
|
+
|
|
2439
|
+
console.print(json.dumps(result, indent=2))
|
|
2440
|
+
|
|
2441
|
+
except Exception as e:
|
|
2442
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2443
|
+
|
|
2444
|
+
|
|
2445
|
+
@cde.command(name="add-relationship")
|
|
2446
|
+
@click.option("--cde-id", required=True, help="Critical data element ID (GUID)")
|
|
2447
|
+
@click.option("--entity-type", required=True,
|
|
2448
|
+
type=click.Choice(["CRITICALDATACOLUMN", "TERM", "DATAASSET", "DATAPRODUCT"], case_sensitive=False),
|
|
2449
|
+
help="Type of entity to relate to")
|
|
2450
|
+
@click.option("--entity-id", required=True, help="Entity ID (GUID) to relate to")
|
|
2451
|
+
@click.option("--asset-id", help="Asset ID (GUID) - defaults to entity-id if not provided")
|
|
2452
|
+
@click.option("--relationship-type", default="Related", help="Relationship type (default: Related)")
|
|
2453
|
+
@click.option("--description", default="", help="Description of the relationship")
|
|
2454
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
2455
|
+
def add_cde_relationship(cde_id, entity_type, entity_id, asset_id, relationship_type, description, output):
|
|
2456
|
+
"""Create a relationship for a critical data element.
|
|
2457
|
+
|
|
2458
|
+
Links a CDE to another entity like a critical data column, term, or data product.
|
|
2459
|
+
|
|
2460
|
+
Examples:
|
|
2461
|
+
pvw uc cde add-relationship --cde-id <id> --entity-type CRITICALDATACOLUMN --entity-id <col-id>
|
|
2462
|
+
pvw uc cde add-relationship --cde-id <id> --entity-type TERM --entity-id <term-id> --description "Primary term"
|
|
2463
|
+
"""
|
|
2464
|
+
try:
|
|
2465
|
+
client = UnifiedCatalogClient()
|
|
2466
|
+
args = {
|
|
2467
|
+
"--cde-id": [cde_id],
|
|
2468
|
+
"--entity-type": [entity_type],
|
|
2469
|
+
"--entity-id": [entity_id],
|
|
2470
|
+
"--relationship-type": [relationship_type],
|
|
2471
|
+
"--description": [description]
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
if asset_id:
|
|
2475
|
+
args["--asset-id"] = [asset_id]
|
|
2476
|
+
|
|
2477
|
+
result = client.create_cde_relationship(args)
|
|
2478
|
+
|
|
2479
|
+
if output == "json":
|
|
2480
|
+
console.print_json(data=result)
|
|
2481
|
+
else:
|
|
2482
|
+
if result and isinstance(result, dict):
|
|
2483
|
+
console.print("[green]SUCCESS:[/green] Created CDE relationship")
|
|
2484
|
+
table = Table(title="CDE Relationship", show_header=True)
|
|
2485
|
+
table.add_column("Property", style="cyan")
|
|
2486
|
+
table.add_column("Value", style="white")
|
|
2487
|
+
|
|
2488
|
+
table.add_row("Entity ID", result.get("entityId", "N/A"))
|
|
2489
|
+
table.add_row("Relationship Type", result.get("relationshipType", "N/A"))
|
|
2490
|
+
table.add_row("Description", result.get("description", "N/A"))
|
|
2491
|
+
|
|
2492
|
+
if "systemData" in result:
|
|
2493
|
+
sys_data = result["systemData"]
|
|
2494
|
+
table.add_row("Created By", sys_data.get("createdBy", "N/A"))
|
|
2495
|
+
table.add_row("Created At", sys_data.get("createdAt", "N/A"))
|
|
2496
|
+
|
|
2497
|
+
console.print(table)
|
|
2498
|
+
else:
|
|
2499
|
+
console.print("[green]SUCCESS:[/green] Created CDE relationship")
|
|
2500
|
+
|
|
2501
|
+
except Exception as e:
|
|
2502
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2503
|
+
|
|
2504
|
+
|
|
2505
|
+
@cde.command(name="list-relationships")
|
|
2506
|
+
@click.option("--cde-id", required=True, help="Critical data element ID (GUID)")
|
|
2507
|
+
@click.option("--entity-type",
|
|
2508
|
+
type=click.Choice(["CRITICALDATACOLUMN", "TERM", "DATAASSET", "DATAPRODUCT"], case_sensitive=False),
|
|
2509
|
+
help="Filter by entity type (optional)")
|
|
2510
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
2511
|
+
def list_cde_relationships(cde_id, entity_type, output):
|
|
2512
|
+
"""List relationships for a critical data element.
|
|
2513
|
+
|
|
2514
|
+
Shows all entities linked to this CDE, optionally filtered by type.
|
|
2515
|
+
|
|
2516
|
+
Examples:
|
|
2517
|
+
pvw uc cde list-relationships --cde-id <id>
|
|
2518
|
+
pvw uc cde list-relationships --cde-id <id> --entity-type CRITICALDATACOLUMN
|
|
2519
|
+
"""
|
|
2520
|
+
try:
|
|
2521
|
+
client = UnifiedCatalogClient()
|
|
2522
|
+
args = {"--cde-id": [cde_id]}
|
|
2523
|
+
|
|
2524
|
+
if entity_type:
|
|
2525
|
+
args["--entity-type"] = [entity_type]
|
|
2526
|
+
|
|
2527
|
+
result = client.get_cde_relationships(args)
|
|
2528
|
+
|
|
2529
|
+
if output == "json":
|
|
2530
|
+
console.print_json(data=result)
|
|
2531
|
+
else:
|
|
2532
|
+
relationships = result.get("value", []) if result else []
|
|
2533
|
+
|
|
2534
|
+
if not relationships:
|
|
2535
|
+
console.print(f"[yellow]No relationships found for CDE '{cde_id}'[/yellow]")
|
|
2536
|
+
return
|
|
2537
|
+
|
|
2538
|
+
table = Table(title=f"CDE Relationships ({len(relationships)} found)", show_header=True)
|
|
2539
|
+
table.add_column("Entity ID", style="cyan")
|
|
2540
|
+
table.add_column("Relationship Type", style="white")
|
|
2541
|
+
table.add_column("Description", style="white")
|
|
2542
|
+
table.add_column("Created", style="dim")
|
|
2543
|
+
|
|
2544
|
+
for rel in relationships:
|
|
2545
|
+
table.add_row(
|
|
2546
|
+
rel.get("entityId", "N/A"),
|
|
2547
|
+
rel.get("relationshipType", "N/A"),
|
|
2548
|
+
rel.get("description", "")[:50] + ("..." if len(rel.get("description", "")) > 50 else ""),
|
|
2549
|
+
rel.get("systemData", {}).get("createdAt", "N/A")[:10]
|
|
2550
|
+
)
|
|
2551
|
+
|
|
2552
|
+
console.print(table)
|
|
2553
|
+
|
|
2554
|
+
except Exception as e:
|
|
2555
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2556
|
+
|
|
2557
|
+
|
|
2558
|
+
@cde.command(name="remove-relationship")
|
|
2559
|
+
@click.option("--cde-id", required=True, help="Critical data element ID (GUID)")
|
|
2560
|
+
@click.option("--entity-type", required=True,
|
|
2561
|
+
type=click.Choice(["CRITICALDATACOLUMN", "TERM", "DATAASSET", "DATAPRODUCT"], case_sensitive=False),
|
|
2562
|
+
help="Type of entity to unlink")
|
|
2563
|
+
@click.option("--entity-id", required=True, help="Entity ID (GUID) to unlink")
|
|
2564
|
+
@click.option("--confirm/--no-confirm", default=True, help="Ask for confirmation before deleting")
|
|
2565
|
+
def remove_cde_relationship(cde_id, entity_type, entity_id, confirm):
|
|
2566
|
+
"""Delete a relationship between a CDE and an entity.
|
|
2567
|
+
|
|
2568
|
+
Removes the link between a critical data element and a specific entity.
|
|
2569
|
+
|
|
2570
|
+
Examples:
|
|
2571
|
+
pvw uc cde remove-relationship --cde-id <id> --entity-type CRITICALDATACOLUMN --entity-id <col-id>
|
|
2572
|
+
pvw uc cde remove-relationship --cde-id <id> --entity-type TERM --entity-id <term-id> --no-confirm
|
|
2573
|
+
"""
|
|
2574
|
+
try:
|
|
2575
|
+
if confirm:
|
|
2576
|
+
confirm = click.confirm(
|
|
2577
|
+
f"Are you sure you want to delete CDE relationship to {entity_type} '{entity_id}'?",
|
|
2578
|
+
default=False
|
|
2579
|
+
)
|
|
2580
|
+
if not confirm:
|
|
2581
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
2582
|
+
return
|
|
2583
|
+
|
|
2584
|
+
client = UnifiedCatalogClient()
|
|
2585
|
+
args = {
|
|
2586
|
+
"--cde-id": [cde_id],
|
|
2587
|
+
"--entity-type": [entity_type],
|
|
2588
|
+
"--entity-id": [entity_id]
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
result = client.delete_cde_relationship(args)
|
|
2592
|
+
|
|
2593
|
+
# DELETE returns 204 No Content on success
|
|
2594
|
+
if result is None or (isinstance(result, dict) and not result.get("error")):
|
|
2595
|
+
console.print(f"[green]SUCCESS:[/green] Deleted CDE relationship to {entity_type} '{entity_id}'")
|
|
2596
|
+
elif isinstance(result, dict) and "error" in result:
|
|
2597
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
2598
|
+
else:
|
|
2599
|
+
console.print(f"[green]SUCCESS:[/green] Deleted CDE relationship")
|
|
2600
|
+
|
|
2601
|
+
except Exception as e:
|
|
2602
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2603
|
+
|
|
2604
|
+
|
|
2605
|
+
@cde.command(name="query")
|
|
2606
|
+
@click.option("--ids", multiple=True, help="Filter by specific CDE IDs (GUIDs)")
|
|
2607
|
+
@click.option("--domain-ids", multiple=True, help="Filter by domain IDs (GUIDs)")
|
|
2608
|
+
@click.option("--name-keyword", help="Filter by name keyword (partial match)")
|
|
2609
|
+
@click.option("--owners", multiple=True, help="Filter by owner IDs (GUIDs)")
|
|
2610
|
+
@click.option("--status", type=click.Choice(["DRAFT", "PUBLISHED", "EXPIRED"], case_sensitive=False),
|
|
2611
|
+
help="Filter by status")
|
|
2612
|
+
@click.option("--multi-status", multiple=True,
|
|
2613
|
+
type=click.Choice(["DRAFT", "PUBLISHED", "EXPIRED"], case_sensitive=False),
|
|
2614
|
+
help="Filter by multiple statuses")
|
|
2615
|
+
@click.option("--skip", type=int, default=0, help="Number of items to skip (pagination)")
|
|
2616
|
+
@click.option("--top", type=int, default=100, help="Number of items to return (max 1000)")
|
|
2617
|
+
@click.option("--order-by-field", help="Field to sort by (e.g., 'name', 'status')")
|
|
2618
|
+
@click.option("--order-by-direction", type=click.Choice(["asc", "desc"]), default="asc",
|
|
2619
|
+
help="Sort direction")
|
|
2620
|
+
@click.option("--output", default="table", type=click.Choice(["json", "table"]), help="Output format")
|
|
2621
|
+
def query_cdes(ids, domain_ids, name_keyword, owners, status, multi_status,
|
|
2622
|
+
skip, top, order_by_field, order_by_direction, output):
|
|
2623
|
+
"""Query critical data elements with advanced filters.
|
|
2624
|
+
|
|
2625
|
+
Perform complex searches across CDEs using multiple filter criteria.
|
|
2626
|
+
Supports pagination and custom sorting.
|
|
2627
|
+
|
|
2628
|
+
Examples:
|
|
2629
|
+
# Find all CDEs in a specific domain
|
|
2630
|
+
pvw uc cde query --domain-ids <domain-guid>
|
|
2631
|
+
|
|
2632
|
+
# Search by keyword
|
|
2633
|
+
pvw uc cde query --name-keyword "customer"
|
|
2634
|
+
|
|
2635
|
+
# Filter by owner and status
|
|
2636
|
+
pvw uc cde query --owners <user-guid> --status PUBLISHED
|
|
2637
|
+
|
|
2638
|
+
# Find all published or expired CDEs
|
|
2639
|
+
pvw uc cde query --multi-status PUBLISHED EXPIRED
|
|
2640
|
+
|
|
2641
|
+
# Pagination example
|
|
2642
|
+
pvw uc cde query --skip 0 --top 50 --order-by-field name --order-by-direction desc
|
|
2643
|
+
"""
|
|
2644
|
+
try:
|
|
2645
|
+
client = UnifiedCatalogClient()
|
|
2646
|
+
args = {}
|
|
2647
|
+
|
|
2648
|
+
# Build args dict from parameters
|
|
2649
|
+
if ids:
|
|
2650
|
+
args["--ids"] = list(ids)
|
|
2651
|
+
if domain_ids:
|
|
2652
|
+
args["--domain-ids"] = list(domain_ids)
|
|
2653
|
+
if name_keyword:
|
|
2654
|
+
args["--name-keyword"] = [name_keyword]
|
|
2655
|
+
if owners:
|
|
2656
|
+
args["--owners"] = list(owners)
|
|
2657
|
+
if status:
|
|
2658
|
+
args["--status"] = [status]
|
|
2659
|
+
if multi_status:
|
|
2660
|
+
args["--multi-status"] = list(multi_status)
|
|
2661
|
+
if skip:
|
|
2662
|
+
args["--skip"] = [str(skip)]
|
|
2663
|
+
if top:
|
|
2664
|
+
args["--top"] = [str(top)]
|
|
2665
|
+
if order_by_field:
|
|
2666
|
+
args["--order-by-field"] = [order_by_field]
|
|
2667
|
+
args["--order-by-direction"] = [order_by_direction]
|
|
2668
|
+
|
|
2669
|
+
result = client.query_critical_data_elements(args)
|
|
2670
|
+
|
|
2671
|
+
if output == "json":
|
|
2672
|
+
console.print_json(data=result)
|
|
2673
|
+
else:
|
|
2674
|
+
cdes = result.get("value", []) if result else []
|
|
2675
|
+
|
|
2676
|
+
if not cdes:
|
|
2677
|
+
console.print("[yellow]No critical data elements found matching the query.[/yellow]")
|
|
2678
|
+
return
|
|
2679
|
+
|
|
2680
|
+
# Check for pagination
|
|
2681
|
+
next_link = result.get("nextLink")
|
|
2682
|
+
if next_link:
|
|
2683
|
+
console.print(f"[dim]Note: More results available (nextLink provided)[/dim]\n")
|
|
2684
|
+
|
|
2685
|
+
table = Table(title=f"Query Results ({len(cdes)} found)", show_header=True)
|
|
2686
|
+
table.add_column("Name", style="cyan")
|
|
2687
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
2688
|
+
table.add_column("Domain", style="yellow", no_wrap=True)
|
|
2689
|
+
table.add_column("Status", style="white")
|
|
2690
|
+
table.add_column("Data Type", style="green")
|
|
2691
|
+
|
|
2692
|
+
for cde in cdes:
|
|
2693
|
+
table.add_row(
|
|
2694
|
+
cde.get("name", "N/A"),
|
|
2695
|
+
cde.get("id", "N/A")[:13] + "...",
|
|
2696
|
+
cde.get("domain", "N/A")[:13] + "...",
|
|
2697
|
+
cde.get("status", "N/A"),
|
|
2698
|
+
cde.get("dataType", "N/A")
|
|
2699
|
+
)
|
|
2700
|
+
|
|
2701
|
+
console.print(table)
|
|
2702
|
+
|
|
2703
|
+
# Show pagination info
|
|
2704
|
+
if skip > 0 or next_link:
|
|
2705
|
+
console.print(f"\n[dim]Showing items {skip + 1} to {skip + len(cdes)}[/dim]")
|
|
2706
|
+
|
|
2707
|
+
except Exception as e:
|
|
2708
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2709
|
+
|
|
2710
|
+
|
|
2711
|
+
# ========================================
|
|
2712
|
+
# KEY RESULTS (OKRs)
|
|
2713
|
+
# ========================================
|
|
2714
|
+
|
|
2715
|
+
|
|
2716
|
+
@uc.group()
|
|
2717
|
+
def keyresult():
|
|
2718
|
+
"""Manage key results for objectives (OKRs)."""
|
|
2719
|
+
pass
|
|
2720
|
+
|
|
2721
|
+
|
|
2722
|
+
@keyresult.command(name="list")
|
|
2723
|
+
@click.option("--objective-id", required=True, help="Objective ID to list key results for")
|
|
2724
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
2725
|
+
def list_key_results(objective_id, output_json):
|
|
2726
|
+
"""List all key results for an objective."""
|
|
2727
|
+
try:
|
|
2728
|
+
client = UnifiedCatalogClient()
|
|
2729
|
+
args = {"--objective-id": [objective_id]}
|
|
2730
|
+
result = client.get_key_results(args)
|
|
2731
|
+
|
|
2732
|
+
if not result:
|
|
2733
|
+
console.print("[yellow]No key results found.[/yellow]")
|
|
2734
|
+
return
|
|
2735
|
+
|
|
2736
|
+
# Handle response format
|
|
2737
|
+
if isinstance(result, (list, tuple)):
|
|
2738
|
+
key_results = result
|
|
2739
|
+
elif isinstance(result, dict):
|
|
2740
|
+
key_results = result.get("value", [])
|
|
2741
|
+
else:
|
|
2742
|
+
key_results = []
|
|
2743
|
+
|
|
2744
|
+
if not key_results:
|
|
2745
|
+
console.print("[yellow]No key results found.[/yellow]")
|
|
2746
|
+
return
|
|
2747
|
+
|
|
2748
|
+
# Output in JSON format if requested
|
|
2749
|
+
if output_json:
|
|
2750
|
+
_format_json_output(key_results)
|
|
2751
|
+
return
|
|
2752
|
+
|
|
2753
|
+
table = Table(title=f"Key Results for Objective {objective_id[:8]}...")
|
|
2754
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
2755
|
+
table.add_column("Definition", style="green", max_width=50)
|
|
2756
|
+
table.add_column("Progress", style="blue")
|
|
2757
|
+
table.add_column("Goal", style="yellow")
|
|
2758
|
+
table.add_column("Max", style="magenta")
|
|
2759
|
+
table.add_column("Status", style="white")
|
|
2760
|
+
|
|
2761
|
+
for kr in key_results:
|
|
2762
|
+
definition = kr.get("definition", "N/A")
|
|
2763
|
+
if len(definition) > 47:
|
|
2764
|
+
definition = definition[:47] + "..."
|
|
2765
|
+
|
|
2766
|
+
table.add_row(
|
|
2767
|
+
kr.get("id", "N/A")[:13] + "...",
|
|
2768
|
+
definition,
|
|
2769
|
+
str(kr.get("progress", "N/A")),
|
|
2770
|
+
str(kr.get("goal", "N/A")),
|
|
2771
|
+
str(kr.get("max", "N/A")),
|
|
2772
|
+
kr.get("status", "N/A"),
|
|
2773
|
+
)
|
|
2774
|
+
|
|
2775
|
+
console.print(table)
|
|
2776
|
+
console.print(f"\n[dim]Found {len(key_results)} key result(s)[/dim]")
|
|
2777
|
+
|
|
2778
|
+
except Exception as e:
|
|
2779
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2780
|
+
|
|
2781
|
+
|
|
2782
|
+
@keyresult.command()
|
|
2783
|
+
@click.option("--objective-id", required=True, help="Objective ID")
|
|
2784
|
+
@click.option("--key-result-id", required=True, help="Key result ID")
|
|
2785
|
+
def show(objective_id, key_result_id):
|
|
2786
|
+
"""Show details of a key result."""
|
|
2787
|
+
try:
|
|
2788
|
+
client = UnifiedCatalogClient()
|
|
2789
|
+
args = {
|
|
2790
|
+
"--objective-id": [objective_id],
|
|
2791
|
+
"--key-result-id": [key_result_id]
|
|
2792
|
+
}
|
|
2793
|
+
result = client.get_key_result_by_id(args)
|
|
2794
|
+
|
|
2795
|
+
if not result:
|
|
2796
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2797
|
+
return
|
|
2798
|
+
if isinstance(result, dict) and "error" in result:
|
|
2799
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Key result not found')}")
|
|
2800
|
+
return
|
|
2801
|
+
|
|
2802
|
+
console.print(json.dumps(result, indent=2))
|
|
2803
|
+
|
|
2804
|
+
except Exception as e:
|
|
2805
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
@keyresult.command()
|
|
2809
|
+
@click.option("--objective-id", required=True, help="Objective ID")
|
|
2810
|
+
@click.option("--governance-domain-id", required=True, help="Governance domain ID")
|
|
2811
|
+
@click.option("--definition", required=True, help="Definition/description of the key result")
|
|
2812
|
+
@click.option("--progress", required=False, type=int, default=0, help="Current progress value (default: 0)")
|
|
2813
|
+
@click.option("--goal", required=True, type=int, help="Target goal value")
|
|
2814
|
+
@click.option("--max", "max_value", required=False, type=int, default=100, help="Maximum possible value (default: 100)")
|
|
2815
|
+
@click.option(
|
|
2816
|
+
"--status",
|
|
2817
|
+
required=False,
|
|
2818
|
+
default="OnTrack",
|
|
2819
|
+
type=click.Choice(["OnTrack", "AtRisk", "OffTrack", "Completed"]),
|
|
2820
|
+
help="Status of the key result",
|
|
2821
|
+
)
|
|
2822
|
+
def create(objective_id, governance_domain_id, definition, progress, goal, max_value, status):
|
|
2823
|
+
"""Create a new key result for an objective."""
|
|
2824
|
+
try:
|
|
2825
|
+
client = UnifiedCatalogClient()
|
|
2826
|
+
|
|
2827
|
+
args = {
|
|
2828
|
+
"--objective-id": [objective_id],
|
|
2829
|
+
"--governance-domain-id": [governance_domain_id],
|
|
2830
|
+
"--definition": [definition],
|
|
2831
|
+
"--progress": [str(progress)],
|
|
2832
|
+
"--goal": [str(goal)],
|
|
2833
|
+
"--max": [str(max_value)],
|
|
2834
|
+
"--status": [status],
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
result = client.create_key_result(args)
|
|
2838
|
+
|
|
2839
|
+
if not result:
|
|
2840
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2841
|
+
return
|
|
2842
|
+
if isinstance(result, dict) and "error" in result:
|
|
2843
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
2844
|
+
return
|
|
2845
|
+
|
|
2846
|
+
console.print(f"[green]SUCCESS:[/green] Created key result")
|
|
2847
|
+
console.print(json.dumps(result, indent=2))
|
|
2848
|
+
|
|
2849
|
+
except Exception as e:
|
|
2850
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
@keyresult.command()
|
|
2854
|
+
@click.option("--objective-id", required=True, help="Objective ID")
|
|
2855
|
+
@click.option("--key-result-id", required=True, help="Key result ID to update")
|
|
2856
|
+
@click.option("--governance-domain-id", required=False, help="Governance domain ID")
|
|
2857
|
+
@click.option("--definition", required=False, help="New definition/description")
|
|
2858
|
+
@click.option("--progress", required=False, type=int, help="New progress value")
|
|
2859
|
+
@click.option("--goal", required=False, type=int, help="New goal value")
|
|
2860
|
+
@click.option("--max", "max_value", required=False, type=int, help="New maximum value")
|
|
2861
|
+
@click.option(
|
|
2862
|
+
"--status",
|
|
2863
|
+
required=False,
|
|
2864
|
+
type=click.Choice(["OnTrack", "AtRisk", "OffTrack", "Completed"]),
|
|
2865
|
+
help="Status of the key result",
|
|
2866
|
+
)
|
|
2867
|
+
def update(objective_id, key_result_id, governance_domain_id, definition, progress, goal, max_value, status):
|
|
2868
|
+
"""Update an existing key result."""
|
|
2869
|
+
try:
|
|
2870
|
+
client = UnifiedCatalogClient()
|
|
2871
|
+
|
|
2872
|
+
# Build args dictionary - only include provided values
|
|
2873
|
+
args = {
|
|
2874
|
+
"--objective-id": [objective_id],
|
|
2875
|
+
"--key-result-id": [key_result_id]
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
if governance_domain_id:
|
|
2879
|
+
args["--governance-domain-id"] = [governance_domain_id]
|
|
2880
|
+
if definition:
|
|
2881
|
+
args["--definition"] = [definition]
|
|
2882
|
+
if progress is not None:
|
|
2883
|
+
args["--progress"] = [str(progress)]
|
|
2884
|
+
if goal is not None:
|
|
2885
|
+
args["--goal"] = [str(goal)]
|
|
2886
|
+
if max_value is not None:
|
|
2887
|
+
args["--max"] = [str(max_value)]
|
|
2888
|
+
if status:
|
|
2889
|
+
args["--status"] = [status]
|
|
2890
|
+
|
|
2891
|
+
result = client.update_key_result(args)
|
|
2892
|
+
|
|
2893
|
+
if not result:
|
|
2894
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
2895
|
+
return
|
|
2896
|
+
if isinstance(result, dict) and "error" in result:
|
|
2897
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
2898
|
+
return
|
|
2899
|
+
|
|
2900
|
+
console.print(f"[green]SUCCESS:[/green] Updated key result '{key_result_id}'")
|
|
2901
|
+
console.print(json.dumps(result, indent=2))
|
|
2902
|
+
|
|
2903
|
+
except Exception as e:
|
|
2904
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
@keyresult.command()
|
|
2908
|
+
@click.option("--objective-id", required=True, help="Objective ID")
|
|
2909
|
+
@click.option("--key-result-id", required=True, help="Key result ID to delete")
|
|
2910
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
2911
|
+
def delete(objective_id, key_result_id, yes):
|
|
2912
|
+
"""Delete a key result."""
|
|
2913
|
+
try:
|
|
2914
|
+
if not yes:
|
|
2915
|
+
confirm = click.confirm(
|
|
2916
|
+
f"Are you sure you want to delete key result '{key_result_id}'?",
|
|
2917
|
+
default=False
|
|
2918
|
+
)
|
|
2919
|
+
if not confirm:
|
|
2920
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
2921
|
+
return
|
|
2922
|
+
|
|
2923
|
+
client = UnifiedCatalogClient()
|
|
2924
|
+
args = {
|
|
2925
|
+
"--objective-id": [objective_id],
|
|
2926
|
+
"--key-result-id": [key_result_id]
|
|
2927
|
+
}
|
|
2928
|
+
result = client.delete_key_result(args)
|
|
2929
|
+
|
|
2930
|
+
# DELETE operations may return empty response on success
|
|
2931
|
+
if result is None or (isinstance(result, dict) and not result.get("error")):
|
|
2932
|
+
console.print(f"[green]SUCCESS:[/green] Deleted key result '{key_result_id}'")
|
|
2933
|
+
elif isinstance(result, dict) and "error" in result:
|
|
2934
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
2935
|
+
else:
|
|
2936
|
+
console.print(f"[green]SUCCESS:[/green] Deleted key result")
|
|
2937
|
+
if result:
|
|
2938
|
+
console.print(json.dumps(result, indent=2))
|
|
2939
|
+
|
|
2940
|
+
except Exception as e:
|
|
2941
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
2942
|
+
|
|
2943
|
+
|
|
2944
|
+
# ========================================
|
|
2945
|
+
# HEALTH MANAGEMENT - IMPLEMENTED!
|
|
2946
|
+
# ========================================
|
|
2947
|
+
|
|
2948
|
+
# Import and register health commands from dedicated module
|
|
2949
|
+
from purviewcli.cli.health import health as health_commands
|
|
2950
|
+
uc.add_command(health_commands, name="health")
|
|
2951
|
+
|
|
2952
|
+
|
|
2953
|
+
# ========================================
|
|
2954
|
+
# DATA POLICIES (NEW)
|
|
2955
|
+
# ========================================
|
|
2956
|
+
|
|
2957
|
+
|
|
2958
|
+
@uc.group()
|
|
2959
|
+
def policy():
|
|
2960
|
+
"""Manage data governance policies."""
|
|
2961
|
+
pass
|
|
2962
|
+
|
|
2963
|
+
|
|
2964
|
+
@policy.command(name="list")
|
|
2965
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
2966
|
+
def list_policies(output):
|
|
2967
|
+
"""List all data governance policies."""
|
|
2968
|
+
client = UnifiedCatalogClient()
|
|
2969
|
+
response = client.list_policies({})
|
|
2970
|
+
|
|
2971
|
+
if output == "json":
|
|
2972
|
+
console.print_json(json.dumps(response))
|
|
2973
|
+
else:
|
|
2974
|
+
# API returns 'values' (plural), not 'value'
|
|
2975
|
+
policies = response.get("values", response.get("value", []))
|
|
2976
|
+
|
|
2977
|
+
if policies:
|
|
2978
|
+
table = Table(title="[bold cyan]Data Governance Policies[/bold cyan]", show_header=True)
|
|
2979
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
2980
|
+
table.add_column("Name", style="green")
|
|
2981
|
+
table.add_column("Entity Type", style="yellow")
|
|
2982
|
+
table.add_column("Entity ID", style="magenta", no_wrap=True)
|
|
2983
|
+
table.add_column("Rules", style="white")
|
|
2984
|
+
|
|
2985
|
+
for item in policies:
|
|
2986
|
+
props = item.get("properties", {})
|
|
2987
|
+
entity = props.get("entity", {})
|
|
2988
|
+
entity_type = entity.get("type", "N/A")
|
|
2989
|
+
entity_ref = entity.get("referenceName", "N/A")
|
|
2990
|
+
|
|
2991
|
+
# Count rules
|
|
2992
|
+
decision_rules = len(props.get("decisionRules", []))
|
|
2993
|
+
attribute_rules = len(props.get("attributeRules", []))
|
|
2994
|
+
rules_summary = f"{decision_rules} decision, {attribute_rules} attribute"
|
|
2995
|
+
|
|
2996
|
+
table.add_row(
|
|
2997
|
+
item.get("id", "N/A")[:36], # Show only GUID
|
|
2998
|
+
item.get("name", "N/A"),
|
|
2999
|
+
entity_type.replace("Reference", ""), # Clean up type name
|
|
3000
|
+
entity_ref[:36], # Show only GUID
|
|
3001
|
+
rules_summary
|
|
3002
|
+
)
|
|
3003
|
+
|
|
3004
|
+
console.print(table)
|
|
3005
|
+
console.print(f"\n[dim]Total: {len(policies)} policy/policies[/dim]")
|
|
3006
|
+
else:
|
|
3007
|
+
console.print("[yellow]No policies found[/yellow]")
|
|
3008
|
+
|
|
3009
|
+
|
|
3010
|
+
@policy.command(name="get")
|
|
3011
|
+
@click.option("--policy-id", required=True, help="Policy ID")
|
|
3012
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="json", help="Output format")
|
|
3013
|
+
def get_policy(policy_id, output):
|
|
3014
|
+
"""Get a specific data governance policy by ID."""
|
|
3015
|
+
client = UnifiedCatalogClient()
|
|
3016
|
+
|
|
3017
|
+
# Get all policies and filter (since GET by ID returns 404)
|
|
3018
|
+
all_policies = client.list_policies({})
|
|
3019
|
+
policies = all_policies.get("values", all_policies.get("value", []))
|
|
3020
|
+
|
|
3021
|
+
# Find the requested policy
|
|
3022
|
+
policy = next((p for p in policies if p.get("id") == policy_id), None)
|
|
3023
|
+
|
|
3024
|
+
if not policy:
|
|
3025
|
+
console.print(f"[red]ERROR:[/red] Policy with ID {policy_id} not found")
|
|
3026
|
+
return
|
|
3027
|
+
|
|
3028
|
+
if output == "json":
|
|
3029
|
+
_format_json_output(policy)
|
|
3030
|
+
else:
|
|
3031
|
+
# Display policy in formatted view
|
|
3032
|
+
props = policy.get("properties", {})
|
|
3033
|
+
entity = props.get("entity", {})
|
|
3034
|
+
|
|
3035
|
+
console.print(f"\n[bold cyan]Policy Details[/bold cyan]")
|
|
3036
|
+
console.print(f"[bold]ID:[/bold] {policy.get('id')}")
|
|
3037
|
+
console.print(f"[bold]Name:[/bold] {policy.get('name')}")
|
|
3038
|
+
console.print(f"[bold]Version:[/bold] {policy.get('version', 0)}")
|
|
3039
|
+
|
|
3040
|
+
console.print(f"\n[bold cyan]Entity[/bold cyan]")
|
|
3041
|
+
console.print(f"[bold]Type:[/bold] {entity.get('type', 'N/A')}")
|
|
3042
|
+
console.print(f"[bold]Reference:[/bold] {entity.get('referenceName', 'N/A')}")
|
|
3043
|
+
console.print(f"[bold]Parent:[/bold] {props.get('parentEntityName', 'N/A')}")
|
|
3044
|
+
|
|
3045
|
+
# Decision Rules
|
|
3046
|
+
decision_rules = props.get("decisionRules", [])
|
|
3047
|
+
if decision_rules:
|
|
3048
|
+
console.print(f"\n[bold cyan]Decision Rules ({len(decision_rules)})[/bold cyan]")
|
|
3049
|
+
for i, rule in enumerate(decision_rules, 1):
|
|
3050
|
+
console.print(f"\n [bold]Rule {i}:[/bold] {rule.get('kind', 'N/A')}")
|
|
3051
|
+
console.print(f" [bold]Effect:[/bold] {rule.get('effect', 'N/A')}")
|
|
3052
|
+
if "dnfCondition" in rule:
|
|
3053
|
+
console.print(f" [bold]Conditions:[/bold] {len(rule['dnfCondition'])} clause(s)")
|
|
3054
|
+
|
|
3055
|
+
# Attribute Rules
|
|
3056
|
+
attribute_rules = props.get("attributeRules", [])
|
|
3057
|
+
if attribute_rules:
|
|
3058
|
+
console.print(f"\n[bold cyan]Attribute Rules ({len(attribute_rules)})[/bold cyan]")
|
|
3059
|
+
for i, rule in enumerate(attribute_rules, 1):
|
|
3060
|
+
console.print(f"\n [bold]Rule {i}:[/bold] {rule.get('name', rule.get('id', 'N/A'))}")
|
|
3061
|
+
if "dnfCondition" in rule:
|
|
3062
|
+
conditions = rule.get("dnfCondition", [])
|
|
3063
|
+
console.print(f" [bold]Conditions:[/bold] {len(conditions)} clause(s)")
|
|
3064
|
+
for j, clause in enumerate(conditions[:3], 1): # Show first 3
|
|
3065
|
+
if clause:
|
|
3066
|
+
attr = clause[0] if isinstance(clause, list) else clause
|
|
3067
|
+
console.print(f" {j}. {attr.get('attributeName', 'N/A')}")
|
|
3068
|
+
if len(conditions) > 3:
|
|
3069
|
+
console.print(f" ... and {len(conditions) - 3} more")
|
|
3070
|
+
|
|
3071
|
+
console.print()
|
|
3072
|
+
|
|
3073
|
+
|
|
3074
|
+
|
|
3075
|
+
@policy.command(name="create")
|
|
3076
|
+
@click.option("--name", required=True, help="Policy name")
|
|
3077
|
+
@click.option("--policy-type", required=True, help="Policy type (e.g., access, retention)")
|
|
3078
|
+
@click.option("--description", default="", help="Policy description")
|
|
3079
|
+
@click.option("--status", default="active", help="Policy status (active, draft)")
|
|
3080
|
+
def create_policy(name, policy_type, description, status):
|
|
3081
|
+
"""Create a new data governance policy."""
|
|
3082
|
+
client = UnifiedCatalogClient()
|
|
3083
|
+
args = {
|
|
3084
|
+
"--name": [name],
|
|
3085
|
+
"--policy-type": [policy_type],
|
|
3086
|
+
"--description": [description],
|
|
3087
|
+
"--status": [status]
|
|
3088
|
+
}
|
|
3089
|
+
response = client.create_policy(args)
|
|
3090
|
+
|
|
3091
|
+
console.print(f"[green]SUCCESS:[/green] Policy created")
|
|
3092
|
+
_format_json_output(response)
|
|
3093
|
+
|
|
3094
|
+
|
|
3095
|
+
@policy.command(name="update")
|
|
3096
|
+
@click.option("--policy-id", required=True, help="Policy ID")
|
|
3097
|
+
@click.option("--name", help="New policy name")
|
|
3098
|
+
@click.option("--description", help="New policy description")
|
|
3099
|
+
@click.option("--status", help="New policy status")
|
|
3100
|
+
def update_policy(policy_id, name, description, status):
|
|
3101
|
+
"""Update an existing data governance policy."""
|
|
3102
|
+
client = UnifiedCatalogClient()
|
|
3103
|
+
args = {"--policy-id": [policy_id]}
|
|
3104
|
+
|
|
3105
|
+
if name:
|
|
3106
|
+
args["--name"] = [name]
|
|
3107
|
+
if description:
|
|
3108
|
+
args["--description"] = [description]
|
|
3109
|
+
if status:
|
|
3110
|
+
args["--status"] = [status]
|
|
3111
|
+
|
|
3112
|
+
response = client.update_policy(args)
|
|
3113
|
+
|
|
3114
|
+
console.print(f"[green]SUCCESS:[/green] Policy updated")
|
|
3115
|
+
_format_json_output(response)
|
|
3116
|
+
|
|
3117
|
+
|
|
3118
|
+
@policy.command(name="delete")
|
|
3119
|
+
@click.option("--policy-id", required=True, help="Policy ID")
|
|
3120
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this policy?")
|
|
3121
|
+
def delete_policy(policy_id):
|
|
3122
|
+
"""Delete a data governance policy."""
|
|
3123
|
+
client = UnifiedCatalogClient()
|
|
3124
|
+
args = {"--policy-id": [policy_id]}
|
|
3125
|
+
response = client.delete_policy(args)
|
|
3126
|
+
|
|
3127
|
+
console.print(f"[green]SUCCESS:[/green] Policy '{policy_id}' deleted")
|
|
3128
|
+
|
|
3129
|
+
|
|
3130
|
+
# ========================================
|
|
3131
|
+
# CUSTOM METADATA (NEW)
|
|
3132
|
+
# ========================================
|
|
3133
|
+
|
|
3134
|
+
|
|
3135
|
+
@uc.group()
|
|
3136
|
+
def metadata():
|
|
3137
|
+
"""Manage custom metadata for assets."""
|
|
3138
|
+
pass
|
|
3139
|
+
|
|
3140
|
+
|
|
3141
|
+
@metadata.command(name="list")
|
|
3142
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
3143
|
+
@click.option("--fallback/--no-fallback", default=True, help="Fallback to Business Metadata if UC is empty")
|
|
3144
|
+
def list_custom_metadata(output, fallback):
|
|
3145
|
+
"""List all custom metadata definitions.
|
|
3146
|
+
|
|
3147
|
+
Uses Atlas API to get Business Metadata definitions.
|
|
3148
|
+
With fallback enabled, shows user-friendly table format.
|
|
3149
|
+
"""
|
|
3150
|
+
client = UnifiedCatalogClient()
|
|
3151
|
+
response = client.list_custom_metadata({})
|
|
3152
|
+
|
|
3153
|
+
# Check if UC API returned business metadata (Atlas returns businessMetadataDefs)
|
|
3154
|
+
has_uc_data = (response and "businessMetadataDefs" in response
|
|
3155
|
+
and response["businessMetadataDefs"])
|
|
3156
|
+
|
|
3157
|
+
if output == "json":
|
|
3158
|
+
if has_uc_data:
|
|
3159
|
+
console.print_json(json.dumps(response))
|
|
3160
|
+
elif fallback:
|
|
3161
|
+
# Fallback message (though Atlas API should always return something)
|
|
3162
|
+
console.print("[dim]No business metadata found.[/dim]\n")
|
|
3163
|
+
console.print_json(json.dumps({"businessMetadataDefs": []}))
|
|
3164
|
+
else:
|
|
3165
|
+
console.print_json(json.dumps(response))
|
|
3166
|
+
else:
|
|
3167
|
+
# Table output
|
|
3168
|
+
if has_uc_data:
|
|
3169
|
+
biz_metadata = response.get('businessMetadataDefs', [])
|
|
3170
|
+
|
|
3171
|
+
if biz_metadata:
|
|
3172
|
+
table = Table(title="[bold green]Business Metadata Attributes[/bold green]", show_header=True)
|
|
3173
|
+
table.add_column("Attribute Name", style="green", no_wrap=True)
|
|
3174
|
+
table.add_column("Group", style="cyan")
|
|
3175
|
+
table.add_column("Type", style="yellow")
|
|
3176
|
+
table.add_column("Scope", style="magenta", max_width=25)
|
|
3177
|
+
table.add_column("Description", style="white", max_width=30)
|
|
3178
|
+
|
|
3179
|
+
total_attrs = 0
|
|
3180
|
+
for group in biz_metadata:
|
|
3181
|
+
group_name = group.get('name', 'N/A')
|
|
3182
|
+
attributes = group.get('attributeDefs', [])
|
|
3183
|
+
|
|
3184
|
+
# Parse group-level scope
|
|
3185
|
+
group_scope = "N/A"
|
|
3186
|
+
options = group.get('options', {})
|
|
3187
|
+
if 'dataGovernanceOptions' in options:
|
|
3188
|
+
try:
|
|
3189
|
+
dg_opts_str = options.get('dataGovernanceOptions', '{}')
|
|
3190
|
+
dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
|
|
3191
|
+
applicable = dg_opts.get('applicableConstructs', [])
|
|
3192
|
+
if applicable:
|
|
3193
|
+
# Categorize scope
|
|
3194
|
+
has_business_concept = any('businessConcept' in c or 'domain' in c for c in applicable)
|
|
3195
|
+
has_dataset = any('dataset' in c.lower() for c in applicable)
|
|
3196
|
+
|
|
3197
|
+
if has_business_concept and has_dataset:
|
|
3198
|
+
group_scope = "Universal (Concept + Dataset)"
|
|
3199
|
+
elif has_business_concept:
|
|
3200
|
+
group_scope = "Business Concept"
|
|
3201
|
+
elif has_dataset:
|
|
3202
|
+
group_scope = "Data Asset"
|
|
3203
|
+
else:
|
|
3204
|
+
# Show first 2 constructs
|
|
3205
|
+
scope_parts = []
|
|
3206
|
+
for construct in applicable[:2]:
|
|
3207
|
+
if ':' in construct:
|
|
3208
|
+
scope_parts.append(construct.split(':')[0])
|
|
3209
|
+
else:
|
|
3210
|
+
scope_parts.append(construct)
|
|
3211
|
+
group_scope = ', '.join(scope_parts)
|
|
3212
|
+
except:
|
|
3213
|
+
pass
|
|
3214
|
+
|
|
3215
|
+
for attr in attributes:
|
|
3216
|
+
total_attrs += 1
|
|
3217
|
+
attr_name = attr.get('name', 'N/A')
|
|
3218
|
+
attr_type = attr.get('typeName', 'N/A')
|
|
3219
|
+
|
|
3220
|
+
# Simplify enum types
|
|
3221
|
+
if 'ATTRIBUTE_ENUM_' in attr_type:
|
|
3222
|
+
attr_type = 'Enum'
|
|
3223
|
+
|
|
3224
|
+
attr_desc = attr.get('description', '')
|
|
3225
|
+
|
|
3226
|
+
# Check if attribute has custom scope
|
|
3227
|
+
attr_scope = group_scope
|
|
3228
|
+
attr_opts = attr.get('options', {})
|
|
3229
|
+
|
|
3230
|
+
# Check dataGovernanceOptions first
|
|
3231
|
+
if 'dataGovernanceOptions' in attr_opts:
|
|
3232
|
+
try:
|
|
3233
|
+
attr_dg_str = attr_opts.get('dataGovernanceOptions', '{}')
|
|
3234
|
+
attr_dg = json.loads(attr_dg_str) if isinstance(attr_dg_str, str) else attr_dg_str
|
|
3235
|
+
inherit = attr_dg.get('inheritApplicableConstructsFromGroup', True)
|
|
3236
|
+
if not inherit:
|
|
3237
|
+
attr_applicable = attr_dg.get('applicableConstructs', [])
|
|
3238
|
+
if attr_applicable:
|
|
3239
|
+
# Categorize custom scope
|
|
3240
|
+
has_business_concept = any('businessConcept' in c or 'domain' in c for c in attr_applicable)
|
|
3241
|
+
has_dataset = any('dataset' in c.lower() for c in attr_applicable)
|
|
3242
|
+
|
|
3243
|
+
if has_business_concept and has_dataset:
|
|
3244
|
+
attr_scope = "Universal"
|
|
3245
|
+
elif has_business_concept:
|
|
3246
|
+
attr_scope = "Business Concept"
|
|
3247
|
+
elif has_dataset:
|
|
3248
|
+
attr_scope = "Data Asset"
|
|
3249
|
+
else:
|
|
3250
|
+
attr_scope = f"Custom ({len(attr_applicable)})"
|
|
3251
|
+
except:
|
|
3252
|
+
pass
|
|
3253
|
+
|
|
3254
|
+
# Fallback: Check applicableEntityTypes (older format)
|
|
3255
|
+
if attr_scope == "N/A" and 'applicableEntityTypes' in attr_opts:
|
|
3256
|
+
try:
|
|
3257
|
+
entity_types_str = attr_opts.get('applicableEntityTypes', '[]')
|
|
3258
|
+
# Parse if string, otherwise use as-is
|
|
3259
|
+
if isinstance(entity_types_str, str):
|
|
3260
|
+
entity_types = json.loads(entity_types_str)
|
|
3261
|
+
else:
|
|
3262
|
+
entity_types = entity_types_str
|
|
3263
|
+
|
|
3264
|
+
if entity_types and isinstance(entity_types, list):
|
|
3265
|
+
# Check if entity types are data assets (tables, etc.)
|
|
3266
|
+
if any('table' in et.lower() or 'database' in et.lower() or 'file' in et.lower()
|
|
3267
|
+
for et in entity_types):
|
|
3268
|
+
attr_scope = "Data Asset"
|
|
3269
|
+
else:
|
|
3270
|
+
attr_scope = f"Assets ({len(entity_types)} types)"
|
|
3271
|
+
except Exception as e:
|
|
3272
|
+
# Silently fail but could log for debugging
|
|
3273
|
+
pass
|
|
3274
|
+
|
|
3275
|
+
table.add_row(
|
|
3276
|
+
attr_name,
|
|
3277
|
+
group_name,
|
|
3278
|
+
attr_type,
|
|
3279
|
+
attr_scope,
|
|
3280
|
+
attr_desc[:30] + "..." if len(attr_desc) > 30 else attr_desc
|
|
3281
|
+
)
|
|
3282
|
+
|
|
3283
|
+
console.print(table)
|
|
3284
|
+
console.print(f"\n[cyan]Total:[/cyan] {total_attrs} business metadata attribute(s) in {len(biz_metadata)} group(s)")
|
|
3285
|
+
console.print("\n[dim]Legend:[/dim]")
|
|
3286
|
+
console.print(" [magenta]Business Concept[/magenta] = Applies to Terms, Domains, Business Rules")
|
|
3287
|
+
console.print(" [magenta]Data Asset[/magenta] = Applies to Tables, Files, Databases")
|
|
3288
|
+
console.print(" [magenta]Universal[/magenta] = Applies to both Concepts and Assets")
|
|
3289
|
+
else:
|
|
3290
|
+
console.print("[yellow]No business metadata found[/yellow]")
|
|
3291
|
+
else:
|
|
3292
|
+
console.print("[yellow]No business metadata found[/yellow]")
|
|
3293
|
+
|
|
3294
|
+
|
|
3295
|
+
@metadata.command(name="get")
|
|
3296
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3297
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="json", help="Output format")
|
|
3298
|
+
def get_custom_metadata(asset_id, output):
|
|
3299
|
+
"""Get custom metadata (business metadata) for a specific asset."""
|
|
3300
|
+
client = UnifiedCatalogClient()
|
|
3301
|
+
args = {"--asset-id": [asset_id]}
|
|
3302
|
+
response = client.get_custom_metadata(args)
|
|
3303
|
+
|
|
3304
|
+
if output == "json":
|
|
3305
|
+
# Extract businessAttributes from entity response
|
|
3306
|
+
# Note: API returns "businessAttributes" not "businessMetadata"
|
|
3307
|
+
if response and "entity" in response:
|
|
3308
|
+
business_metadata = response["entity"].get("businessAttributes", {})
|
|
3309
|
+
_format_json_output(business_metadata)
|
|
3310
|
+
elif response and isinstance(response, dict):
|
|
3311
|
+
business_metadata = response.get("businessAttributes", {})
|
|
3312
|
+
_format_json_output(business_metadata)
|
|
3313
|
+
else:
|
|
3314
|
+
_format_json_output({})
|
|
3315
|
+
else:
|
|
3316
|
+
table = Table(title=f"[bold cyan]Business Metadata for Asset: {asset_id}[/bold cyan]")
|
|
3317
|
+
table.add_column("Group", style="cyan")
|
|
3318
|
+
table.add_column("Attribute", style="green")
|
|
3319
|
+
table.add_column("Value", style="white")
|
|
3320
|
+
|
|
3321
|
+
if response and "entity" in response:
|
|
3322
|
+
business_metadata = response["entity"].get("businessAttributes", {})
|
|
3323
|
+
if business_metadata:
|
|
3324
|
+
for group_name, attributes in business_metadata.items():
|
|
3325
|
+
if isinstance(attributes, dict):
|
|
3326
|
+
for attr_name, attr_value in attributes.items():
|
|
3327
|
+
table.add_row(group_name, attr_name, str(attr_value))
|
|
3328
|
+
elif response and isinstance(response, dict):
|
|
3329
|
+
business_metadata = response.get("businessAttributes", {})
|
|
3330
|
+
if business_metadata:
|
|
3331
|
+
for group_name, attributes in business_metadata.items():
|
|
3332
|
+
if isinstance(attributes, dict):
|
|
3333
|
+
for attr_name, attr_value in attributes.items():
|
|
3334
|
+
table.add_row(group_name, attr_name, str(attr_value))
|
|
3335
|
+
|
|
3336
|
+
console.print(table)
|
|
3337
|
+
|
|
3338
|
+
|
|
3339
|
+
@metadata.command(name="add")
|
|
3340
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3341
|
+
@click.option("--group", required=True, help="Business metadata group name (e.g., 'Governance', 'Privacy')")
|
|
3342
|
+
@click.option("--key", required=True, help="Attribute name")
|
|
3343
|
+
@click.option("--value", required=True, help="Attribute value")
|
|
3344
|
+
def add_custom_metadata(asset_id, group, key, value):
|
|
3345
|
+
"""Add custom metadata (business metadata) to an asset.
|
|
3346
|
+
|
|
3347
|
+
Example: pvw uc metadata add --asset-id <guid> --group Governance --key DataOwner --value "John Doe"
|
|
3348
|
+
"""
|
|
3349
|
+
client = UnifiedCatalogClient()
|
|
3350
|
+
args = {
|
|
3351
|
+
"--asset-id": [asset_id],
|
|
3352
|
+
"--group": [group],
|
|
3353
|
+
"--key": [key],
|
|
3354
|
+
"--value": [value]
|
|
3355
|
+
}
|
|
3356
|
+
response = client.add_custom_metadata(args)
|
|
3357
|
+
|
|
3358
|
+
console.print(f"[green]SUCCESS:[/green] Business metadata '{key}' added to group '{group}' on asset '{asset_id}'")
|
|
3359
|
+
if response:
|
|
3360
|
+
_format_json_output(response)
|
|
3361
|
+
|
|
3362
|
+
|
|
3363
|
+
@metadata.command(name="update")
|
|
3364
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3365
|
+
@click.option("--group", required=True, help="Business metadata group name")
|
|
3366
|
+
@click.option("--key", required=True, help="Attribute name to update")
|
|
3367
|
+
@click.option("--value", required=True, help="New attribute value")
|
|
3368
|
+
def update_custom_metadata(asset_id, group, key, value):
|
|
3369
|
+
"""Update custom metadata (business metadata) for an asset.
|
|
3370
|
+
|
|
3371
|
+
Example: pvw uc metadata update --asset-id <guid> --group Governance --key DataOwner --value "Jane Smith"
|
|
3372
|
+
"""
|
|
3373
|
+
client = UnifiedCatalogClient()
|
|
3374
|
+
args = {
|
|
3375
|
+
"--asset-id": [asset_id],
|
|
3376
|
+
"--group": [group],
|
|
3377
|
+
"--key": [key],
|
|
3378
|
+
"--value": [value]
|
|
3379
|
+
}
|
|
3380
|
+
response = client.update_custom_metadata(args)
|
|
3381
|
+
|
|
3382
|
+
console.print(f"[green]SUCCESS:[/green] Business metadata '{key}' updated in group '{group}' on asset '{asset_id}'")
|
|
3383
|
+
if response:
|
|
3384
|
+
_format_json_output(response)
|
|
3385
|
+
|
|
3386
|
+
|
|
3387
|
+
@metadata.command(name="delete")
|
|
3388
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3389
|
+
@click.option("--group", required=True, help="Business metadata group name to delete")
|
|
3390
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this business metadata group?")
|
|
3391
|
+
def delete_custom_metadata(asset_id, group):
|
|
3392
|
+
"""Delete custom metadata (business metadata) from an asset.
|
|
3393
|
+
|
|
3394
|
+
This removes the entire business metadata group from the asset.
|
|
3395
|
+
Example: pvw uc metadata delete --asset-id <guid> --group Governance
|
|
3396
|
+
"""
|
|
3397
|
+
client = UnifiedCatalogClient()
|
|
3398
|
+
args = {
|
|
3399
|
+
"--asset-id": [asset_id],
|
|
3400
|
+
"--group": [group]
|
|
3401
|
+
}
|
|
3402
|
+
response = client.delete_custom_metadata(args)
|
|
3403
|
+
|
|
3404
|
+
console.print(f"[green]SUCCESS:[/green] Business metadata group '{group}' deleted from asset '{asset_id}'")
|
|
3405
|
+
|
|
3406
|
+
|
|
3407
|
+
# ========================================
|
|
3408
|
+
# CUSTOM ATTRIBUTES (NEW)
|
|
3409
|
+
# ========================================
|
|
3410
|
+
|
|
3411
|
+
|
|
3412
|
+
@uc.group()
|
|
3413
|
+
def attribute():
|
|
3414
|
+
"""Manage custom attribute definitions."""
|
|
3415
|
+
pass
|
|
3416
|
+
|
|
3417
|
+
|
|
3418
|
+
@attribute.command(name="list")
|
|
3419
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
3420
|
+
def list_custom_attributes(output):
|
|
3421
|
+
"""List all custom attribute definitions."""
|
|
3422
|
+
client = UnifiedCatalogClient()
|
|
3423
|
+
response = client.list_custom_attributes({})
|
|
3424
|
+
|
|
3425
|
+
if output == "json":
|
|
3426
|
+
console.print_json(json.dumps(response))
|
|
3427
|
+
else:
|
|
3428
|
+
if "value" in response and response["value"]:
|
|
3429
|
+
table = Table(title="[bold cyan]Custom Attribute Definitions[/bold cyan]", show_header=True)
|
|
3430
|
+
table.add_column("ID", style="cyan")
|
|
3431
|
+
table.add_column("Name", style="green")
|
|
3432
|
+
table.add_column("Data Type", style="yellow")
|
|
3433
|
+
table.add_column("Required", style="magenta")
|
|
3434
|
+
table.add_column("Description", style="white")
|
|
3435
|
+
|
|
3436
|
+
for item in response["value"]:
|
|
3437
|
+
table.add_row(
|
|
3438
|
+
item.get("id", "N/A"),
|
|
3439
|
+
item.get("name", "N/A"),
|
|
3440
|
+
item.get("dataType", "N/A"),
|
|
3441
|
+
"Yes" if item.get("required") else "No",
|
|
3442
|
+
item.get("description", "")[:50] + "..." if len(item.get("description", "")) > 50 else item.get("description", "")
|
|
3443
|
+
)
|
|
3444
|
+
console.print(table)
|
|
3445
|
+
else:
|
|
3446
|
+
console.print("[yellow]No custom attributes found[/yellow]")
|
|
3447
|
+
|
|
3448
|
+
|
|
3449
|
+
@attribute.command(name="get")
|
|
3450
|
+
@click.option("--attribute-id", required=True, help="Attribute ID")
|
|
3451
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="json", help="Output format")
|
|
3452
|
+
def get_custom_attribute(attribute_id, output):
|
|
3453
|
+
"""Get a specific custom attribute definition."""
|
|
3454
|
+
client = UnifiedCatalogClient()
|
|
3455
|
+
args = {"--attribute-id": [attribute_id]}
|
|
3456
|
+
response = client.get_custom_attribute(args)
|
|
3457
|
+
|
|
3458
|
+
if output == "json":
|
|
3459
|
+
_format_json_output(response)
|
|
3460
|
+
else:
|
|
3461
|
+
table = Table(title=f"[bold cyan]Attribute: {response.get('name', 'N/A')}[/bold cyan]")
|
|
3462
|
+
table.add_column("Property", style="cyan")
|
|
3463
|
+
table.add_column("Value", style="white")
|
|
3464
|
+
|
|
3465
|
+
for key, value in response.items():
|
|
3466
|
+
table.add_row(key, str(value))
|
|
3467
|
+
console.print(table)
|
|
3468
|
+
|
|
3469
|
+
|
|
3470
|
+
@attribute.command(name="create")
|
|
3471
|
+
@click.option("--name", required=True, help="Attribute name")
|
|
3472
|
+
@click.option("--data-type", required=True, help="Data type (string, number, boolean, date)")
|
|
3473
|
+
@click.option("--description", default="", help="Attribute description")
|
|
3474
|
+
@click.option("--required", is_flag=True, help="Is this attribute required?")
|
|
3475
|
+
def create_custom_attribute(name, data_type, description, required):
|
|
3476
|
+
"""Create a new custom attribute definition."""
|
|
3477
|
+
client = UnifiedCatalogClient()
|
|
3478
|
+
args = {
|
|
3479
|
+
"--name": [name],
|
|
3480
|
+
"--data-type": [data_type],
|
|
3481
|
+
"--description": [description],
|
|
3482
|
+
"--required": ["true" if required else "false"]
|
|
3483
|
+
}
|
|
3484
|
+
response = client.create_custom_attribute(args)
|
|
3485
|
+
|
|
3486
|
+
console.print(f"[green]SUCCESS:[/green] Custom attribute created")
|
|
3487
|
+
_format_json_output(response)
|
|
3488
|
+
|
|
3489
|
+
|
|
3490
|
+
@attribute.command(name="update")
|
|
3491
|
+
@click.option("--attribute-id", required=True, help="Attribute ID")
|
|
3492
|
+
@click.option("--name", help="New attribute name")
|
|
3493
|
+
@click.option("--description", help="New attribute description")
|
|
3494
|
+
@click.option("--required", type=bool, help="Is this attribute required? (true/false)")
|
|
3495
|
+
def update_custom_attribute(attribute_id, name, description, required):
|
|
3496
|
+
"""Update an existing custom attribute definition."""
|
|
3497
|
+
client = UnifiedCatalogClient()
|
|
3498
|
+
args = {"--attribute-id": [attribute_id]}
|
|
3499
|
+
|
|
3500
|
+
if name:
|
|
3501
|
+
args["--name"] = [name]
|
|
3502
|
+
if description:
|
|
3503
|
+
args["--description"] = [description]
|
|
3504
|
+
if required is not None:
|
|
3505
|
+
args["--required"] = ["true" if required else "false"]
|
|
3506
|
+
|
|
3507
|
+
response = client.update_custom_attribute(args)
|
|
3508
|
+
|
|
3509
|
+
console.print(f"[green]SUCCESS:[/green] Custom attribute updated")
|
|
3510
|
+
_format_json_output(response)
|
|
3511
|
+
|
|
3512
|
+
|
|
3513
|
+
@attribute.command(name="delete")
|
|
3514
|
+
@click.option("--attribute-id", required=True, help="Attribute ID")
|
|
3515
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this attribute?")
|
|
3516
|
+
def delete_custom_attribute(attribute_id):
|
|
3517
|
+
"""Delete a custom attribute definition."""
|
|
3518
|
+
client = UnifiedCatalogClient()
|
|
3519
|
+
args = {"--attribute-id": [attribute_id]}
|
|
3520
|
+
response = client.delete_custom_attribute(args)
|
|
3521
|
+
|
|
3522
|
+
console.print(f"[green]SUCCESS:[/green] Custom attribute '{attribute_id}' deleted")
|
|
3523
|
+
|
|
3524
|
+
|
|
3525
|
+
# ========================================
|
|
3526
|
+
# REQUESTS (Coming Soon)
|
|
3527
|
+
# ========================================
|
|
3528
|
+
|
|
3529
|
+
|
|
3530
|
+
@uc.group()
|
|
3531
|
+
def request():
|
|
3532
|
+
"""Manage access requests (coming soon)."""
|
|
3533
|
+
pass
|
|
3534
|
+
|
|
3535
|
+
|
|
3536
|
+
@request.command(name="list")
|
|
3537
|
+
def list_requests():
|
|
3538
|
+
"""List access requests (coming soon)."""
|
|
3539
|
+
console.print("[yellow]🚧 Access Requests are coming soon[/yellow]")
|
|
3540
|
+
console.print("This feature is under development for data access workflows")
|