pvw-cli 1.2.4__py3-none-any.whl → 1.2.5__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/types.py +333 -0
- purviewcli/cli/unified_catalog.py +1329 -9
- purviewcli/client/_entity.py +74 -17
- purviewcli/client/_types.py +31 -0
- purviewcli/client/_unified_catalog.py +646 -30
- purviewcli/client/endpoints.py +75 -0
- {pvw_cli-1.2.4.dist-info → pvw_cli-1.2.5.dist-info}/METADATA +19 -9
- {pvw_cli-1.2.4.dist-info → pvw_cli-1.2.5.dist-info}/RECORD +12 -12
- {pvw_cli-1.2.4.dist-info → pvw_cli-1.2.5.dist-info}/WHEEL +0 -0
- {pvw_cli-1.2.4.dist-info → pvw_cli-1.2.5.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.2.4.dist-info → pvw_cli-1.2.5.dist-info}/top_level.txt +0 -0
|
@@ -491,7 +491,7 @@ def delete(product_id, yes):
|
|
|
491
491
|
if result is None or (isinstance(result, dict) and not result.get("error")):
|
|
492
492
|
console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
|
|
493
493
|
elif isinstance(result, dict) and "error" in result:
|
|
494
|
-
|
|
494
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
495
495
|
else:
|
|
496
496
|
console.print(f"[green] SUCCESS:[/green] Deleted data product '{product_id}'")
|
|
497
497
|
if result:
|
|
@@ -501,11 +501,288 @@ def delete(product_id, yes):
|
|
|
501
501
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
502
502
|
|
|
503
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
|
+
|
|
504
783
|
# ========================================
|
|
505
784
|
# GLOSSARIES
|
|
506
785
|
# ========================================
|
|
507
|
-
|
|
508
|
-
|
|
509
786
|
@uc.group()
|
|
510
787
|
def glossary():
|
|
511
788
|
"""Manage glossaries (for finding glossary GUIDs)."""
|
|
@@ -1657,6 +1934,119 @@ def update_terms_from_json(json_file, dry_run):
|
|
|
1657
1934
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1658
1935
|
|
|
1659
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
|
+
|
|
1660
2050
|
|
|
1661
2051
|
# ========================================
|
|
1662
2052
|
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
@@ -1797,6 +2187,116 @@ def show(objective_id):
|
|
|
1797
2187
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1798
2188
|
|
|
1799
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
|
+
|
|
1800
2300
|
# ========================================
|
|
1801
2301
|
# CRITICAL DATA ELEMENTS (CDEs)
|
|
1802
2302
|
# ========================================
|
|
@@ -1942,6 +2442,272 @@ def show(cde_id):
|
|
|
1942
2442
|
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
1943
2443
|
|
|
1944
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
|
+
|
|
1945
2711
|
# ========================================
|
|
1946
2712
|
# HEALTH MANAGEMENT - IMPLEMENTED!
|
|
1947
2713
|
# ========================================
|
|
@@ -1952,21 +2718,575 @@ uc.add_command(health_commands, name="health")
|
|
|
1952
2718
|
|
|
1953
2719
|
|
|
1954
2720
|
# ========================================
|
|
1955
|
-
#
|
|
2721
|
+
# DATA POLICIES (NEW)
|
|
2722
|
+
# ========================================
|
|
2723
|
+
|
|
2724
|
+
|
|
2725
|
+
@uc.group()
|
|
2726
|
+
def policy():
|
|
2727
|
+
"""Manage data governance policies."""
|
|
2728
|
+
pass
|
|
2729
|
+
|
|
2730
|
+
|
|
2731
|
+
@policy.command(name="list")
|
|
2732
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
2733
|
+
def list_policies(output):
|
|
2734
|
+
"""List all data governance policies."""
|
|
2735
|
+
client = UnifiedCatalogClient()
|
|
2736
|
+
response = client.list_policies({})
|
|
2737
|
+
|
|
2738
|
+
if output == "json":
|
|
2739
|
+
console.print_json(json.dumps(response))
|
|
2740
|
+
else:
|
|
2741
|
+
# API returns 'values' (plural), not 'value'
|
|
2742
|
+
policies = response.get("values", response.get("value", []))
|
|
2743
|
+
|
|
2744
|
+
if policies:
|
|
2745
|
+
table = Table(title="[bold cyan]Data Governance Policies[/bold cyan]", show_header=True)
|
|
2746
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
2747
|
+
table.add_column("Name", style="green")
|
|
2748
|
+
table.add_column("Entity Type", style="yellow")
|
|
2749
|
+
table.add_column("Entity ID", style="magenta", no_wrap=True)
|
|
2750
|
+
table.add_column("Rules", style="white")
|
|
2751
|
+
|
|
2752
|
+
for item in policies:
|
|
2753
|
+
props = item.get("properties", {})
|
|
2754
|
+
entity = props.get("entity", {})
|
|
2755
|
+
entity_type = entity.get("type", "N/A")
|
|
2756
|
+
entity_ref = entity.get("referenceName", "N/A")
|
|
2757
|
+
|
|
2758
|
+
# Count rules
|
|
2759
|
+
decision_rules = len(props.get("decisionRules", []))
|
|
2760
|
+
attribute_rules = len(props.get("attributeRules", []))
|
|
2761
|
+
rules_summary = f"{decision_rules} decision, {attribute_rules} attribute"
|
|
2762
|
+
|
|
2763
|
+
table.add_row(
|
|
2764
|
+
item.get("id", "N/A")[:36], # Show only GUID
|
|
2765
|
+
item.get("name", "N/A"),
|
|
2766
|
+
entity_type.replace("Reference", ""), # Clean up type name
|
|
2767
|
+
entity_ref[:36], # Show only GUID
|
|
2768
|
+
rules_summary
|
|
2769
|
+
)
|
|
2770
|
+
|
|
2771
|
+
console.print(table)
|
|
2772
|
+
console.print(f"\n[dim]Total: {len(policies)} policy/policies[/dim]")
|
|
2773
|
+
else:
|
|
2774
|
+
console.print("[yellow]No policies found[/yellow]")
|
|
2775
|
+
|
|
2776
|
+
|
|
2777
|
+
@policy.command(name="get")
|
|
2778
|
+
@click.option("--policy-id", required=True, help="Policy ID")
|
|
2779
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="json", help="Output format")
|
|
2780
|
+
def get_policy(policy_id, output):
|
|
2781
|
+
"""Get a specific data governance policy by ID."""
|
|
2782
|
+
client = UnifiedCatalogClient()
|
|
2783
|
+
|
|
2784
|
+
# Get all policies and filter (since GET by ID returns 404)
|
|
2785
|
+
all_policies = client.list_policies({})
|
|
2786
|
+
policies = all_policies.get("values", all_policies.get("value", []))
|
|
2787
|
+
|
|
2788
|
+
# Find the requested policy
|
|
2789
|
+
policy = next((p for p in policies if p.get("id") == policy_id), None)
|
|
2790
|
+
|
|
2791
|
+
if not policy:
|
|
2792
|
+
console.print(f"[red]ERROR:[/red] Policy with ID {policy_id} not found")
|
|
2793
|
+
return
|
|
2794
|
+
|
|
2795
|
+
if output == "json":
|
|
2796
|
+
_format_json_output(policy)
|
|
2797
|
+
else:
|
|
2798
|
+
# Display policy in formatted view
|
|
2799
|
+
props = policy.get("properties", {})
|
|
2800
|
+
entity = props.get("entity", {})
|
|
2801
|
+
|
|
2802
|
+
console.print(f"\n[bold cyan]Policy Details[/bold cyan]")
|
|
2803
|
+
console.print(f"[bold]ID:[/bold] {policy.get('id')}")
|
|
2804
|
+
console.print(f"[bold]Name:[/bold] {policy.get('name')}")
|
|
2805
|
+
console.print(f"[bold]Version:[/bold] {policy.get('version', 0)}")
|
|
2806
|
+
|
|
2807
|
+
console.print(f"\n[bold cyan]Entity[/bold cyan]")
|
|
2808
|
+
console.print(f"[bold]Type:[/bold] {entity.get('type', 'N/A')}")
|
|
2809
|
+
console.print(f"[bold]Reference:[/bold] {entity.get('referenceName', 'N/A')}")
|
|
2810
|
+
console.print(f"[bold]Parent:[/bold] {props.get('parentEntityName', 'N/A')}")
|
|
2811
|
+
|
|
2812
|
+
# Decision Rules
|
|
2813
|
+
decision_rules = props.get("decisionRules", [])
|
|
2814
|
+
if decision_rules:
|
|
2815
|
+
console.print(f"\n[bold cyan]Decision Rules ({len(decision_rules)})[/bold cyan]")
|
|
2816
|
+
for i, rule in enumerate(decision_rules, 1):
|
|
2817
|
+
console.print(f"\n [bold]Rule {i}:[/bold] {rule.get('kind', 'N/A')}")
|
|
2818
|
+
console.print(f" [bold]Effect:[/bold] {rule.get('effect', 'N/A')}")
|
|
2819
|
+
if "dnfCondition" in rule:
|
|
2820
|
+
console.print(f" [bold]Conditions:[/bold] {len(rule['dnfCondition'])} clause(s)")
|
|
2821
|
+
|
|
2822
|
+
# Attribute Rules
|
|
2823
|
+
attribute_rules = props.get("attributeRules", [])
|
|
2824
|
+
if attribute_rules:
|
|
2825
|
+
console.print(f"\n[bold cyan]Attribute Rules ({len(attribute_rules)})[/bold cyan]")
|
|
2826
|
+
for i, rule in enumerate(attribute_rules, 1):
|
|
2827
|
+
console.print(f"\n [bold]Rule {i}:[/bold] {rule.get('name', rule.get('id', 'N/A'))}")
|
|
2828
|
+
if "dnfCondition" in rule:
|
|
2829
|
+
conditions = rule.get("dnfCondition", [])
|
|
2830
|
+
console.print(f" [bold]Conditions:[/bold] {len(conditions)} clause(s)")
|
|
2831
|
+
for j, clause in enumerate(conditions[:3], 1): # Show first 3
|
|
2832
|
+
if clause:
|
|
2833
|
+
attr = clause[0] if isinstance(clause, list) else clause
|
|
2834
|
+
console.print(f" {j}. {attr.get('attributeName', 'N/A')}")
|
|
2835
|
+
if len(conditions) > 3:
|
|
2836
|
+
console.print(f" ... and {len(conditions) - 3} more")
|
|
2837
|
+
|
|
2838
|
+
console.print()
|
|
2839
|
+
|
|
2840
|
+
|
|
2841
|
+
|
|
2842
|
+
@policy.command(name="create")
|
|
2843
|
+
@click.option("--name", required=True, help="Policy name")
|
|
2844
|
+
@click.option("--policy-type", required=True, help="Policy type (e.g., access, retention)")
|
|
2845
|
+
@click.option("--description", default="", help="Policy description")
|
|
2846
|
+
@click.option("--status", default="active", help="Policy status (active, draft)")
|
|
2847
|
+
def create_policy(name, policy_type, description, status):
|
|
2848
|
+
"""Create a new data governance policy."""
|
|
2849
|
+
client = UnifiedCatalogClient()
|
|
2850
|
+
args = {
|
|
2851
|
+
"--name": [name],
|
|
2852
|
+
"--policy-type": [policy_type],
|
|
2853
|
+
"--description": [description],
|
|
2854
|
+
"--status": [status]
|
|
2855
|
+
}
|
|
2856
|
+
response = client.create_policy(args)
|
|
2857
|
+
|
|
2858
|
+
console.print(f"[green]SUCCESS:[/green] Policy created")
|
|
2859
|
+
_format_json_output(response)
|
|
2860
|
+
|
|
2861
|
+
|
|
2862
|
+
@policy.command(name="update")
|
|
2863
|
+
@click.option("--policy-id", required=True, help="Policy ID")
|
|
2864
|
+
@click.option("--name", help="New policy name")
|
|
2865
|
+
@click.option("--description", help="New policy description")
|
|
2866
|
+
@click.option("--status", help="New policy status")
|
|
2867
|
+
def update_policy(policy_id, name, description, status):
|
|
2868
|
+
"""Update an existing data governance policy."""
|
|
2869
|
+
client = UnifiedCatalogClient()
|
|
2870
|
+
args = {"--policy-id": [policy_id]}
|
|
2871
|
+
|
|
2872
|
+
if name:
|
|
2873
|
+
args["--name"] = [name]
|
|
2874
|
+
if description:
|
|
2875
|
+
args["--description"] = [description]
|
|
2876
|
+
if status:
|
|
2877
|
+
args["--status"] = [status]
|
|
2878
|
+
|
|
2879
|
+
response = client.update_policy(args)
|
|
2880
|
+
|
|
2881
|
+
console.print(f"[green]SUCCESS:[/green] Policy updated")
|
|
2882
|
+
_format_json_output(response)
|
|
2883
|
+
|
|
2884
|
+
|
|
2885
|
+
@policy.command(name="delete")
|
|
2886
|
+
@click.option("--policy-id", required=True, help="Policy ID")
|
|
2887
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this policy?")
|
|
2888
|
+
def delete_policy(policy_id):
|
|
2889
|
+
"""Delete a data governance policy."""
|
|
2890
|
+
client = UnifiedCatalogClient()
|
|
2891
|
+
args = {"--policy-id": [policy_id]}
|
|
2892
|
+
response = client.delete_policy(args)
|
|
2893
|
+
|
|
2894
|
+
console.print(f"[green]SUCCESS:[/green] Policy '{policy_id}' deleted")
|
|
2895
|
+
|
|
2896
|
+
|
|
2897
|
+
# ========================================
|
|
2898
|
+
# CUSTOM METADATA (NEW)
|
|
2899
|
+
# ========================================
|
|
2900
|
+
|
|
2901
|
+
|
|
2902
|
+
@uc.group()
|
|
2903
|
+
def metadata():
|
|
2904
|
+
"""Manage custom metadata for assets."""
|
|
2905
|
+
pass
|
|
2906
|
+
|
|
2907
|
+
|
|
2908
|
+
@metadata.command(name="list")
|
|
2909
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
2910
|
+
@click.option("--fallback/--no-fallback", default=True, help="Fallback to Business Metadata if UC is empty")
|
|
2911
|
+
def list_custom_metadata(output, fallback):
|
|
2912
|
+
"""List all custom metadata definitions.
|
|
2913
|
+
|
|
2914
|
+
Uses Atlas API to get Business Metadata definitions.
|
|
2915
|
+
With fallback enabled, shows user-friendly table format.
|
|
2916
|
+
"""
|
|
2917
|
+
client = UnifiedCatalogClient()
|
|
2918
|
+
response = client.list_custom_metadata({})
|
|
2919
|
+
|
|
2920
|
+
# Check if UC API returned business metadata (Atlas returns businessMetadataDefs)
|
|
2921
|
+
has_uc_data = (response and "businessMetadataDefs" in response
|
|
2922
|
+
and response["businessMetadataDefs"])
|
|
2923
|
+
|
|
2924
|
+
if output == "json":
|
|
2925
|
+
if has_uc_data:
|
|
2926
|
+
console.print_json(json.dumps(response))
|
|
2927
|
+
elif fallback:
|
|
2928
|
+
# Fallback message (though Atlas API should always return something)
|
|
2929
|
+
console.print("[dim]No business metadata found.[/dim]\n")
|
|
2930
|
+
console.print_json(json.dumps({"businessMetadataDefs": []}))
|
|
2931
|
+
else:
|
|
2932
|
+
console.print_json(json.dumps(response))
|
|
2933
|
+
else:
|
|
2934
|
+
# Table output
|
|
2935
|
+
if has_uc_data:
|
|
2936
|
+
biz_metadata = response.get('businessMetadataDefs', [])
|
|
2937
|
+
|
|
2938
|
+
if biz_metadata:
|
|
2939
|
+
table = Table(title="[bold green]Business Metadata Attributes[/bold green]", show_header=True)
|
|
2940
|
+
table.add_column("Attribute Name", style="green", no_wrap=True)
|
|
2941
|
+
table.add_column("Group", style="cyan")
|
|
2942
|
+
table.add_column("Type", style="yellow")
|
|
2943
|
+
table.add_column("Scope", style="magenta", max_width=25)
|
|
2944
|
+
table.add_column("Description", style="white", max_width=30)
|
|
2945
|
+
|
|
2946
|
+
total_attrs = 0
|
|
2947
|
+
for group in biz_metadata:
|
|
2948
|
+
group_name = group.get('name', 'N/A')
|
|
2949
|
+
attributes = group.get('attributeDefs', [])
|
|
2950
|
+
|
|
2951
|
+
# Parse group-level scope
|
|
2952
|
+
group_scope = "N/A"
|
|
2953
|
+
options = group.get('options', {})
|
|
2954
|
+
if 'dataGovernanceOptions' in options:
|
|
2955
|
+
try:
|
|
2956
|
+
dg_opts_str = options.get('dataGovernanceOptions', '{}')
|
|
2957
|
+
dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
|
|
2958
|
+
applicable = dg_opts.get('applicableConstructs', [])
|
|
2959
|
+
if applicable:
|
|
2960
|
+
# Categorize scope
|
|
2961
|
+
has_business_concept = any('businessConcept' in c or 'domain' in c for c in applicable)
|
|
2962
|
+
has_dataset = any('dataset' in c.lower() for c in applicable)
|
|
2963
|
+
|
|
2964
|
+
if has_business_concept and has_dataset:
|
|
2965
|
+
group_scope = "Universal (Concept + Dataset)"
|
|
2966
|
+
elif has_business_concept:
|
|
2967
|
+
group_scope = "Business Concept"
|
|
2968
|
+
elif has_dataset:
|
|
2969
|
+
group_scope = "Data Asset"
|
|
2970
|
+
else:
|
|
2971
|
+
# Show first 2 constructs
|
|
2972
|
+
scope_parts = []
|
|
2973
|
+
for construct in applicable[:2]:
|
|
2974
|
+
if ':' in construct:
|
|
2975
|
+
scope_parts.append(construct.split(':')[0])
|
|
2976
|
+
else:
|
|
2977
|
+
scope_parts.append(construct)
|
|
2978
|
+
group_scope = ', '.join(scope_parts)
|
|
2979
|
+
except:
|
|
2980
|
+
pass
|
|
2981
|
+
|
|
2982
|
+
for attr in attributes:
|
|
2983
|
+
total_attrs += 1
|
|
2984
|
+
attr_name = attr.get('name', 'N/A')
|
|
2985
|
+
attr_type = attr.get('typeName', 'N/A')
|
|
2986
|
+
|
|
2987
|
+
# Simplify enum types
|
|
2988
|
+
if 'ATTRIBUTE_ENUM_' in attr_type:
|
|
2989
|
+
attr_type = 'Enum'
|
|
2990
|
+
|
|
2991
|
+
attr_desc = attr.get('description', '')
|
|
2992
|
+
|
|
2993
|
+
# Check if attribute has custom scope
|
|
2994
|
+
attr_scope = group_scope
|
|
2995
|
+
attr_opts = attr.get('options', {})
|
|
2996
|
+
|
|
2997
|
+
# Check dataGovernanceOptions first
|
|
2998
|
+
if 'dataGovernanceOptions' in attr_opts:
|
|
2999
|
+
try:
|
|
3000
|
+
attr_dg_str = attr_opts.get('dataGovernanceOptions', '{}')
|
|
3001
|
+
attr_dg = json.loads(attr_dg_str) if isinstance(attr_dg_str, str) else attr_dg_str
|
|
3002
|
+
inherit = attr_dg.get('inheritApplicableConstructsFromGroup', True)
|
|
3003
|
+
if not inherit:
|
|
3004
|
+
attr_applicable = attr_dg.get('applicableConstructs', [])
|
|
3005
|
+
if attr_applicable:
|
|
3006
|
+
# Categorize custom scope
|
|
3007
|
+
has_business_concept = any('businessConcept' in c or 'domain' in c for c in attr_applicable)
|
|
3008
|
+
has_dataset = any('dataset' in c.lower() for c in attr_applicable)
|
|
3009
|
+
|
|
3010
|
+
if has_business_concept and has_dataset:
|
|
3011
|
+
attr_scope = "Universal"
|
|
3012
|
+
elif has_business_concept:
|
|
3013
|
+
attr_scope = "Business Concept"
|
|
3014
|
+
elif has_dataset:
|
|
3015
|
+
attr_scope = "Data Asset"
|
|
3016
|
+
else:
|
|
3017
|
+
attr_scope = f"Custom ({len(attr_applicable)})"
|
|
3018
|
+
except:
|
|
3019
|
+
pass
|
|
3020
|
+
|
|
3021
|
+
# Fallback: Check applicableEntityTypes (older format)
|
|
3022
|
+
if attr_scope == "N/A" and 'applicableEntityTypes' in attr_opts:
|
|
3023
|
+
try:
|
|
3024
|
+
entity_types_str = attr_opts.get('applicableEntityTypes', '[]')
|
|
3025
|
+
# Parse if string, otherwise use as-is
|
|
3026
|
+
if isinstance(entity_types_str, str):
|
|
3027
|
+
entity_types = json.loads(entity_types_str)
|
|
3028
|
+
else:
|
|
3029
|
+
entity_types = entity_types_str
|
|
3030
|
+
|
|
3031
|
+
if entity_types and isinstance(entity_types, list):
|
|
3032
|
+
# Check if entity types are data assets (tables, etc.)
|
|
3033
|
+
if any('table' in et.lower() or 'database' in et.lower() or 'file' in et.lower()
|
|
3034
|
+
for et in entity_types):
|
|
3035
|
+
attr_scope = "Data Asset"
|
|
3036
|
+
else:
|
|
3037
|
+
attr_scope = f"Assets ({len(entity_types)} types)"
|
|
3038
|
+
except Exception as e:
|
|
3039
|
+
# Silently fail but could log for debugging
|
|
3040
|
+
pass
|
|
3041
|
+
|
|
3042
|
+
table.add_row(
|
|
3043
|
+
attr_name,
|
|
3044
|
+
group_name,
|
|
3045
|
+
attr_type,
|
|
3046
|
+
attr_scope,
|
|
3047
|
+
attr_desc[:30] + "..." if len(attr_desc) > 30 else attr_desc
|
|
3048
|
+
)
|
|
3049
|
+
|
|
3050
|
+
console.print(table)
|
|
3051
|
+
console.print(f"\n[cyan]Total:[/cyan] {total_attrs} business metadata attribute(s) in {len(biz_metadata)} group(s)")
|
|
3052
|
+
console.print("\n[dim]Legend:[/dim]")
|
|
3053
|
+
console.print(" [magenta]Business Concept[/magenta] = Applies to Terms, Domains, Business Rules")
|
|
3054
|
+
console.print(" [magenta]Data Asset[/magenta] = Applies to Tables, Files, Databases")
|
|
3055
|
+
console.print(" [magenta]Universal[/magenta] = Applies to both Concepts and Assets")
|
|
3056
|
+
else:
|
|
3057
|
+
console.print("[yellow]No business metadata found[/yellow]")
|
|
3058
|
+
else:
|
|
3059
|
+
console.print("[yellow]No business metadata found[/yellow]")
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
@metadata.command(name="get")
|
|
3063
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3064
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="json", help="Output format")
|
|
3065
|
+
def get_custom_metadata(asset_id, output):
|
|
3066
|
+
"""Get custom metadata (business metadata) for a specific asset."""
|
|
3067
|
+
client = UnifiedCatalogClient()
|
|
3068
|
+
args = {"--asset-id": [asset_id]}
|
|
3069
|
+
response = client.get_custom_metadata(args)
|
|
3070
|
+
|
|
3071
|
+
if output == "json":
|
|
3072
|
+
# Extract businessAttributes from entity response
|
|
3073
|
+
# Note: API returns "businessAttributes" not "businessMetadata"
|
|
3074
|
+
if response and "entity" in response:
|
|
3075
|
+
business_metadata = response["entity"].get("businessAttributes", {})
|
|
3076
|
+
_format_json_output(business_metadata)
|
|
3077
|
+
elif response and isinstance(response, dict):
|
|
3078
|
+
business_metadata = response.get("businessAttributes", {})
|
|
3079
|
+
_format_json_output(business_metadata)
|
|
3080
|
+
else:
|
|
3081
|
+
_format_json_output({})
|
|
3082
|
+
else:
|
|
3083
|
+
table = Table(title=f"[bold cyan]Business Metadata for Asset: {asset_id}[/bold cyan]")
|
|
3084
|
+
table.add_column("Group", style="cyan")
|
|
3085
|
+
table.add_column("Attribute", style="green")
|
|
3086
|
+
table.add_column("Value", style="white")
|
|
3087
|
+
|
|
3088
|
+
if response and "entity" in response:
|
|
3089
|
+
business_metadata = response["entity"].get("businessAttributes", {})
|
|
3090
|
+
if business_metadata:
|
|
3091
|
+
for group_name, attributes in business_metadata.items():
|
|
3092
|
+
if isinstance(attributes, dict):
|
|
3093
|
+
for attr_name, attr_value in attributes.items():
|
|
3094
|
+
table.add_row(group_name, attr_name, str(attr_value))
|
|
3095
|
+
elif response and isinstance(response, dict):
|
|
3096
|
+
business_metadata = response.get("businessAttributes", {})
|
|
3097
|
+
if business_metadata:
|
|
3098
|
+
for group_name, attributes in business_metadata.items():
|
|
3099
|
+
if isinstance(attributes, dict):
|
|
3100
|
+
for attr_name, attr_value in attributes.items():
|
|
3101
|
+
table.add_row(group_name, attr_name, str(attr_value))
|
|
3102
|
+
|
|
3103
|
+
console.print(table)
|
|
3104
|
+
|
|
3105
|
+
|
|
3106
|
+
@metadata.command(name="add")
|
|
3107
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3108
|
+
@click.option("--group", required=True, help="Business metadata group name (e.g., 'Governance', 'Privacy')")
|
|
3109
|
+
@click.option("--key", required=True, help="Attribute name")
|
|
3110
|
+
@click.option("--value", required=True, help="Attribute value")
|
|
3111
|
+
def add_custom_metadata(asset_id, group, key, value):
|
|
3112
|
+
"""Add custom metadata (business metadata) to an asset.
|
|
3113
|
+
|
|
3114
|
+
Example: pvw uc metadata add --asset-id <guid> --group Governance --key DataOwner --value "John Doe"
|
|
3115
|
+
"""
|
|
3116
|
+
client = UnifiedCatalogClient()
|
|
3117
|
+
args = {
|
|
3118
|
+
"--asset-id": [asset_id],
|
|
3119
|
+
"--group": [group],
|
|
3120
|
+
"--key": [key],
|
|
3121
|
+
"--value": [value]
|
|
3122
|
+
}
|
|
3123
|
+
response = client.add_custom_metadata(args)
|
|
3124
|
+
|
|
3125
|
+
console.print(f"[green]SUCCESS:[/green] Business metadata '{key}' added to group '{group}' on asset '{asset_id}'")
|
|
3126
|
+
if response:
|
|
3127
|
+
_format_json_output(response)
|
|
3128
|
+
|
|
3129
|
+
|
|
3130
|
+
@metadata.command(name="update")
|
|
3131
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3132
|
+
@click.option("--group", required=True, help="Business metadata group name")
|
|
3133
|
+
@click.option("--key", required=True, help="Attribute name to update")
|
|
3134
|
+
@click.option("--value", required=True, help="New attribute value")
|
|
3135
|
+
def update_custom_metadata(asset_id, group, key, value):
|
|
3136
|
+
"""Update custom metadata (business metadata) for an asset.
|
|
3137
|
+
|
|
3138
|
+
Example: pvw uc metadata update --asset-id <guid> --group Governance --key DataOwner --value "Jane Smith"
|
|
3139
|
+
"""
|
|
3140
|
+
client = UnifiedCatalogClient()
|
|
3141
|
+
args = {
|
|
3142
|
+
"--asset-id": [asset_id],
|
|
3143
|
+
"--group": [group],
|
|
3144
|
+
"--key": [key],
|
|
3145
|
+
"--value": [value]
|
|
3146
|
+
}
|
|
3147
|
+
response = client.update_custom_metadata(args)
|
|
3148
|
+
|
|
3149
|
+
console.print(f"[green]SUCCESS:[/green] Business metadata '{key}' updated in group '{group}' on asset '{asset_id}'")
|
|
3150
|
+
if response:
|
|
3151
|
+
_format_json_output(response)
|
|
3152
|
+
|
|
3153
|
+
|
|
3154
|
+
@metadata.command(name="delete")
|
|
3155
|
+
@click.option("--asset-id", required=True, help="Asset GUID")
|
|
3156
|
+
@click.option("--group", required=True, help="Business metadata group name to delete")
|
|
3157
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this business metadata group?")
|
|
3158
|
+
def delete_custom_metadata(asset_id, group):
|
|
3159
|
+
"""Delete custom metadata (business metadata) from an asset.
|
|
3160
|
+
|
|
3161
|
+
This removes the entire business metadata group from the asset.
|
|
3162
|
+
Example: pvw uc metadata delete --asset-id <guid> --group Governance
|
|
3163
|
+
"""
|
|
3164
|
+
client = UnifiedCatalogClient()
|
|
3165
|
+
args = {
|
|
3166
|
+
"--asset-id": [asset_id],
|
|
3167
|
+
"--group": [group]
|
|
3168
|
+
}
|
|
3169
|
+
response = client.delete_custom_metadata(args)
|
|
3170
|
+
|
|
3171
|
+
console.print(f"[green]SUCCESS:[/green] Business metadata group '{group}' deleted from asset '{asset_id}'")
|
|
3172
|
+
|
|
3173
|
+
|
|
3174
|
+
# ========================================
|
|
3175
|
+
# CUSTOM ATTRIBUTES (NEW)
|
|
1956
3176
|
# ========================================
|
|
1957
3177
|
|
|
1958
3178
|
|
|
1959
3179
|
@uc.group()
|
|
1960
3180
|
def attribute():
|
|
1961
|
-
"""Manage custom
|
|
3181
|
+
"""Manage custom attribute definitions."""
|
|
1962
3182
|
pass
|
|
1963
3183
|
|
|
1964
3184
|
|
|
1965
3185
|
@attribute.command(name="list")
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
3186
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="table", help="Output format")
|
|
3187
|
+
def list_custom_attributes(output):
|
|
3188
|
+
"""List all custom attribute definitions."""
|
|
3189
|
+
client = UnifiedCatalogClient()
|
|
3190
|
+
response = client.list_custom_attributes({})
|
|
3191
|
+
|
|
3192
|
+
if output == "json":
|
|
3193
|
+
console.print_json(json.dumps(response))
|
|
3194
|
+
else:
|
|
3195
|
+
if "value" in response and response["value"]:
|
|
3196
|
+
table = Table(title="[bold cyan]Custom Attribute Definitions[/bold cyan]", show_header=True)
|
|
3197
|
+
table.add_column("ID", style="cyan")
|
|
3198
|
+
table.add_column("Name", style="green")
|
|
3199
|
+
table.add_column("Data Type", style="yellow")
|
|
3200
|
+
table.add_column("Required", style="magenta")
|
|
3201
|
+
table.add_column("Description", style="white")
|
|
3202
|
+
|
|
3203
|
+
for item in response["value"]:
|
|
3204
|
+
table.add_row(
|
|
3205
|
+
item.get("id", "N/A"),
|
|
3206
|
+
item.get("name", "N/A"),
|
|
3207
|
+
item.get("dataType", "N/A"),
|
|
3208
|
+
"Yes" if item.get("required") else "No",
|
|
3209
|
+
item.get("description", "")[:50] + "..." if len(item.get("description", "")) > 50 else item.get("description", "")
|
|
3210
|
+
)
|
|
3211
|
+
console.print(table)
|
|
3212
|
+
else:
|
|
3213
|
+
console.print("[yellow]No custom attributes found[/yellow]")
|
|
3214
|
+
|
|
3215
|
+
|
|
3216
|
+
@attribute.command(name="get")
|
|
3217
|
+
@click.option("--attribute-id", required=True, help="Attribute ID")
|
|
3218
|
+
@click.option("--output", type=click.Choice(["table", "json"]), default="json", help="Output format")
|
|
3219
|
+
def get_custom_attribute(attribute_id, output):
|
|
3220
|
+
"""Get a specific custom attribute definition."""
|
|
3221
|
+
client = UnifiedCatalogClient()
|
|
3222
|
+
args = {"--attribute-id": [attribute_id]}
|
|
3223
|
+
response = client.get_custom_attribute(args)
|
|
3224
|
+
|
|
3225
|
+
if output == "json":
|
|
3226
|
+
_format_json_output(response)
|
|
3227
|
+
else:
|
|
3228
|
+
table = Table(title=f"[bold cyan]Attribute: {response.get('name', 'N/A')}[/bold cyan]")
|
|
3229
|
+
table.add_column("Property", style="cyan")
|
|
3230
|
+
table.add_column("Value", style="white")
|
|
3231
|
+
|
|
3232
|
+
for key, value in response.items():
|
|
3233
|
+
table.add_row(key, str(value))
|
|
3234
|
+
console.print(table)
|
|
3235
|
+
|
|
3236
|
+
|
|
3237
|
+
@attribute.command(name="create")
|
|
3238
|
+
@click.option("--name", required=True, help="Attribute name")
|
|
3239
|
+
@click.option("--data-type", required=True, help="Data type (string, number, boolean, date)")
|
|
3240
|
+
@click.option("--description", default="", help="Attribute description")
|
|
3241
|
+
@click.option("--required", is_flag=True, help="Is this attribute required?")
|
|
3242
|
+
def create_custom_attribute(name, data_type, description, required):
|
|
3243
|
+
"""Create a new custom attribute definition."""
|
|
3244
|
+
client = UnifiedCatalogClient()
|
|
3245
|
+
args = {
|
|
3246
|
+
"--name": [name],
|
|
3247
|
+
"--data-type": [data_type],
|
|
3248
|
+
"--description": [description],
|
|
3249
|
+
"--required": ["true" if required else "false"]
|
|
3250
|
+
}
|
|
3251
|
+
response = client.create_custom_attribute(args)
|
|
3252
|
+
|
|
3253
|
+
console.print(f"[green]SUCCESS:[/green] Custom attribute created")
|
|
3254
|
+
_format_json_output(response)
|
|
3255
|
+
|
|
3256
|
+
|
|
3257
|
+
@attribute.command(name="update")
|
|
3258
|
+
@click.option("--attribute-id", required=True, help="Attribute ID")
|
|
3259
|
+
@click.option("--name", help="New attribute name")
|
|
3260
|
+
@click.option("--description", help="New attribute description")
|
|
3261
|
+
@click.option("--required", type=bool, help="Is this attribute required? (true/false)")
|
|
3262
|
+
def update_custom_attribute(attribute_id, name, description, required):
|
|
3263
|
+
"""Update an existing custom attribute definition."""
|
|
3264
|
+
client = UnifiedCatalogClient()
|
|
3265
|
+
args = {"--attribute-id": [attribute_id]}
|
|
3266
|
+
|
|
3267
|
+
if name:
|
|
3268
|
+
args["--name"] = [name]
|
|
3269
|
+
if description:
|
|
3270
|
+
args["--description"] = [description]
|
|
3271
|
+
if required is not None:
|
|
3272
|
+
args["--required"] = ["true" if required else "false"]
|
|
3273
|
+
|
|
3274
|
+
response = client.update_custom_attribute(args)
|
|
3275
|
+
|
|
3276
|
+
console.print(f"[green]SUCCESS:[/green] Custom attribute updated")
|
|
3277
|
+
_format_json_output(response)
|
|
3278
|
+
|
|
3279
|
+
|
|
3280
|
+
@attribute.command(name="delete")
|
|
3281
|
+
@click.option("--attribute-id", required=True, help="Attribute ID")
|
|
3282
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this attribute?")
|
|
3283
|
+
def delete_custom_attribute(attribute_id):
|
|
3284
|
+
"""Delete a custom attribute definition."""
|
|
3285
|
+
client = UnifiedCatalogClient()
|
|
3286
|
+
args = {"--attribute-id": [attribute_id]}
|
|
3287
|
+
response = client.delete_custom_attribute(args)
|
|
3288
|
+
|
|
3289
|
+
console.print(f"[green]SUCCESS:[/green] Custom attribute '{attribute_id}' deleted")
|
|
1970
3290
|
|
|
1971
3291
|
|
|
1972
3292
|
# ========================================
|