pvw-cli 1.0.8__py3-none-any.whl → 1.0.10__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 +1 -1
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/search.py +201 -10
- purviewcli/cli/unified_catalog.py +678 -142
- purviewcli/cli/workflow.py +44 -4
- purviewcli/client/_health.py +192 -0
- purviewcli/client/_unified_catalog.py +728 -62
- purviewcli/client/_workflow.py +3 -3
- purviewcli/client/endpoint.py +21 -0
- purviewcli/client/sync_client.py +13 -8
- pvw_cli-1.0.10.dist-info/METADATA +888 -0
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/RECORD +15 -13
- pvw_cli-1.0.8.dist-info/METADATA +0 -458
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/WHEEL +0 -0
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/top_level.txt +0 -0
|
@@ -11,11 +11,20 @@ import os
|
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
from rich.text import Text
|
|
14
|
+
from rich.syntax import Syntax
|
|
14
15
|
from purviewcli.client._unified_catalog import UnifiedCatalogClient
|
|
15
16
|
|
|
16
17
|
console = Console()
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def _format_json_output(data):
|
|
21
|
+
"""Format JSON output with syntax highlighting using Rich"""
|
|
22
|
+
# Pretty print JSON with syntax highlighting
|
|
23
|
+
json_str = json.dumps(data, indent=2)
|
|
24
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
|
|
25
|
+
console.print(syntax)
|
|
26
|
+
|
|
27
|
+
|
|
19
28
|
@click.group()
|
|
20
29
|
def uc():
|
|
21
30
|
"""Manage Unified Catalog in Microsoft Purview (domains, terms, data products, OKRs, CDEs)."""
|
|
@@ -88,7 +97,8 @@ def create(name, description, type, owner_id, status):
|
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
@domain.command(name="list")
|
|
91
|
-
|
|
100
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
101
|
+
def list_domains(output_json):
|
|
92
102
|
"""List all governance domains."""
|
|
93
103
|
try:
|
|
94
104
|
client = UnifiedCatalogClient()
|
|
@@ -111,6 +121,11 @@ def list_domains():
|
|
|
111
121
|
console.print("[yellow]No governance domains found.[/yellow]")
|
|
112
122
|
return
|
|
113
123
|
|
|
124
|
+
# Output in JSON format if requested
|
|
125
|
+
if output_json:
|
|
126
|
+
_format_json_output(domains)
|
|
127
|
+
return
|
|
128
|
+
|
|
114
129
|
table = Table(title="Governance Domains")
|
|
115
130
|
table.add_column("ID", style="cyan")
|
|
116
131
|
table.add_column("Name", style="green")
|
|
@@ -243,7 +258,8 @@ def create(
|
|
|
243
258
|
@dataproduct.command(name="list")
|
|
244
259
|
@click.option("--domain-id", required=False, help="Governance domain ID (optional filter)")
|
|
245
260
|
@click.option("--status", required=False, help="Status filter (Draft, Published, Archived)")
|
|
246
|
-
|
|
261
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
262
|
+
def list_data_products(domain_id, status, output_json):
|
|
247
263
|
"""List all data products (optionally filtered by domain or status)."""
|
|
248
264
|
try:
|
|
249
265
|
client = UnifiedCatalogClient()
|
|
@@ -274,24 +290,35 @@ def list_data_products(domain_id, status):
|
|
|
274
290
|
console.print(f"[yellow]No data products found{filter_msg}.[/yellow]")
|
|
275
291
|
return
|
|
276
292
|
|
|
293
|
+
# Output in JSON format if requested
|
|
294
|
+
if output_json:
|
|
295
|
+
_format_json_output(products)
|
|
296
|
+
return
|
|
297
|
+
|
|
277
298
|
table = Table(title="Data Products")
|
|
278
|
-
table.add_column("ID", style="cyan")
|
|
299
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
279
300
|
table.add_column("Name", style="green")
|
|
280
|
-
table.add_column("Domain ID", style="blue")
|
|
301
|
+
table.add_column("Domain ID", style="blue", no_wrap=True)
|
|
281
302
|
table.add_column("Status", style="yellow")
|
|
282
|
-
table.add_column("Description", style="white")
|
|
303
|
+
table.add_column("Description", style="white", max_width=50)
|
|
283
304
|
|
|
284
305
|
for product in products:
|
|
306
|
+
# Get domain ID and handle "N/A" display
|
|
307
|
+
domain_id = product.get("domain") or product.get("domainId", "")
|
|
308
|
+
domain_display = domain_id if domain_id else "N/A"
|
|
309
|
+
|
|
310
|
+
# Clean HTML tags from description
|
|
311
|
+
description = product.get("description", "")
|
|
312
|
+
import re
|
|
313
|
+
description = re.sub(r'<[^>]+>', '', description)
|
|
314
|
+
description = description.strip()
|
|
315
|
+
|
|
285
316
|
table.add_row(
|
|
286
317
|
product.get("id", "N/A"),
|
|
287
318
|
product.get("name", "N/A"),
|
|
288
|
-
|
|
319
|
+
domain_display,
|
|
289
320
|
product.get("status", "N/A"),
|
|
290
|
-
(
|
|
291
|
-
(product.get("description", "")[:50] + "...")
|
|
292
|
-
if len(product.get("description", "")) > 50
|
|
293
|
-
else product.get("description", "")
|
|
294
|
-
),
|
|
321
|
+
(description[:50] + "...") if len(description) > 50 else description,
|
|
295
322
|
)
|
|
296
323
|
|
|
297
324
|
console.print(table)
|
|
@@ -322,10 +349,417 @@ def show(product_id):
|
|
|
322
349
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
323
350
|
|
|
324
351
|
|
|
352
|
+
@dataproduct.command()
|
|
353
|
+
@click.option("--product-id", required=True, help="ID of the data product to update")
|
|
354
|
+
@click.option("--name", required=False, help="Name of the data product")
|
|
355
|
+
@click.option("--description", required=False, help="Description of the data product")
|
|
356
|
+
@click.option("--domain-id", required=False, help="Governance domain ID")
|
|
357
|
+
@click.option(
|
|
358
|
+
"--type",
|
|
359
|
+
required=False,
|
|
360
|
+
type=click.Choice(["Operational", "Analytical", "Reference"]),
|
|
361
|
+
help="Type of data product",
|
|
362
|
+
)
|
|
363
|
+
@click.option(
|
|
364
|
+
"--owner-id",
|
|
365
|
+
required=False,
|
|
366
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
367
|
+
multiple=True,
|
|
368
|
+
)
|
|
369
|
+
@click.option("--business-use", required=False, help="Business use description")
|
|
370
|
+
@click.option(
|
|
371
|
+
"--update-frequency",
|
|
372
|
+
required=False,
|
|
373
|
+
type=click.Choice(["Daily", "Weekly", "Monthly", "Quarterly", "Annually"]),
|
|
374
|
+
help="Update frequency",
|
|
375
|
+
)
|
|
376
|
+
@click.option("--endorsed", is_flag=True, help="Mark as endorsed")
|
|
377
|
+
@click.option(
|
|
378
|
+
"--status",
|
|
379
|
+
required=False,
|
|
380
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
381
|
+
help="Status of the data product",
|
|
382
|
+
)
|
|
383
|
+
def update(
|
|
384
|
+
product_id, name, description, domain_id, type, owner_id, business_use, update_frequency, endorsed, status
|
|
385
|
+
):
|
|
386
|
+
"""Update an existing data product."""
|
|
387
|
+
try:
|
|
388
|
+
client = UnifiedCatalogClient()
|
|
389
|
+
|
|
390
|
+
# Build args dictionary - only include provided values
|
|
391
|
+
args = {"--product-id": [product_id]}
|
|
392
|
+
|
|
393
|
+
if name:
|
|
394
|
+
args["--name"] = [name]
|
|
395
|
+
if description is not None: # Allow empty string
|
|
396
|
+
args["--description"] = [description]
|
|
397
|
+
if domain_id:
|
|
398
|
+
args["--domain-id"] = [domain_id]
|
|
399
|
+
if type:
|
|
400
|
+
args["--type"] = [type]
|
|
401
|
+
if status:
|
|
402
|
+
args["--status"] = [status]
|
|
403
|
+
if business_use is not None:
|
|
404
|
+
args["--business-use"] = [business_use]
|
|
405
|
+
if update_frequency:
|
|
406
|
+
args["--update-frequency"] = [update_frequency]
|
|
407
|
+
if endorsed:
|
|
408
|
+
args["--endorsed"] = ["true"]
|
|
409
|
+
if owner_id:
|
|
410
|
+
args["--owner-id"] = list(owner_id)
|
|
411
|
+
|
|
412
|
+
result = client.update_data_product(args)
|
|
413
|
+
|
|
414
|
+
if not result:
|
|
415
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
416
|
+
return
|
|
417
|
+
if isinstance(result, dict) and "error" in result:
|
|
418
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
console.print(f"[green]✅ SUCCESS:[/green] Updated data product '{product_id}'")
|
|
422
|
+
console.print(json.dumps(result, indent=2))
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@dataproduct.command()
|
|
429
|
+
@click.option("--product-id", required=True, help="ID of the data product to delete")
|
|
430
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
431
|
+
def delete(product_id, yes):
|
|
432
|
+
"""Delete a data product."""
|
|
433
|
+
try:
|
|
434
|
+
if not yes:
|
|
435
|
+
confirm = click.confirm(
|
|
436
|
+
f"Are you sure you want to delete data product '{product_id}'?",
|
|
437
|
+
default=False
|
|
438
|
+
)
|
|
439
|
+
if not confirm:
|
|
440
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
client = UnifiedCatalogClient()
|
|
444
|
+
args = {"--product-id": [product_id]}
|
|
445
|
+
result = client.delete_data_product(args)
|
|
446
|
+
|
|
447
|
+
# DELETE operations may return empty response on success
|
|
448
|
+
if result is None or (isinstance(result, dict) and not result.get("error")):
|
|
449
|
+
console.print(f"[green]✅ SUCCESS:[/green] Deleted data product '{product_id}'")
|
|
450
|
+
elif isinstance(result, dict) and "error" in result:
|
|
451
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
452
|
+
else:
|
|
453
|
+
console.print(f"[green]✅ SUCCESS:[/green] Deleted data product '{product_id}'")
|
|
454
|
+
if result:
|
|
455
|
+
console.print(json.dumps(result, indent=2))
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ========================================
|
|
462
|
+
# GLOSSARIES
|
|
463
|
+
# ========================================
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@uc.group()
|
|
467
|
+
def glossary():
|
|
468
|
+
"""Manage glossaries (for finding glossary GUIDs)."""
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@glossary.command(name="list")
|
|
473
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
474
|
+
def list_glossaries(output_json):
|
|
475
|
+
"""List all glossaries with their GUIDs."""
|
|
476
|
+
try:
|
|
477
|
+
from purviewcli.client._glossary import Glossary
|
|
478
|
+
|
|
479
|
+
client = Glossary()
|
|
480
|
+
result = client.glossaryRead({})
|
|
481
|
+
|
|
482
|
+
# Normalize response
|
|
483
|
+
if isinstance(result, dict):
|
|
484
|
+
glossaries = result.get("value", []) or []
|
|
485
|
+
elif isinstance(result, (list, tuple)):
|
|
486
|
+
glossaries = result
|
|
487
|
+
else:
|
|
488
|
+
glossaries = []
|
|
489
|
+
|
|
490
|
+
if not glossaries:
|
|
491
|
+
console.print("[yellow]No glossaries found.[/yellow]")
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
# Output in JSON format if requested
|
|
495
|
+
if output_json:
|
|
496
|
+
_format_json_output(glossaries)
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
table = Table(title="Glossaries")
|
|
500
|
+
table.add_column("GUID", style="cyan", no_wrap=True)
|
|
501
|
+
table.add_column("Name", style="green")
|
|
502
|
+
table.add_column("Qualified Name", style="yellow")
|
|
503
|
+
table.add_column("Description", style="white")
|
|
504
|
+
|
|
505
|
+
for g in glossaries:
|
|
506
|
+
if not isinstance(g, dict):
|
|
507
|
+
continue
|
|
508
|
+
table.add_row(
|
|
509
|
+
g.get("guid", "N/A"),
|
|
510
|
+
g.get("name", "N/A"),
|
|
511
|
+
g.get("qualifiedName", "N/A"),
|
|
512
|
+
(g.get("shortDescription", "")[:60] + "...") if len(g.get("shortDescription", "")) > 60 else g.get("shortDescription", ""),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
console.print(table)
|
|
516
|
+
console.print("\n[dim]Tip: Use the GUID with --glossary-guid option when listing/creating terms[/dim]")
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@glossary.command(name="create")
|
|
523
|
+
@click.option("--name", required=True, help="Name of the glossary")
|
|
524
|
+
@click.option("--description", required=False, default="", help="Description of the glossary")
|
|
525
|
+
@click.option("--domain-id", required=False, help="Associate with governance domain ID (optional)")
|
|
526
|
+
def create_glossary(name, description, domain_id):
|
|
527
|
+
"""Create a new glossary."""
|
|
528
|
+
try:
|
|
529
|
+
from purviewcli.client._glossary import Glossary
|
|
530
|
+
|
|
531
|
+
client = Glossary()
|
|
532
|
+
|
|
533
|
+
# Build qualified name - include domain_id if provided
|
|
534
|
+
if domain_id:
|
|
535
|
+
qualified_name = f"{name}@{domain_id}"
|
|
536
|
+
else:
|
|
537
|
+
qualified_name = name
|
|
538
|
+
|
|
539
|
+
payload = {
|
|
540
|
+
"name": name,
|
|
541
|
+
"qualifiedName": qualified_name,
|
|
542
|
+
"shortDescription": description,
|
|
543
|
+
"longDescription": description,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
result = client.glossaryCreate({"--payloadFile": payload})
|
|
547
|
+
|
|
548
|
+
if not result:
|
|
549
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
550
|
+
return
|
|
551
|
+
if isinstance(result, dict) and "error" in result:
|
|
552
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
guid = result.get("guid") if isinstance(result, dict) else None
|
|
556
|
+
console.print(f"[green]✅ SUCCESS:[/green] Created glossary '{name}'")
|
|
557
|
+
if guid:
|
|
558
|
+
console.print(f"[cyan]GUID:[/cyan] {guid}")
|
|
559
|
+
console.print(f"\n[dim]Use this GUID: --glossary-guid {guid}[/dim]")
|
|
560
|
+
console.print(json.dumps(result, indent=2))
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@glossary.command(name="create-for-domains")
|
|
567
|
+
def create_glossaries_for_domains():
|
|
568
|
+
"""Create glossaries for all governance domains that don't have one."""
|
|
569
|
+
try:
|
|
570
|
+
from purviewcli.client._glossary import Glossary
|
|
571
|
+
|
|
572
|
+
uc_client = UnifiedCatalogClient()
|
|
573
|
+
glossary_client = Glossary()
|
|
574
|
+
|
|
575
|
+
# Get all domains
|
|
576
|
+
domains_result = uc_client.get_governance_domains({})
|
|
577
|
+
if isinstance(domains_result, dict):
|
|
578
|
+
domains = domains_result.get("value", [])
|
|
579
|
+
elif isinstance(domains_result, (list, tuple)):
|
|
580
|
+
domains = domains_result
|
|
581
|
+
else:
|
|
582
|
+
domains = []
|
|
583
|
+
|
|
584
|
+
if not domains:
|
|
585
|
+
console.print("[yellow]No governance domains found.[/yellow]")
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# Get existing glossaries
|
|
589
|
+
glossaries_result = glossary_client.glossaryRead({})
|
|
590
|
+
if isinstance(glossaries_result, dict):
|
|
591
|
+
existing_glossaries = glossaries_result.get("value", [])
|
|
592
|
+
elif isinstance(glossaries_result, (list, tuple)):
|
|
593
|
+
existing_glossaries = glossaries_result
|
|
594
|
+
else:
|
|
595
|
+
existing_glossaries = []
|
|
596
|
+
|
|
597
|
+
# Build set of domain IDs that already have glossaries (check qualifiedName)
|
|
598
|
+
existing_domain_ids = set()
|
|
599
|
+
for g in existing_glossaries:
|
|
600
|
+
if isinstance(g, dict):
|
|
601
|
+
qn = g.get("qualifiedName", "")
|
|
602
|
+
# Extract domain_id from qualifiedName if it contains @domain_id pattern
|
|
603
|
+
if "@" in qn:
|
|
604
|
+
domain_id_part = qn.split("@")[-1]
|
|
605
|
+
existing_domain_ids.add(domain_id_part)
|
|
606
|
+
|
|
607
|
+
console.print(f"[cyan]Found {len(domains)} governance domains and {len(existing_glossaries)} existing glossaries[/cyan]\n")
|
|
608
|
+
|
|
609
|
+
created_count = 0
|
|
610
|
+
for domain in domains:
|
|
611
|
+
if not isinstance(domain, dict):
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
domain_id = domain.get("id")
|
|
615
|
+
domain_name = domain.get("name")
|
|
616
|
+
|
|
617
|
+
if not domain_id or not domain_name:
|
|
618
|
+
continue
|
|
619
|
+
|
|
620
|
+
# Check if glossary already exists for this domain
|
|
621
|
+
if domain_id in existing_domain_ids:
|
|
622
|
+
console.print(f"[dim]⏭ Skipping {domain_name} - glossary already exists[/dim]")
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
# Create glossary for this domain
|
|
626
|
+
glossary_name = f"{domain_name} Glossary"
|
|
627
|
+
qualified_name = f"{glossary_name}@{domain_id}"
|
|
628
|
+
|
|
629
|
+
payload = {
|
|
630
|
+
"name": glossary_name,
|
|
631
|
+
"qualifiedName": qualified_name,
|
|
632
|
+
"shortDescription": f"Glossary for {domain_name} domain",
|
|
633
|
+
"longDescription": f"This glossary contains business terms for the {domain_name} governance domain.",
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
result = glossary_client.glossaryCreate({"--payloadFile": payload})
|
|
638
|
+
guid = result.get("guid") if isinstance(result, dict) else None
|
|
639
|
+
|
|
640
|
+
if guid:
|
|
641
|
+
console.print(f"[green]✅ Created:[/green] {glossary_name} (GUID: {guid})")
|
|
642
|
+
created_count += 1
|
|
643
|
+
else:
|
|
644
|
+
console.print(f"[yellow]⚠ Created {glossary_name} but no GUID returned[/yellow]")
|
|
645
|
+
|
|
646
|
+
except Exception as e:
|
|
647
|
+
console.print(f"[red]❌ Failed to create {glossary_name}:[/red] {str(e)}")
|
|
648
|
+
|
|
649
|
+
console.print(f"\n[cyan]Created {created_count} new glossaries[/cyan]")
|
|
650
|
+
console.print("[dim]Run 'pvw uc glossary list' to see all glossaries[/dim]")
|
|
651
|
+
|
|
652
|
+
except Exception as e:
|
|
653
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@glossary.command(name="verify-links")
|
|
657
|
+
def verify_glossary_links():
|
|
658
|
+
"""Verify which domains have properly linked glossaries."""
|
|
659
|
+
try:
|
|
660
|
+
from purviewcli.client._glossary import Glossary
|
|
661
|
+
|
|
662
|
+
uc_client = UnifiedCatalogClient()
|
|
663
|
+
glossary_client = Glossary()
|
|
664
|
+
|
|
665
|
+
# Get all domains
|
|
666
|
+
domains_result = uc_client.get_governance_domains({})
|
|
667
|
+
if isinstance(domains_result, dict):
|
|
668
|
+
domains = domains_result.get("value", [])
|
|
669
|
+
elif isinstance(domains_result, (list, tuple)):
|
|
670
|
+
domains = domains_result
|
|
671
|
+
else:
|
|
672
|
+
domains = []
|
|
673
|
+
|
|
674
|
+
# Get all glossaries
|
|
675
|
+
glossaries_result = glossary_client.glossaryRead({})
|
|
676
|
+
if isinstance(glossaries_result, dict):
|
|
677
|
+
glossaries = glossaries_result.get("value", [])
|
|
678
|
+
elif isinstance(glossaries_result, (list, tuple)):
|
|
679
|
+
glossaries = glossaries_result
|
|
680
|
+
else:
|
|
681
|
+
glossaries = []
|
|
682
|
+
|
|
683
|
+
console.print(f"[bold cyan]Governance Domain → Glossary Link Verification[/bold cyan]\n")
|
|
684
|
+
|
|
685
|
+
table = Table(title="Domain-Glossary Associations")
|
|
686
|
+
table.add_column("Domain Name", style="green")
|
|
687
|
+
table.add_column("Domain ID", style="cyan", no_wrap=True)
|
|
688
|
+
table.add_column("Linked Glossary", style="yellow")
|
|
689
|
+
table.add_column("Glossary GUID", style="magenta", no_wrap=True)
|
|
690
|
+
table.add_column("Status", style="white")
|
|
691
|
+
|
|
692
|
+
# Build a map of domain_id -> glossary info
|
|
693
|
+
domain_glossary_map = {}
|
|
694
|
+
for g in glossaries:
|
|
695
|
+
if not isinstance(g, dict):
|
|
696
|
+
continue
|
|
697
|
+
qn = g.get("qualifiedName", "")
|
|
698
|
+
# Check if qualifiedName contains @domain_id pattern
|
|
699
|
+
if "@" in qn:
|
|
700
|
+
domain_id_part = qn.split("@")[-1]
|
|
701
|
+
domain_glossary_map[domain_id_part] = {
|
|
702
|
+
"name": g.get("name"),
|
|
703
|
+
"guid": g.get("guid"),
|
|
704
|
+
"qualifiedName": qn,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
linked_count = 0
|
|
708
|
+
unlinked_count = 0
|
|
709
|
+
|
|
710
|
+
for domain in domains:
|
|
711
|
+
if not isinstance(domain, dict):
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
domain_id = domain.get("id")
|
|
715
|
+
domain_name = domain.get("name", "N/A")
|
|
716
|
+
parent_id = domain.get("parentDomainId")
|
|
717
|
+
|
|
718
|
+
# Skip if no domain_id
|
|
719
|
+
if not domain_id:
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
# Show if it's a nested domain
|
|
723
|
+
nested_indicator = " (nested)" if parent_id else ""
|
|
724
|
+
domain_display = f"{domain_name}{nested_indicator}"
|
|
725
|
+
|
|
726
|
+
if domain_id in domain_glossary_map:
|
|
727
|
+
glossary_info = domain_glossary_map[domain_id]
|
|
728
|
+
table.add_row(
|
|
729
|
+
domain_display,
|
|
730
|
+
domain_id[:8] + "...",
|
|
731
|
+
glossary_info["name"],
|
|
732
|
+
glossary_info["guid"][:8] + "...",
|
|
733
|
+
"[green]✅ Linked[/green]"
|
|
734
|
+
)
|
|
735
|
+
linked_count += 1
|
|
736
|
+
else:
|
|
737
|
+
table.add_row(
|
|
738
|
+
domain_display,
|
|
739
|
+
domain_id[:8] + "...",
|
|
740
|
+
"[dim]No glossary[/dim]",
|
|
741
|
+
"[dim]N/A[/dim]",
|
|
742
|
+
"[yellow]⚠ Not Linked[/yellow]"
|
|
743
|
+
)
|
|
744
|
+
unlinked_count += 1
|
|
745
|
+
|
|
746
|
+
console.print(table)
|
|
747
|
+
console.print(f"\n[cyan]Summary:[/cyan]")
|
|
748
|
+
console.print(f" • Linked domains: [green]{linked_count}[/green]")
|
|
749
|
+
console.print(f" • Unlinked domains: [yellow]{unlinked_count}[/yellow]")
|
|
750
|
+
|
|
751
|
+
if unlinked_count > 0:
|
|
752
|
+
console.print(f"\n[dim]💡 Tip: Run 'pvw uc glossary create-for-domains' to create glossaries for unlinked domains[/dim]")
|
|
753
|
+
|
|
754
|
+
except Exception as e:
|
|
755
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
756
|
+
|
|
757
|
+
|
|
325
758
|
# ========================================
|
|
326
759
|
# GLOSSARY TERMS
|
|
327
760
|
# ========================================
|
|
328
761
|
|
|
762
|
+
|
|
329
763
|
@uc.group()
|
|
330
764
|
def term():
|
|
331
765
|
"""Manage glossary terms."""
|
|
@@ -336,119 +770,204 @@ def term():
|
|
|
336
770
|
@click.option("--name", required=True, help="Name of the glossary term")
|
|
337
771
|
@click.option("--description", required=False, default="", help="Rich text description of the term")
|
|
338
772
|
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
339
|
-
@click.option(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
773
|
+
@click.option(
|
|
774
|
+
"--status",
|
|
775
|
+
required=False,
|
|
776
|
+
default="Draft",
|
|
777
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
778
|
+
help="Status of the term",
|
|
779
|
+
)
|
|
780
|
+
@click.option(
|
|
781
|
+
"--acronym",
|
|
782
|
+
required=False,
|
|
783
|
+
help="Acronyms for the term (can be specified multiple times)",
|
|
784
|
+
multiple=True,
|
|
785
|
+
)
|
|
786
|
+
@click.option(
|
|
787
|
+
"--owner-id",
|
|
788
|
+
required=False,
|
|
789
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
790
|
+
multiple=True,
|
|
791
|
+
)
|
|
792
|
+
@click.option("--resource-name", required=False, help="Resource name for additional reading (can be specified multiple times)", multiple=True)
|
|
793
|
+
@click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times)", multiple=True)
|
|
346
794
|
def create(name, description, domain_id, status, acronym, owner_id, resource_name, resource_url):
|
|
347
|
-
"""Create a new
|
|
795
|
+
"""Create a new Unified Catalog term (Governance Domain term)."""
|
|
348
796
|
try:
|
|
349
797
|
client = UnifiedCatalogClient()
|
|
350
|
-
|
|
798
|
+
|
|
351
799
|
# Build args dictionary
|
|
352
800
|
args = {
|
|
353
801
|
"--name": [name],
|
|
354
802
|
"--description": [description],
|
|
355
803
|
"--governance-domain-id": [domain_id],
|
|
356
|
-
"--status": [status]
|
|
804
|
+
"--status": [status],
|
|
357
805
|
}
|
|
358
|
-
|
|
806
|
+
|
|
359
807
|
if acronym:
|
|
360
|
-
args["--
|
|
808
|
+
args["--acronym"] = list(acronym)
|
|
361
809
|
if owner_id:
|
|
362
810
|
args["--owner-id"] = list(owner_id)
|
|
363
|
-
if resource_name
|
|
364
|
-
args["--resource-name"] =
|
|
365
|
-
|
|
366
|
-
|
|
811
|
+
if resource_name:
|
|
812
|
+
args["--resource-name"] = list(resource_name)
|
|
813
|
+
if resource_url:
|
|
814
|
+
args["--resource-url"] = list(resource_url)
|
|
815
|
+
|
|
367
816
|
result = client.create_term(args)
|
|
368
|
-
|
|
817
|
+
|
|
369
818
|
if not result:
|
|
370
819
|
console.print("[red]ERROR:[/red] No response received")
|
|
371
820
|
return
|
|
372
821
|
if isinstance(result, dict) and "error" in result:
|
|
373
822
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
374
823
|
return
|
|
375
|
-
|
|
824
|
+
|
|
376
825
|
console.print(f"[green]✅ SUCCESS:[/green] Created glossary term '{name}'")
|
|
377
826
|
console.print(json.dumps(result, indent=2))
|
|
378
|
-
|
|
827
|
+
|
|
379
828
|
except Exception as e:
|
|
380
829
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
381
830
|
|
|
382
831
|
|
|
383
832
|
@term.command(name="list")
|
|
384
833
|
@click.option("--domain-id", required=True, help="Governance domain ID to list terms from")
|
|
385
|
-
|
|
386
|
-
|
|
834
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
835
|
+
def list_terms(domain_id, output_json):
|
|
836
|
+
"""List all Unified Catalog terms in a governance domain."""
|
|
387
837
|
try:
|
|
388
838
|
client = UnifiedCatalogClient()
|
|
389
839
|
args = {"--governance-domain-id": [domain_id]}
|
|
390
840
|
result = client.get_terms(args)
|
|
391
|
-
|
|
841
|
+
|
|
392
842
|
if not result:
|
|
393
|
-
console.print("[yellow]No
|
|
843
|
+
console.print("[yellow]No terms found.[/yellow]")
|
|
394
844
|
return
|
|
395
|
-
|
|
396
|
-
#
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
845
|
+
|
|
846
|
+
# Unified Catalog API returns terms directly in value array
|
|
847
|
+
all_terms = []
|
|
848
|
+
|
|
849
|
+
if isinstance(result, dict):
|
|
850
|
+
all_terms = result.get("value", [])
|
|
851
|
+
elif isinstance(result, (list, tuple)):
|
|
852
|
+
all_terms = result
|
|
401
853
|
else:
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if not terms:
|
|
405
|
-
console.print("[yellow]No glossary terms found.[/yellow]")
|
|
854
|
+
console.print("[yellow]Unexpected response format.[/yellow]")
|
|
406
855
|
return
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
856
|
+
|
|
857
|
+
if not all_terms:
|
|
858
|
+
console.print("[yellow]No terms found.[/yellow]")
|
|
859
|
+
return
|
|
860
|
+
|
|
861
|
+
# Output in JSON format if requested
|
|
862
|
+
if output_json:
|
|
863
|
+
_format_json_output(all_terms)
|
|
864
|
+
return
|
|
865
|
+
|
|
866
|
+
table = Table(title="Unified Catalog Terms")
|
|
867
|
+
table.add_column("Term ID", style="cyan", no_wrap=False)
|
|
410
868
|
table.add_column("Name", style="green")
|
|
411
|
-
table.add_column("Status", style="yellow")
|
|
412
|
-
table.add_column("Acronyms", style="blue")
|
|
869
|
+
table.add_column("Status", style="yellow")
|
|
413
870
|
table.add_column("Description", style="white")
|
|
414
|
-
|
|
415
|
-
for term in
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
871
|
+
|
|
872
|
+
for term in all_terms:
|
|
873
|
+
description = term.get("description", "")
|
|
874
|
+
# Strip HTML tags from description
|
|
875
|
+
import re
|
|
876
|
+
description = re.sub(r'<[^>]+>', '', description)
|
|
877
|
+
# Truncate long descriptions
|
|
878
|
+
if len(description) > 50:
|
|
879
|
+
description = description[:50] + "..."
|
|
880
|
+
|
|
421
881
|
table.add_row(
|
|
422
882
|
term.get("id", "N/A"),
|
|
423
883
|
term.get("name", "N/A"),
|
|
424
884
|
term.get("status", "N/A"),
|
|
425
|
-
|
|
426
|
-
desc
|
|
885
|
+
description.strip(),
|
|
427
886
|
)
|
|
428
|
-
|
|
887
|
+
|
|
429
888
|
console.print(table)
|
|
430
|
-
|
|
889
|
+
console.print(f"\n[dim]Found {len(all_terms)} term(s)[/dim]")
|
|
890
|
+
|
|
431
891
|
except Exception as e:
|
|
432
892
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
433
893
|
|
|
434
894
|
|
|
435
895
|
@term.command()
|
|
436
896
|
@click.option("--term-id", required=True, help="ID of the glossary term")
|
|
437
|
-
|
|
897
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
898
|
+
def show(term_id, output_json):
|
|
438
899
|
"""Show details of a glossary term."""
|
|
439
900
|
try:
|
|
440
901
|
client = UnifiedCatalogClient()
|
|
441
902
|
args = {"--term-id": [term_id]}
|
|
442
903
|
result = client.get_term_by_id(args)
|
|
443
|
-
|
|
904
|
+
|
|
444
905
|
if not result:
|
|
445
906
|
console.print("[red]ERROR:[/red] No response received")
|
|
446
907
|
return
|
|
447
908
|
if isinstance(result, dict) and "error" in result:
|
|
448
909
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'Term not found')}")
|
|
449
910
|
return
|
|
911
|
+
|
|
912
|
+
if output_json:
|
|
913
|
+
_format_json_output(result)
|
|
914
|
+
else:
|
|
915
|
+
# Display key information in a readable format
|
|
916
|
+
if isinstance(result, dict):
|
|
917
|
+
console.print(f"[cyan]Term Name:[/cyan] {result.get('name', 'N/A')}")
|
|
918
|
+
console.print(f"[cyan]GUID:[/cyan] {result.get('guid', 'N/A')}")
|
|
919
|
+
console.print(f"[cyan]Status:[/cyan] {result.get('status', 'N/A')}")
|
|
920
|
+
console.print(f"[cyan]Qualified Name:[/cyan] {result.get('qualifiedName', 'N/A')}")
|
|
921
|
+
|
|
922
|
+
# Show glossary info
|
|
923
|
+
anchor = result.get('anchor', {})
|
|
924
|
+
if anchor:
|
|
925
|
+
console.print(f"[cyan]Glossary GUID:[/cyan] {anchor.get('glossaryGuid', 'N/A')}")
|
|
926
|
+
|
|
927
|
+
# Show description
|
|
928
|
+
desc = result.get('shortDescription') or result.get('longDescription', '')
|
|
929
|
+
if desc:
|
|
930
|
+
console.print(f"[cyan]Description:[/cyan] {desc}")
|
|
931
|
+
|
|
932
|
+
# Show full JSON if needed
|
|
933
|
+
console.print(f"\n[dim]Full details (JSON):[/dim]")
|
|
934
|
+
console.print(json.dumps(result, indent=2))
|
|
935
|
+
else:
|
|
936
|
+
console.print(json.dumps(result, indent=2))
|
|
937
|
+
|
|
938
|
+
except Exception as e:
|
|
939
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
@term.command()
|
|
943
|
+
@click.option("--term-id", required=True, help="ID of the glossary term to delete")
|
|
944
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
945
|
+
def delete(term_id, force):
|
|
946
|
+
"""Delete a glossary term."""
|
|
947
|
+
try:
|
|
948
|
+
if not force:
|
|
949
|
+
# Show term details first
|
|
950
|
+
client = UnifiedCatalogClient()
|
|
951
|
+
term_info = client.get_term_by_id({"--term-id": [term_id]})
|
|
450
952
|
|
|
451
|
-
|
|
953
|
+
if isinstance(term_info, dict) and term_info.get('name'):
|
|
954
|
+
console.print(f"[yellow]About to delete term:[/yellow]")
|
|
955
|
+
console.print(f" Name: {term_info.get('name')}")
|
|
956
|
+
console.print(f" GUID: {term_info.get('guid')}")
|
|
957
|
+
console.print(f" Status: {term_info.get('status')}")
|
|
958
|
+
|
|
959
|
+
confirm = click.confirm("Are you sure you want to delete this term?", default=False)
|
|
960
|
+
if not confirm:
|
|
961
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
962
|
+
return
|
|
963
|
+
|
|
964
|
+
# Import glossary client to delete term
|
|
965
|
+
from purviewcli.client._glossary import Glossary
|
|
966
|
+
|
|
967
|
+
gclient = Glossary()
|
|
968
|
+
result = gclient.glossaryDeleteTerm({"--termGuid": term_id})
|
|
969
|
+
|
|
970
|
+
console.print(f"[green]✅ SUCCESS:[/green] Deleted term with ID: {term_id}")
|
|
452
971
|
|
|
453
972
|
except Exception as e:
|
|
454
973
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
@@ -458,6 +977,7 @@ def show(term_id):
|
|
|
458
977
|
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
459
978
|
# ========================================
|
|
460
979
|
|
|
980
|
+
|
|
461
981
|
@uc.group()
|
|
462
982
|
def objective():
|
|
463
983
|
"""Manage objectives and key results (OKRs)."""
|
|
@@ -467,56 +987,68 @@ def objective():
|
|
|
467
987
|
@objective.command()
|
|
468
988
|
@click.option("--definition", required=True, help="Definition of the objective")
|
|
469
989
|
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
470
|
-
@click.option(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
990
|
+
@click.option(
|
|
991
|
+
"--status",
|
|
992
|
+
required=False,
|
|
993
|
+
default="Draft",
|
|
994
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
995
|
+
help="Status of the objective",
|
|
996
|
+
)
|
|
997
|
+
@click.option(
|
|
998
|
+
"--owner-id",
|
|
999
|
+
required=False,
|
|
1000
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
1001
|
+
multiple=True,
|
|
1002
|
+
)
|
|
1003
|
+
@click.option(
|
|
1004
|
+
"--target-date", required=False, help="Target date (ISO format: 2025-12-30T14:00:00.000Z)"
|
|
1005
|
+
)
|
|
475
1006
|
def create(definition, domain_id, status, owner_id, target_date):
|
|
476
1007
|
"""Create a new objective."""
|
|
477
1008
|
try:
|
|
478
1009
|
client = UnifiedCatalogClient()
|
|
479
|
-
|
|
1010
|
+
|
|
480
1011
|
args = {
|
|
481
1012
|
"--definition": [definition],
|
|
482
1013
|
"--governance-domain-id": [domain_id],
|
|
483
|
-
"--status": [status]
|
|
1014
|
+
"--status": [status],
|
|
484
1015
|
}
|
|
485
|
-
|
|
1016
|
+
|
|
486
1017
|
if owner_id:
|
|
487
1018
|
args["--owner-id"] = list(owner_id)
|
|
488
1019
|
if target_date:
|
|
489
1020
|
args["--target-date"] = [target_date]
|
|
490
|
-
|
|
1021
|
+
|
|
491
1022
|
result = client.create_objective(args)
|
|
492
|
-
|
|
1023
|
+
|
|
493
1024
|
if not result:
|
|
494
1025
|
console.print("[red]ERROR:[/red] No response received")
|
|
495
1026
|
return
|
|
496
1027
|
if isinstance(result, dict) and "error" in result:
|
|
497
1028
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
498
1029
|
return
|
|
499
|
-
|
|
1030
|
+
|
|
500
1031
|
console.print(f"[green]✅ SUCCESS:[/green] Created objective")
|
|
501
1032
|
console.print(json.dumps(result, indent=2))
|
|
502
|
-
|
|
1033
|
+
|
|
503
1034
|
except Exception as e:
|
|
504
1035
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
505
1036
|
|
|
506
1037
|
|
|
507
1038
|
@objective.command(name="list")
|
|
508
1039
|
@click.option("--domain-id", required=True, help="Governance domain ID to list objectives from")
|
|
509
|
-
|
|
1040
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
1041
|
+
def list_objectives(domain_id, output_json):
|
|
510
1042
|
"""List all objectives in a governance domain."""
|
|
511
1043
|
try:
|
|
512
1044
|
client = UnifiedCatalogClient()
|
|
513
1045
|
args = {"--governance-domain-id": [domain_id]}
|
|
514
1046
|
result = client.get_objectives(args)
|
|
515
|
-
|
|
1047
|
+
|
|
516
1048
|
if not result:
|
|
517
1049
|
console.print("[yellow]No objectives found.[/yellow]")
|
|
518
1050
|
return
|
|
519
|
-
|
|
1051
|
+
|
|
520
1052
|
# Handle response format
|
|
521
1053
|
if isinstance(result, (list, tuple)):
|
|
522
1054
|
objectives = result
|
|
@@ -524,31 +1056,36 @@ def list_objectives(domain_id):
|
|
|
524
1056
|
objectives = result.get("value", [])
|
|
525
1057
|
else:
|
|
526
1058
|
objectives = []
|
|
527
|
-
|
|
1059
|
+
|
|
528
1060
|
if not objectives:
|
|
529
1061
|
console.print("[yellow]No objectives found.[/yellow]")
|
|
530
1062
|
return
|
|
531
|
-
|
|
1063
|
+
|
|
1064
|
+
# Output in JSON format if requested
|
|
1065
|
+
if output_json:
|
|
1066
|
+
_format_json_output(objectives)
|
|
1067
|
+
return
|
|
1068
|
+
|
|
532
1069
|
table = Table(title="Objectives")
|
|
533
1070
|
table.add_column("ID", style="cyan")
|
|
534
1071
|
table.add_column("Definition", style="green")
|
|
535
1072
|
table.add_column("Status", style="yellow")
|
|
536
1073
|
table.add_column("Target Date", style="blue")
|
|
537
|
-
|
|
1074
|
+
|
|
538
1075
|
for obj in objectives:
|
|
539
1076
|
definition = obj.get("definition", "")
|
|
540
1077
|
if len(definition) > 50:
|
|
541
1078
|
definition = definition[:50] + "..."
|
|
542
|
-
|
|
1079
|
+
|
|
543
1080
|
table.add_row(
|
|
544
1081
|
obj.get("id", "N/A"),
|
|
545
1082
|
definition,
|
|
546
1083
|
obj.get("status", "N/A"),
|
|
547
|
-
obj.get("targetDate", "N/A")
|
|
1084
|
+
obj.get("targetDate", "N/A"),
|
|
548
1085
|
)
|
|
549
|
-
|
|
1086
|
+
|
|
550
1087
|
console.print(table)
|
|
551
|
-
|
|
1088
|
+
|
|
552
1089
|
except Exception as e:
|
|
553
1090
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
554
1091
|
|
|
@@ -561,16 +1098,16 @@ def show(objective_id):
|
|
|
561
1098
|
client = UnifiedCatalogClient()
|
|
562
1099
|
args = {"--objective-id": [objective_id]}
|
|
563
1100
|
result = client.get_objective_by_id(args)
|
|
564
|
-
|
|
1101
|
+
|
|
565
1102
|
if not result:
|
|
566
1103
|
console.print("[red]ERROR:[/red] No response received")
|
|
567
1104
|
return
|
|
568
1105
|
if isinstance(result, dict) and "error" in result:
|
|
569
1106
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'Objective not found')}")
|
|
570
1107
|
return
|
|
571
|
-
|
|
1108
|
+
|
|
572
1109
|
console.print(json.dumps(result, indent=2))
|
|
573
|
-
|
|
1110
|
+
|
|
574
1111
|
except Exception as e:
|
|
575
1112
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
576
1113
|
|
|
@@ -579,6 +1116,7 @@ def show(objective_id):
|
|
|
579
1116
|
# CRITICAL DATA ELEMENTS (CDEs)
|
|
580
1117
|
# ========================================
|
|
581
1118
|
|
|
1119
|
+
|
|
582
1120
|
@uc.group()
|
|
583
1121
|
def cde():
|
|
584
1122
|
"""Manage critical data elements."""
|
|
@@ -589,58 +1127,71 @@ def cde():
|
|
|
589
1127
|
@click.option("--name", required=True, help="Name of the critical data element")
|
|
590
1128
|
@click.option("--description", required=False, default="", help="Description of the CDE")
|
|
591
1129
|
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
592
|
-
@click.option(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
@click.option(
|
|
1130
|
+
@click.option(
|
|
1131
|
+
"--data-type",
|
|
1132
|
+
required=True,
|
|
1133
|
+
type=click.Choice(["String", "Number", "Boolean", "Date", "DateTime"]),
|
|
1134
|
+
help="Data type of the CDE",
|
|
1135
|
+
)
|
|
1136
|
+
@click.option(
|
|
1137
|
+
"--status",
|
|
1138
|
+
required=False,
|
|
1139
|
+
default="Draft",
|
|
1140
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
1141
|
+
help="Status of the CDE",
|
|
1142
|
+
)
|
|
1143
|
+
@click.option(
|
|
1144
|
+
"--owner-id",
|
|
1145
|
+
required=False,
|
|
1146
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
1147
|
+
multiple=True,
|
|
1148
|
+
)
|
|
599
1149
|
def create(name, description, domain_id, data_type, status, owner_id):
|
|
600
1150
|
"""Create a new critical data element."""
|
|
601
1151
|
try:
|
|
602
1152
|
client = UnifiedCatalogClient()
|
|
603
|
-
|
|
1153
|
+
|
|
604
1154
|
args = {
|
|
605
1155
|
"--name": [name],
|
|
606
1156
|
"--description": [description],
|
|
607
1157
|
"--governance-domain-id": [domain_id],
|
|
608
1158
|
"--data-type": [data_type],
|
|
609
|
-
"--status": [status]
|
|
1159
|
+
"--status": [status],
|
|
610
1160
|
}
|
|
611
|
-
|
|
1161
|
+
|
|
612
1162
|
if owner_id:
|
|
613
1163
|
args["--owner-id"] = list(owner_id)
|
|
614
|
-
|
|
1164
|
+
|
|
615
1165
|
result = client.create_critical_data_element(args)
|
|
616
|
-
|
|
1166
|
+
|
|
617
1167
|
if not result:
|
|
618
1168
|
console.print("[red]ERROR:[/red] No response received")
|
|
619
1169
|
return
|
|
620
1170
|
if isinstance(result, dict) and "error" in result:
|
|
621
1171
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
622
1172
|
return
|
|
623
|
-
|
|
1173
|
+
|
|
624
1174
|
console.print(f"[green]✅ SUCCESS:[/green] Created critical data element '{name}'")
|
|
625
1175
|
console.print(json.dumps(result, indent=2))
|
|
626
|
-
|
|
1176
|
+
|
|
627
1177
|
except Exception as e:
|
|
628
1178
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
629
1179
|
|
|
630
1180
|
|
|
631
1181
|
@cde.command(name="list")
|
|
632
1182
|
@click.option("--domain-id", required=True, help="Governance domain ID to list CDEs from")
|
|
633
|
-
|
|
1183
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
1184
|
+
def list_cdes(domain_id, output_json):
|
|
634
1185
|
"""List all critical data elements in a governance domain."""
|
|
635
1186
|
try:
|
|
636
1187
|
client = UnifiedCatalogClient()
|
|
637
1188
|
args = {"--governance-domain-id": [domain_id]}
|
|
638
1189
|
result = client.get_critical_data_elements(args)
|
|
639
|
-
|
|
1190
|
+
|
|
640
1191
|
if not result:
|
|
641
1192
|
console.print("[yellow]No critical data elements found.[/yellow]")
|
|
642
1193
|
return
|
|
643
|
-
|
|
1194
|
+
|
|
644
1195
|
# Handle response format
|
|
645
1196
|
if isinstance(result, (list, tuple)):
|
|
646
1197
|
cdes = result
|
|
@@ -648,33 +1199,38 @@ def list_cdes(domain_id):
|
|
|
648
1199
|
cdes = result.get("value", [])
|
|
649
1200
|
else:
|
|
650
1201
|
cdes = []
|
|
651
|
-
|
|
1202
|
+
|
|
652
1203
|
if not cdes:
|
|
653
1204
|
console.print("[yellow]No critical data elements found.[/yellow]")
|
|
654
1205
|
return
|
|
655
|
-
|
|
1206
|
+
|
|
1207
|
+
# Output in JSON format if requested
|
|
1208
|
+
if output_json:
|
|
1209
|
+
_format_json_output(cdes)
|
|
1210
|
+
return
|
|
1211
|
+
|
|
656
1212
|
table = Table(title="Critical Data Elements")
|
|
657
1213
|
table.add_column("ID", style="cyan")
|
|
658
1214
|
table.add_column("Name", style="green")
|
|
659
1215
|
table.add_column("Data Type", style="blue")
|
|
660
1216
|
table.add_column("Status", style="yellow")
|
|
661
1217
|
table.add_column("Description", style="white")
|
|
662
|
-
|
|
1218
|
+
|
|
663
1219
|
for cde_item in cdes:
|
|
664
1220
|
desc = cde_item.get("description", "")
|
|
665
1221
|
if len(desc) > 30:
|
|
666
1222
|
desc = desc[:30] + "..."
|
|
667
|
-
|
|
1223
|
+
|
|
668
1224
|
table.add_row(
|
|
669
1225
|
cde_item.get("id", "N/A"),
|
|
670
1226
|
cde_item.get("name", "N/A"),
|
|
671
1227
|
cde_item.get("dataType", "N/A"),
|
|
672
1228
|
cde_item.get("status", "N/A"),
|
|
673
|
-
desc
|
|
1229
|
+
desc,
|
|
674
1230
|
)
|
|
675
|
-
|
|
1231
|
+
|
|
676
1232
|
console.print(table)
|
|
677
|
-
|
|
1233
|
+
|
|
678
1234
|
except Exception as e:
|
|
679
1235
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
680
1236
|
|
|
@@ -687,55 +1243,34 @@ def show(cde_id):
|
|
|
687
1243
|
client = UnifiedCatalogClient()
|
|
688
1244
|
args = {"--cde-id": [cde_id]}
|
|
689
1245
|
result = client.get_critical_data_element_by_id(args)
|
|
690
|
-
|
|
1246
|
+
|
|
691
1247
|
if not result:
|
|
692
1248
|
console.print("[red]ERROR:[/red] No response received")
|
|
693
1249
|
return
|
|
694
1250
|
if isinstance(result, dict) and "error" in result:
|
|
695
1251
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'CDE not found')}")
|
|
696
1252
|
return
|
|
697
|
-
|
|
1253
|
+
|
|
698
1254
|
console.print(json.dumps(result, indent=2))
|
|
699
|
-
|
|
1255
|
+
|
|
700
1256
|
except Exception as e:
|
|
701
1257
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
702
1258
|
|
|
703
1259
|
|
|
704
1260
|
# ========================================
|
|
705
|
-
# HEALTH MANAGEMENT
|
|
1261
|
+
# HEALTH MANAGEMENT - IMPLEMENTED! ✅
|
|
706
1262
|
# ========================================
|
|
707
1263
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
pass
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
@health.command(name="controls")
|
|
715
|
-
def list_controls():
|
|
716
|
-
"""List health controls (preview - not yet implemented)."""
|
|
717
|
-
console.print("[yellow]🚧 Health Controls are not yet implemented in the API[/yellow]")
|
|
718
|
-
console.print("This feature is coming soon to Microsoft Purview Unified Catalog")
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
@health.command(name="actions")
|
|
722
|
-
def list_actions():
|
|
723
|
-
"""List health actions (preview - not yet implemented)."""
|
|
724
|
-
console.print("[yellow]🚧 Health Actions are not yet implemented in the API[/yellow]")
|
|
725
|
-
console.print("This feature is coming soon to Microsoft Purview Unified Catalog")
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
@health.command(name="quality")
|
|
729
|
-
def data_quality():
|
|
730
|
-
"""Data quality management (not yet supported by API)."""
|
|
731
|
-
console.print("[yellow]🚧 Data Quality management is not yet supported by the API[/yellow]")
|
|
732
|
-
console.print("This feature requires complex API interactions not yet available")
|
|
1264
|
+
# Import and register health commands from dedicated module
|
|
1265
|
+
from purviewcli.cli.health import health as health_commands
|
|
1266
|
+
uc.add_command(health_commands, name="health")
|
|
733
1267
|
|
|
734
1268
|
|
|
735
1269
|
# ========================================
|
|
736
1270
|
# CUSTOM ATTRIBUTES (Coming Soon)
|
|
737
1271
|
# ========================================
|
|
738
1272
|
|
|
1273
|
+
|
|
739
1274
|
@uc.group()
|
|
740
1275
|
def attribute():
|
|
741
1276
|
"""Manage custom attributes (coming soon)."""
|
|
@@ -753,6 +1288,7 @@ def list_attributes():
|
|
|
753
1288
|
# REQUESTS (Coming Soon)
|
|
754
1289
|
# ========================================
|
|
755
1290
|
|
|
1291
|
+
|
|
756
1292
|
@uc.group()
|
|
757
1293
|
def request():
|
|
758
1294
|
"""Manage access requests (coming soon)."""
|
|
@@ -763,4 +1299,4 @@ def request():
|
|
|
763
1299
|
def list_requests():
|
|
764
1300
|
"""List access requests (coming soon)."""
|
|
765
1301
|
console.print("[yellow]🚧 Access Requests are coming soon[/yellow]")
|
|
766
|
-
console.print("This feature is under development for data access workflows")
|
|
1302
|
+
console.print("This feature is under development for data access workflows")
|