pvw-cli 1.0.8__py3-none-any.whl → 1.0.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pvw-cli might be problematic. Click here for more details.

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