pvw-cli 1.0.9__py3-none-any.whl → 1.0.11__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/entity.py +34 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/unified_catalog.py +519 -74
- purviewcli/cli/workflow.py +44 -4
- purviewcli/client/_health.py +192 -0
- purviewcli/client/_unified_catalog.py +715 -95
- purviewcli/client/_workflow.py +3 -3
- purviewcli/client/endpoint.py +21 -0
- purviewcli/client/sync_client.py +7 -2
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/METADATA +110 -22
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/RECORD +15 -13
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/WHEEL +0 -0
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/top_level.txt +0 -0
|
@@ -296,23 +296,29 @@ def list_data_products(domain_id, status, output_json):
|
|
|
296
296
|
return
|
|
297
297
|
|
|
298
298
|
table = Table(title="Data Products")
|
|
299
|
-
table.add_column("ID", style="cyan")
|
|
299
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
300
300
|
table.add_column("Name", style="green")
|
|
301
|
-
table.add_column("Domain ID", style="blue")
|
|
301
|
+
table.add_column("Domain ID", style="blue", no_wrap=True)
|
|
302
302
|
table.add_column("Status", style="yellow")
|
|
303
|
-
table.add_column("Description", style="white")
|
|
303
|
+
table.add_column("Description", style="white", max_width=50)
|
|
304
304
|
|
|
305
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
|
+
|
|
306
316
|
table.add_row(
|
|
307
317
|
product.get("id", "N/A"),
|
|
308
318
|
product.get("name", "N/A"),
|
|
309
|
-
|
|
319
|
+
domain_display,
|
|
310
320
|
product.get("status", "N/A"),
|
|
311
|
-
(
|
|
312
|
-
(product.get("description", "")[:50] + "...")
|
|
313
|
-
if len(product.get("description", "")) > 50
|
|
314
|
-
else product.get("description", "")
|
|
315
|
-
),
|
|
321
|
+
(description[:50] + "...") if len(description) > 50 else description,
|
|
316
322
|
)
|
|
317
323
|
|
|
318
324
|
console.print(table)
|
|
@@ -343,6 +349,412 @@ def show(product_id):
|
|
|
343
349
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
344
350
|
|
|
345
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
|
+
|
|
346
758
|
# ========================================
|
|
347
759
|
# GLOSSARY TERMS
|
|
348
760
|
# ========================================
|
|
@@ -377,10 +789,10 @@ def term():
|
|
|
377
789
|
help="Owner Entra ID (can be specified multiple times)",
|
|
378
790
|
multiple=True,
|
|
379
791
|
)
|
|
380
|
-
@click.option("--resource-name", required=False, help="Resource name for additional reading")
|
|
381
|
-
@click.option("--resource-url", required=False, help="Resource URL for additional reading")
|
|
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)
|
|
382
794
|
def create(name, description, domain_id, status, acronym, owner_id, resource_name, resource_url):
|
|
383
|
-
"""Create a new
|
|
795
|
+
"""Create a new Unified Catalog term (Governance Domain term)."""
|
|
384
796
|
try:
|
|
385
797
|
client = UnifiedCatalogClient()
|
|
386
798
|
|
|
@@ -393,12 +805,13 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
|
|
|
393
805
|
}
|
|
394
806
|
|
|
395
807
|
if acronym:
|
|
396
|
-
args["--
|
|
808
|
+
args["--acronym"] = list(acronym)
|
|
397
809
|
if owner_id:
|
|
398
810
|
args["--owner-id"] = list(owner_id)
|
|
399
|
-
if resource_name
|
|
400
|
-
args["--resource-name"] =
|
|
401
|
-
|
|
811
|
+
if resource_name:
|
|
812
|
+
args["--resource-name"] = list(resource_name)
|
|
813
|
+
if resource_url:
|
|
814
|
+
args["--resource-url"] = list(resource_url)
|
|
402
815
|
|
|
403
816
|
result = client.create_term(args)
|
|
404
817
|
|
|
@@ -420,42 +833,29 @@ def create(name, description, domain_id, status, acronym, owner_id, resource_nam
|
|
|
420
833
|
@click.option("--domain-id", required=True, help="Governance domain ID to list terms from")
|
|
421
834
|
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
422
835
|
def list_terms(domain_id, output_json):
|
|
423
|
-
"""List all
|
|
836
|
+
"""List all Unified Catalog terms in a governance domain."""
|
|
424
837
|
try:
|
|
425
838
|
client = UnifiedCatalogClient()
|
|
426
839
|
args = {"--governance-domain-id": [domain_id]}
|
|
427
840
|
result = client.get_terms(args)
|
|
428
841
|
|
|
429
842
|
if not result:
|
|
430
|
-
console.print("[yellow]No
|
|
843
|
+
console.print("[yellow]No terms found.[/yellow]")
|
|
431
844
|
return
|
|
432
845
|
|
|
433
|
-
#
|
|
434
|
-
# Extract all terms from all glossaries
|
|
846
|
+
# Unified Catalog API returns terms directly in value array
|
|
435
847
|
all_terms = []
|
|
436
848
|
|
|
437
|
-
if isinstance(result,
|
|
438
|
-
|
|
439
|
-
elif isinstance(result,
|
|
440
|
-
|
|
849
|
+
if isinstance(result, dict):
|
|
850
|
+
all_terms = result.get("value", [])
|
|
851
|
+
elif isinstance(result, (list, tuple)):
|
|
852
|
+
all_terms = result
|
|
441
853
|
else:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
# Extract terms from glossaries
|
|
445
|
-
for glossary in glossaries:
|
|
446
|
-
if isinstance(glossary, dict) and "terms" in glossary:
|
|
447
|
-
for term in glossary["terms"]:
|
|
448
|
-
all_terms.append(
|
|
449
|
-
{
|
|
450
|
-
"id": term.get("termGuid"),
|
|
451
|
-
"name": term.get("displayText"),
|
|
452
|
-
"glossary": glossary.get("name"),
|
|
453
|
-
"glossary_id": glossary.get("guid"),
|
|
454
|
-
}
|
|
455
|
-
)
|
|
854
|
+
console.print("[yellow]Unexpected response format.[/yellow]")
|
|
855
|
+
return
|
|
456
856
|
|
|
457
857
|
if not all_terms:
|
|
458
|
-
console.print("[yellow]No
|
|
858
|
+
console.print("[yellow]No terms found.[/yellow]")
|
|
459
859
|
return
|
|
460
860
|
|
|
461
861
|
# Output in JSON format if requested
|
|
@@ -463,21 +863,30 @@ def list_terms(domain_id, output_json):
|
|
|
463
863
|
_format_json_output(all_terms)
|
|
464
864
|
return
|
|
465
865
|
|
|
466
|
-
table = Table(title="
|
|
467
|
-
table.add_column("Term ID", style="cyan")
|
|
866
|
+
table = Table(title="Unified Catalog Terms")
|
|
867
|
+
table.add_column("Term ID", style="cyan", no_wrap=False)
|
|
468
868
|
table.add_column("Name", style="green")
|
|
469
|
-
table.add_column("
|
|
470
|
-
table.add_column("
|
|
869
|
+
table.add_column("Status", style="yellow")
|
|
870
|
+
table.add_column("Description", style="white")
|
|
471
871
|
|
|
472
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
|
+
|
|
473
881
|
table.add_row(
|
|
474
882
|
term.get("id", "N/A"),
|
|
475
883
|
term.get("name", "N/A"),
|
|
476
|
-
term.get("
|
|
477
|
-
|
|
884
|
+
term.get("status", "N/A"),
|
|
885
|
+
description.strip(),
|
|
478
886
|
)
|
|
479
887
|
|
|
480
888
|
console.print(table)
|
|
889
|
+
console.print(f"\n[dim]Found {len(all_terms)} term(s)[/dim]")
|
|
481
890
|
|
|
482
891
|
except Exception as e:
|
|
483
892
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
@@ -485,7 +894,8 @@ def list_terms(domain_id, output_json):
|
|
|
485
894
|
|
|
486
895
|
@term.command()
|
|
487
896
|
@click.option("--term-id", required=True, help="ID of the glossary term")
|
|
488
|
-
|
|
897
|
+
@click.option("--json", "output_json", is_flag=True, help="Output results in JSON format")
|
|
898
|
+
def show(term_id, output_json):
|
|
489
899
|
"""Show details of a glossary term."""
|
|
490
900
|
try:
|
|
491
901
|
client = UnifiedCatalogClient()
|
|
@@ -499,12 +909,70 @@ def show(term_id):
|
|
|
499
909
|
console.print(f"[red]ERROR:[/red] {result.get('error', 'Term not found')}")
|
|
500
910
|
return
|
|
501
911
|
|
|
502
|
-
|
|
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))
|
|
503
937
|
|
|
504
938
|
except Exception as e:
|
|
505
939
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
506
940
|
|
|
507
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]})
|
|
952
|
+
|
|
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}")
|
|
971
|
+
|
|
972
|
+
except Exception as e:
|
|
973
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
974
|
+
|
|
975
|
+
|
|
508
976
|
# ========================================
|
|
509
977
|
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
510
978
|
# ========================================
|
|
@@ -790,35 +1258,12 @@ def show(cde_id):
|
|
|
790
1258
|
|
|
791
1259
|
|
|
792
1260
|
# ========================================
|
|
793
|
-
# HEALTH MANAGEMENT
|
|
1261
|
+
# HEALTH MANAGEMENT - IMPLEMENTED! ✅
|
|
794
1262
|
# ========================================
|
|
795
1263
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
"""Manage health controls and actions (preview)."""
|
|
800
|
-
pass
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
@health.command(name="controls")
|
|
804
|
-
def list_controls():
|
|
805
|
-
"""List health controls (preview - not yet implemented)."""
|
|
806
|
-
console.print("[yellow]🚧 Health Controls are not yet implemented in the API[/yellow]")
|
|
807
|
-
console.print("This feature is coming soon to Microsoft Purview Unified Catalog")
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
@health.command(name="actions")
|
|
811
|
-
def list_actions():
|
|
812
|
-
"""List health actions (preview - not yet implemented)."""
|
|
813
|
-
console.print("[yellow]🚧 Health Actions are not yet implemented in the API[/yellow]")
|
|
814
|
-
console.print("This feature is coming soon to Microsoft Purview Unified Catalog")
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
@health.command(name="quality")
|
|
818
|
-
def data_quality():
|
|
819
|
-
"""Data quality management (not yet supported by API)."""
|
|
820
|
-
console.print("[yellow]🚧 Data Quality management is not yet supported by the API[/yellow]")
|
|
821
|
-
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")
|
|
822
1267
|
|
|
823
1268
|
|
|
824
1269
|
# ========================================
|