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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.2.4"
1
+ __version__ = "1.2.5"
2
2
 
3
3
  # Import main client modules
4
4
  from .client import *
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']