pvw-cli 1.2.3__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/lineage.py +4 -1
- purviewcli/cli/types.py +333 -0
- purviewcli/cli/unified_catalog.py +1329 -9
- purviewcli/client/_entity.py +74 -17
- purviewcli/client/_lineage.py +206 -44
- purviewcli/client/_types.py +31 -0
- purviewcli/client/_unified_catalog.py +646 -30
- purviewcli/client/endpoints.py +75 -0
- {pvw_cli-1.2.3.dist-info → pvw_cli-1.2.5.dist-info}/METADATA +232 -75
- {pvw_cli-1.2.3.dist-info → pvw_cli-1.2.5.dist-info}/RECORD +14 -14
- {pvw_cli-1.2.3.dist-info → pvw_cli-1.2.5.dist-info}/WHEEL +0 -0
- {pvw_cli-1.2.3.dist-info → pvw_cli-1.2.5.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.2.3.dist-info → pvw_cli-1.2.5.dist-info}/top_level.txt +0 -0
purviewcli/__init__.py
CHANGED
purviewcli/cli/lineage.py
CHANGED
|
@@ -54,7 +54,7 @@ def lineage(ctx):
|
|
|
54
54
|
def import_cmd(ctx, csv_file):
|
|
55
55
|
"""Import lineage relationships from CSV file (calls client lineageCSVProcess)."""
|
|
56
56
|
try:
|
|
57
|
-
if ctx.obj.get("mock"):
|
|
57
|
+
if ctx.obj and ctx.obj.get("mock"):
|
|
58
58
|
console.print("[yellow][MOCK] lineage import command[/yellow]")
|
|
59
59
|
console.print(f"[dim]File: {csv_file}[/dim]")
|
|
60
60
|
console.print("[green]MOCK lineage import completed successfully[/green]")
|
|
@@ -68,6 +68,9 @@ def import_cmd(ctx, csv_file):
|
|
|
68
68
|
console.print(json.dumps(result, indent=2))
|
|
69
69
|
except Exception as e:
|
|
70
70
|
console.print(f"[red]ERROR: Error executing lineage import: {str(e)}[/red]")
|
|
71
|
+
import traceback
|
|
72
|
+
if ctx.obj and ctx.obj.get("debug"):
|
|
73
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
71
74
|
|
|
72
75
|
|
|
73
76
|
@lineage.command()
|
purviewcli/cli/types.py
CHANGED
|
@@ -495,4 +495,337 @@ def update_enum_def(payload_file, dry_run, validate):
|
|
|
495
495
|
except Exception as e:
|
|
496
496
|
click.echo(f"Error: {e}")
|
|
497
497
|
|
|
498
|
+
|
|
499
|
+
@types.command(name="list-business-attributes")
|
|
500
|
+
@click.option('--output', type=click.Choice(['table', 'json']), default='table', help='Output format')
|
|
501
|
+
@click.option('--show-empty-groups/--hide-empty-groups', default=True, help='Show groups with no attributes')
|
|
502
|
+
def list_business_attributes(output, show_empty_groups):
|
|
503
|
+
"""List all business metadata attributes (Custom metadata in Purview UI)
|
|
504
|
+
|
|
505
|
+
This command displays individual attributes organized by their parent groups,
|
|
506
|
+
matching what you see in the Purview "Custom metadata (preview)" interface
|
|
507
|
+
under "Business concept attributes" tab.
|
|
508
|
+
|
|
509
|
+
Examples:
|
|
510
|
+
pvw types list-business-attributes
|
|
511
|
+
pvw types list-business-attributes --output json
|
|
512
|
+
pvw types list-business-attributes --hide-empty-groups
|
|
513
|
+
"""
|
|
514
|
+
from rich.console import Console
|
|
515
|
+
from rich.table import Table
|
|
516
|
+
|
|
517
|
+
console = Console()
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
client = Types()
|
|
521
|
+
result = client.typesRead({})
|
|
522
|
+
|
|
523
|
+
if not result:
|
|
524
|
+
console.print("[red]ERROR:[/red] Failed to retrieve type definitions")
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
biz = result.get('businessMetadataDefs', [])
|
|
528
|
+
|
|
529
|
+
if not biz:
|
|
530
|
+
console.print("[yellow]No business metadata found[/yellow]")
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
if output == 'json':
|
|
534
|
+
# JSON output: list of attributes with their group info
|
|
535
|
+
attributes_list = []
|
|
536
|
+
for group in biz:
|
|
537
|
+
group_name = group.get('name', 'N/A')
|
|
538
|
+
group_guid = group.get('guid', 'N/A')
|
|
539
|
+
|
|
540
|
+
attributes = group.get('attributeDefs', [])
|
|
541
|
+
if not attributes and not show_empty_groups:
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
for attr in attributes:
|
|
545
|
+
attributes_list.append({
|
|
546
|
+
'attributeName': attr.get('name'),
|
|
547
|
+
'group': group_name,
|
|
548
|
+
'groupGuid': group_guid,
|
|
549
|
+
'type': attr.get('typeName'),
|
|
550
|
+
'description': attr.get('description', ''),
|
|
551
|
+
'isOptional': attr.get('isOptional', True),
|
|
552
|
+
'isIndexable': attr.get('isIndexable', False)
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
click.echo(json.dumps(attributes_list, indent=2))
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
# Table output
|
|
559
|
+
table = Table(title="[bold cyan]Business Concept Attributes (Custom Metadata)[/bold cyan]")
|
|
560
|
+
table.add_column("Attribute Name", style="green", no_wrap=True)
|
|
561
|
+
table.add_column("Group", style="cyan")
|
|
562
|
+
table.add_column("Type", style="yellow")
|
|
563
|
+
table.add_column("Description", style="white", max_width=40)
|
|
564
|
+
table.add_column("Scope", style="magenta")
|
|
565
|
+
|
|
566
|
+
total_attributes = 0
|
|
567
|
+
|
|
568
|
+
for group in biz:
|
|
569
|
+
group_name = group.get('name', 'N/A')
|
|
570
|
+
|
|
571
|
+
# Parse data governance options for scope
|
|
572
|
+
scope_info = "N/A"
|
|
573
|
+
options = group.get('options', {})
|
|
574
|
+
if 'dataGovernanceOptions' in options:
|
|
575
|
+
try:
|
|
576
|
+
dg_opts_str = options.get('dataGovernanceOptions', '{}')
|
|
577
|
+
dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
|
|
578
|
+
applicable = dg_opts.get('applicableConstructs', [])
|
|
579
|
+
if applicable:
|
|
580
|
+
# Extract construct types (domain, businessConcept, etc.)
|
|
581
|
+
scope_parts = []
|
|
582
|
+
for construct in applicable[:2]: # Show first 2
|
|
583
|
+
if ':' in construct:
|
|
584
|
+
scope_parts.append(construct.split(':')[0])
|
|
585
|
+
else:
|
|
586
|
+
scope_parts.append(construct)
|
|
587
|
+
scope_info = ', '.join(scope_parts)
|
|
588
|
+
if len(applicable) > 2:
|
|
589
|
+
scope_info += f", +{len(applicable)-2} more"
|
|
590
|
+
except:
|
|
591
|
+
pass
|
|
592
|
+
|
|
593
|
+
# List all attributes in this group
|
|
594
|
+
attributes = group.get('attributeDefs', [])
|
|
595
|
+
|
|
596
|
+
if attributes:
|
|
597
|
+
for attr in attributes:
|
|
598
|
+
total_attributes += 1
|
|
599
|
+
attr_name = attr.get('name', 'N/A')
|
|
600
|
+
attr_type = attr.get('typeName', 'N/A')
|
|
601
|
+
|
|
602
|
+
# Simplify enum types for display
|
|
603
|
+
if 'ATTRIBUTE_ENUM_' in attr_type:
|
|
604
|
+
attr_type = 'Enum (Single choice)'
|
|
605
|
+
|
|
606
|
+
attr_desc = attr.get('description', '')
|
|
607
|
+
|
|
608
|
+
# Get scope from attribute if it overrides group
|
|
609
|
+
attr_scope = scope_info
|
|
610
|
+
attr_opts = attr.get('options', {})
|
|
611
|
+
if 'dataGovernanceOptions' in attr_opts:
|
|
612
|
+
try:
|
|
613
|
+
attr_dg_str = attr_opts.get('dataGovernanceOptions', '{}')
|
|
614
|
+
attr_dg = json.loads(attr_dg_str) if isinstance(attr_dg_str, str) else attr_dg_str
|
|
615
|
+
inherit = attr_dg.get('inheritApplicableConstructsFromGroup', True)
|
|
616
|
+
if not inherit:
|
|
617
|
+
attr_applicable = attr_dg.get('applicableConstructs', [])
|
|
618
|
+
if attr_applicable:
|
|
619
|
+
attr_scope = f"{len(attr_applicable)} custom scope(s)"
|
|
620
|
+
except:
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
table.add_row(
|
|
624
|
+
attr_name,
|
|
625
|
+
group_name,
|
|
626
|
+
attr_type,
|
|
627
|
+
attr_desc[:40] + "..." if len(attr_desc) > 40 else attr_desc,
|
|
628
|
+
attr_scope
|
|
629
|
+
)
|
|
630
|
+
elif show_empty_groups:
|
|
631
|
+
# Show group with no attributes
|
|
632
|
+
table.add_row(
|
|
633
|
+
f"[dim](no attributes)[/dim]",
|
|
634
|
+
group_name,
|
|
635
|
+
"-",
|
|
636
|
+
f"[dim]Empty group[/dim]",
|
|
637
|
+
scope_info
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
console.print(table)
|
|
641
|
+
console.print(f"\n[cyan]Total:[/cyan] {total_attributes} attribute(s) in {len(biz)} group(s)")
|
|
642
|
+
|
|
643
|
+
if total_attributes > 0:
|
|
644
|
+
console.print("\n[dim]Tip: Use 'pvw types read-business-metadata-def --name <GroupName>' for details[/dim]")
|
|
645
|
+
console.print("[dim]Tip: Use 'pvw types list-business-metadata-groups' to see group-level summary[/dim]")
|
|
646
|
+
|
|
647
|
+
except Exception as e:
|
|
648
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@types.command(name="list-business-metadata-groups")
|
|
652
|
+
@click.option('--output', type=click.Choice(['table', 'json']), default='table', help='Output format')
|
|
653
|
+
def list_business_metadata_groups(output):
|
|
654
|
+
"""List business metadata groups with their scope (Business Concept vs Data Asset).
|
|
655
|
+
|
|
656
|
+
Shows a summary view of metadata groups to distinguish which apply to:
|
|
657
|
+
- Business Concepts (Terms, Domains, Business Rules)
|
|
658
|
+
- Data Assets (Tables, Files, Databases)
|
|
659
|
+
- Universal (Both)
|
|
660
|
+
|
|
661
|
+
Examples:
|
|
662
|
+
pvw types list-business-metadata-groups
|
|
663
|
+
pvw types list-business-metadata-groups --output json
|
|
664
|
+
"""
|
|
665
|
+
from rich.console import Console
|
|
666
|
+
from rich.table import Table
|
|
667
|
+
import json
|
|
668
|
+
|
|
669
|
+
console = Console()
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
client = Types()
|
|
673
|
+
result = client.typesRead({})
|
|
674
|
+
|
|
675
|
+
if not result:
|
|
676
|
+
console.print("[red]ERROR:[/red] Failed to retrieve type definitions")
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
biz = result.get('businessMetadataDefs', [])
|
|
680
|
+
|
|
681
|
+
if not biz:
|
|
682
|
+
console.print("[yellow]No business metadata groups found[/yellow]")
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
if output == 'json':
|
|
686
|
+
# JSON output
|
|
687
|
+
groups_list = []
|
|
688
|
+
for group in biz:
|
|
689
|
+
group_name = group.get('name', 'N/A')
|
|
690
|
+
group_guid = group.get('guid', 'N/A')
|
|
691
|
+
attr_count = len(group.get('attributeDefs', []))
|
|
692
|
+
|
|
693
|
+
# Determine scope
|
|
694
|
+
scope = "N/A"
|
|
695
|
+
scope_type = "unknown"
|
|
696
|
+
options = group.get('options', {})
|
|
697
|
+
|
|
698
|
+
if 'dataGovernanceOptions' in options:
|
|
699
|
+
try:
|
|
700
|
+
dg_opts_str = options.get('dataGovernanceOptions', '{}')
|
|
701
|
+
dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
|
|
702
|
+
applicable = dg_opts.get('applicableConstructs', [])
|
|
703
|
+
|
|
704
|
+
if applicable:
|
|
705
|
+
has_business_concept = any('businessConcept' in c or 'domain' in c for c in applicable)
|
|
706
|
+
has_dataset = any('dataset' in c.lower() for c in applicable)
|
|
707
|
+
|
|
708
|
+
if has_business_concept and has_dataset:
|
|
709
|
+
scope = "Universal (Concept + Dataset)"
|
|
710
|
+
scope_type = "universal"
|
|
711
|
+
elif has_business_concept:
|
|
712
|
+
scope = "Business Concept"
|
|
713
|
+
scope_type = "business_concept"
|
|
714
|
+
elif has_dataset:
|
|
715
|
+
scope = "Data Asset"
|
|
716
|
+
scope_type = "data_asset"
|
|
717
|
+
else:
|
|
718
|
+
scope = ', '.join([c.split(':')[0] if ':' in c else c for c in applicable[:3]])
|
|
719
|
+
scope_type = "custom"
|
|
720
|
+
except:
|
|
721
|
+
pass
|
|
722
|
+
|
|
723
|
+
# Check for legacy applicableEntityTypes in attributes
|
|
724
|
+
if scope == "N/A":
|
|
725
|
+
for attr in group.get('attributeDefs', []):
|
|
726
|
+
attr_opts = attr.get('options', {})
|
|
727
|
+
if 'applicableEntityTypes' in attr_opts:
|
|
728
|
+
try:
|
|
729
|
+
entity_types_str = attr_opts.get('applicableEntityTypes', '[]')
|
|
730
|
+
entity_types = json.loads(entity_types_str) if isinstance(entity_types_str, str) else entity_types_str
|
|
731
|
+
if entity_types and isinstance(entity_types, list):
|
|
732
|
+
if any('table' in et.lower() or 'database' in et.lower() for et in entity_types):
|
|
733
|
+
scope = "Data Asset (Legacy)"
|
|
734
|
+
scope_type = "data_asset_legacy"
|
|
735
|
+
break
|
|
736
|
+
except:
|
|
737
|
+
pass
|
|
738
|
+
|
|
739
|
+
groups_list.append({
|
|
740
|
+
'groupName': group_name,
|
|
741
|
+
'groupGuid': group_guid,
|
|
742
|
+
'scope': scope,
|
|
743
|
+
'scopeType': scope_type,
|
|
744
|
+
'attributeCount': attr_count,
|
|
745
|
+
'description': group.get('description', '')
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
click.echo(json.dumps(groups_list, indent=2))
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Table output
|
|
752
|
+
table = Table(title="[bold cyan]Business Metadata Groups[/bold cyan]", show_header=True)
|
|
753
|
+
table.add_column("Group Name", style="cyan", no_wrap=True)
|
|
754
|
+
table.add_column("Scope", style="magenta", max_width=30)
|
|
755
|
+
table.add_column("Attributes", style="yellow", justify="center")
|
|
756
|
+
table.add_column("Description", style="white", max_width=40)
|
|
757
|
+
|
|
758
|
+
for group in biz:
|
|
759
|
+
group_name = group.get('name', 'N/A')
|
|
760
|
+
attr_count = len(group.get('attributeDefs', []))
|
|
761
|
+
group_desc = group.get('description', '')
|
|
762
|
+
|
|
763
|
+
# Determine scope
|
|
764
|
+
scope = "N/A"
|
|
765
|
+
scope_style = "white"
|
|
766
|
+
options = group.get('options', {})
|
|
767
|
+
|
|
768
|
+
if 'dataGovernanceOptions' in options:
|
|
769
|
+
try:
|
|
770
|
+
dg_opts_str = options.get('dataGovernanceOptions', '{}')
|
|
771
|
+
dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
|
|
772
|
+
applicable = dg_opts.get('applicableConstructs', [])
|
|
773
|
+
|
|
774
|
+
if applicable:
|
|
775
|
+
has_business_concept = any('businessConcept' in c or 'domain' in c for c in applicable)
|
|
776
|
+
has_dataset = any('dataset' in c.lower() for c in applicable)
|
|
777
|
+
|
|
778
|
+
if has_business_concept and has_dataset:
|
|
779
|
+
scope = "Universal"
|
|
780
|
+
scope_style = "magenta bold"
|
|
781
|
+
elif has_business_concept:
|
|
782
|
+
scope = "Business Concept"
|
|
783
|
+
scope_style = "green"
|
|
784
|
+
elif has_dataset:
|
|
785
|
+
scope = "Data Asset"
|
|
786
|
+
scope_style = "blue"
|
|
787
|
+
else:
|
|
788
|
+
scope = ', '.join([c.split(':')[0] if ':' in c else c for c in applicable[:2]])
|
|
789
|
+
scope_style = "yellow"
|
|
790
|
+
except:
|
|
791
|
+
pass
|
|
792
|
+
|
|
793
|
+
# Check for legacy applicableEntityTypes
|
|
794
|
+
if scope == "N/A":
|
|
795
|
+
for attr in group.get('attributeDefs', []):
|
|
796
|
+
attr_opts = attr.get('options', {})
|
|
797
|
+
if 'applicableEntityTypes' in attr_opts:
|
|
798
|
+
try:
|
|
799
|
+
entity_types_str = attr_opts.get('applicableEntityTypes', '[]')
|
|
800
|
+
entity_types = json.loads(entity_types_str) if isinstance(entity_types_str, str) else entity_types_str
|
|
801
|
+
if entity_types and isinstance(entity_types, list):
|
|
802
|
+
if any('table' in et.lower() or 'database' in et.lower() for et in entity_types):
|
|
803
|
+
scope = "Data Asset (Legacy)"
|
|
804
|
+
scope_style = "blue dim"
|
|
805
|
+
break
|
|
806
|
+
except:
|
|
807
|
+
pass
|
|
808
|
+
|
|
809
|
+
table.add_row(
|
|
810
|
+
group_name,
|
|
811
|
+
f"[{scope_style}]{scope}[/{scope_style}]",
|
|
812
|
+
str(attr_count),
|
|
813
|
+
group_desc[:40] + "..." if len(group_desc) > 40 else group_desc
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
console.print(table)
|
|
817
|
+
console.print(f"\n[cyan]Total:[/cyan] {len(biz)} group(s)")
|
|
818
|
+
|
|
819
|
+
console.print("\n[bold]Legend:[/bold]")
|
|
820
|
+
console.print(" [green]Business Concept[/green] = Applies to Glossary Terms, Domains, Business Rules")
|
|
821
|
+
console.print(" [blue]Data Asset[/blue] = Applies to Tables, Files, Databases, etc.")
|
|
822
|
+
console.print(" [magenta bold]Universal[/magenta bold] = Applies to both Concepts and Assets")
|
|
823
|
+
|
|
824
|
+
console.print("\n[dim]Tip: Use 'pvw types list-business-attributes' to see individual attributes[/dim]")
|
|
825
|
+
console.print("[dim]Tip: Use 'pvw types read-business-metadata-def --name <GroupName>' for full details[/dim]")
|
|
826
|
+
|
|
827
|
+
except Exception as e:
|
|
828
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
829
|
+
|
|
830
|
+
|
|
498
831
|
__all__ = ['types']
|