pvw-cli 1.0.11__py3-none-any.whl → 1.0.12__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.0.11"
1
+ __version__ = "1.0.12"
2
2
 
3
3
  # Import main client modules
4
4
  from .client import *
@@ -51,8 +51,7 @@ def domain():
51
51
  "--type",
52
52
  required=False,
53
53
  default="FunctionalUnit",
54
- type=click.Choice(["FunctionalUnit", "BusinessUnit", "Department"]),
55
- help="Type of governance domain",
54
+ help="Type of governance domain (default: FunctionalUnit). Note: UC API currently only accepts 'FunctionalUnit'.",
56
55
  )
57
56
  @click.option(
58
57
  "--owner-id",
@@ -67,19 +66,46 @@ def domain():
67
66
  type=click.Choice(["Draft", "Published", "Archived"]),
68
67
  help="Status of the governance domain",
69
68
  )
70
- def create(name, description, type, owner_id, status):
69
+ @click.option(
70
+ "--parent-id",
71
+ required=False,
72
+ help="Parent governance domain ID (create as subdomain under this domain)",
73
+ )
74
+ @click.option(
75
+ "--payload-file",
76
+ required=False,
77
+ type=click.Path(exists=True),
78
+ help="Optional JSON payload file to use for creating the domain (overrides flags if provided)",
79
+ )
80
+ def create(name, description, type, owner_id, status, parent_id, payload_file):
71
81
  """Create a new governance domain."""
72
82
  try:
73
83
  client = UnifiedCatalogClient()
74
84
 
75
85
  # Build args dictionary in Purview CLI format
76
- args = {
77
- "--name": [name],
78
- "--description": [description],
79
- "--type": [type],
80
- "--status": [status],
81
- }
82
-
86
+ # If payload-file is provided we will let the client read the file directly
87
+ # otherwise build args from individual flags.
88
+ args = {}
89
+ # Note: click will pass None for owner_id if not provided, but multiple=True returns ()
90
+ # We'll only include values if payload-file not used.
91
+ if locals().get('payload_file'):
92
+ args = {"--payloadFile": locals().get('payload_file')}
93
+ else:
94
+ args = {
95
+ "--name": [name],
96
+ "--description": [description],
97
+ "--type": [type],
98
+ "--status": [status],
99
+ }
100
+ if owner_id:
101
+ args["--owner-id"] = list(owner_id)
102
+ # include parent id if provided
103
+ parent_id = locals().get('parent_id')
104
+ if parent_id:
105
+ # use a consistent arg name for client lookup
106
+ args["--parent-domain-id"] = [parent_id]
107
+
108
+ # Call the client to create the governance domain
83
109
  result = client.create_governance_domain(args)
84
110
 
85
111
  if not result:
@@ -973,6 +999,84 @@ def delete(term_id, force):
973
999
  console.print(f"[red]ERROR:[/red] {str(e)}")
974
1000
 
975
1001
 
1002
+ @term.command()
1003
+ @click.option("--term-id", required=True, help="ID of the glossary term to update")
1004
+ @click.option("--name", required=False, help="Name of the glossary term")
1005
+ @click.option("--description", required=False, help="Rich text description of the term")
1006
+ @click.option("--domain-id", required=False, help="Governance domain ID")
1007
+ @click.option(
1008
+ "--status",
1009
+ required=False,
1010
+ type=click.Choice(["Draft", "Published", "Archived"]),
1011
+ help="Status of the term",
1012
+ )
1013
+ @click.option(
1014
+ "--acronym",
1015
+ required=False,
1016
+ help="Acronyms for the term (can be specified multiple times, replaces existing)",
1017
+ multiple=True,
1018
+ )
1019
+ @click.option(
1020
+ "--owner-id",
1021
+ required=False,
1022
+ help="Owner Entra ID (can be specified multiple times, replaces existing)",
1023
+ multiple=True,
1024
+ )
1025
+ @click.option("--resource-name", required=False, help="Resource name for additional reading (can be specified multiple times, replaces existing)", multiple=True)
1026
+ @click.option("--resource-url", required=False, help="Resource URL for additional reading (can be specified multiple times, replaces existing)", multiple=True)
1027
+ @click.option("--add-acronym", required=False, help="Add acronym to existing ones (can be specified multiple times)", multiple=True)
1028
+ @click.option("--add-owner-id", required=False, help="Add owner to existing ones (can be specified multiple times)", multiple=True)
1029
+ def update(term_id, name, description, domain_id, status, acronym, owner_id, resource_name, resource_url, add_acronym, add_owner_id):
1030
+ """Update an existing Unified Catalog term."""
1031
+ try:
1032
+ client = UnifiedCatalogClient()
1033
+
1034
+ # Build args dictionary - only include provided values
1035
+ args = {"--term-id": [term_id]}
1036
+
1037
+ if name:
1038
+ args["--name"] = [name]
1039
+ if description is not None: # Allow empty string
1040
+ args["--description"] = [description]
1041
+ if domain_id:
1042
+ args["--governance-domain-id"] = [domain_id]
1043
+ if status:
1044
+ args["--status"] = [status]
1045
+
1046
+ # Handle acronyms - either replace or add
1047
+ if acronym:
1048
+ args["--acronym"] = list(acronym)
1049
+ elif add_acronym:
1050
+ args["--add-acronym"] = list(add_acronym)
1051
+
1052
+ # Handle owners - either replace or add
1053
+ if owner_id:
1054
+ args["--owner-id"] = list(owner_id)
1055
+ elif add_owner_id:
1056
+ args["--add-owner-id"] = list(add_owner_id)
1057
+
1058
+ # Handle resources
1059
+ if resource_name:
1060
+ args["--resource-name"] = list(resource_name)
1061
+ if resource_url:
1062
+ args["--resource-url"] = list(resource_url)
1063
+
1064
+ result = client.update_term(args)
1065
+
1066
+ if not result:
1067
+ console.print("[red]ERROR:[/red] No response received")
1068
+ return
1069
+ if isinstance(result, dict) and "error" in result:
1070
+ console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
1071
+ return
1072
+
1073
+ console.print(f"[green]✅ SUCCESS:[/green] Updated glossary term '{term_id}'")
1074
+ console.print(json.dumps(result, indent=2))
1075
+
1076
+ except Exception as e:
1077
+ console.print(f"[red]ERROR:[/red] {str(e)}")
1078
+
1079
+
976
1080
  # ========================================
977
1081
  # OBJECTIVES AND KEY RESULTS (OKRs)
978
1082
  # ========================================
@@ -39,12 +39,22 @@ class UnifiedCatalogClient(Endpoint):
39
39
  """Create a new governance domain."""
40
40
  self.method = "POST"
41
41
  self.endpoint = "/datagovernance/catalog/businessdomains"
42
- self.payload = get_json(args, "--payloadFile") or {
43
- "name": args.get("--name", [""])[0],
44
- "description": args.get("--description", [""])[0],
45
- "type": args.get("--type", ["FunctionalUnit"])[0],
46
- "status": args.get("--status", ["Draft"])[0],
47
- }
42
+ # Allow payload file to fully control creation; otherwise build payload from flags
43
+ payload = get_json(args, "--payloadFile")
44
+ if not payload:
45
+ payload = {
46
+ "name": args.get("--name", [""])[0],
47
+ "description": args.get("--description", [""])[0],
48
+ "type": args.get("--type", ["FunctionalUnit"])[0],
49
+ "status": args.get("--status", ["Draft"])[0],
50
+ }
51
+ # Support parent domain ID passed via CLI as --parent-domain-id
52
+ parent_id = args.get("--parent-domain-id", [""])[0]
53
+ if parent_id:
54
+ payload["parentId"] = parent_id
55
+
56
+ # If payload file contains parentId or parentDomainId, keep it as-is
57
+ self.payload = payload
48
58
 
49
59
  @decorator
50
60
  def update_governance_domain(self, args):
@@ -411,57 +421,102 @@ class UnifiedCatalogClient(Endpoint):
411
421
 
412
422
  self.payload = payload
413
423
 
414
- @decorator
415
424
  def update_term(self, args):
416
- """Update an existing Unified Catalog term."""
425
+ """Update an existing Unified Catalog term (supports partial updates)."""
426
+ from purviewcli.client.endpoint import get_data
427
+
417
428
  term_id = args.get("--term-id", [""])[0]
418
- self.method = "PUT"
419
- self.endpoint = f"/datagovernance/catalog/terms/{term_id}"
420
-
421
- # Build payload with all fields (UC API requires full object)
422
- domain_id = args.get("--governance-domain-id", [""])[0]
423
- name = args.get("--name", [""])[0]
424
- description = args.get("--description", [""])[0]
425
- status = args.get("--status", ["Draft"])[0]
426
429
 
427
- # Get owner IDs if provided
428
- owner_ids = args.get("--owner-id", [])
429
- owners = []
430
- if owner_ids:
431
- for owner_id in owner_ids:
432
- owners.append({"id": owner_id})
430
+ # First, fetch the existing term to get current values
431
+ fetch_client = UnifiedCatalogClient()
432
+ existing_term = fetch_client.get_term_by_id({"--term-id": [term_id]})
433
433
 
434
- # Get acronyms if provided
435
- acronyms = args.get("--acronym", [])
434
+ if not existing_term or (isinstance(existing_term, dict) and existing_term.get("error")):
435
+ return {"error": f"Could not fetch existing term {term_id}"}
436
436
 
437
- # Get resources if provided
438
- resources = []
437
+ # Start with existing term data
438
+ payload = {
439
+ "id": term_id,
440
+ "name": existing_term.get("name", ""),
441
+ "description": existing_term.get("description", ""),
442
+ "domain": existing_term.get("domain", ""),
443
+ "status": existing_term.get("status", "Draft"),
444
+ }
445
+
446
+ # Update with provided values (only if explicitly provided)
447
+ if args.get("--name"):
448
+ payload["name"] = args["--name"][0]
449
+ if "--description" in args: # Allow empty string
450
+ payload["description"] = args.get("--description", [""])[0]
451
+ if args.get("--governance-domain-id"):
452
+ payload["domain"] = args["--governance-domain-id"][0]
453
+ if args.get("--status"):
454
+ payload["status"] = args["--status"][0]
455
+
456
+ # Handle owners - replace or add to existing
457
+ contacts = existing_term.get("contacts") or {}
458
+ existing_owners = contacts.get("owner", []) if isinstance(contacts, dict) else []
459
+ if args.get("--owner-id"):
460
+ # Replace owners
461
+ owners = [{"id": oid} for oid in args["--owner-id"]]
462
+ payload["contacts"] = {"owner": owners}
463
+ elif args.get("--add-owner-id"):
464
+ # Add to existing owners
465
+ existing_owner_ids = set()
466
+ if isinstance(existing_owners, list):
467
+ for o in existing_owners:
468
+ if isinstance(o, dict) and o.get("id"):
469
+ existing_owner_ids.add(o.get("id"))
470
+ new_owner_ids = args["--add-owner-id"]
471
+ combined_owner_ids = existing_owner_ids.union(set(new_owner_ids))
472
+ owners = [{"id": oid} for oid in combined_owner_ids]
473
+ payload["contacts"] = {"owner": owners}
474
+ elif existing_owners:
475
+ # Keep existing owners
476
+ payload["contacts"] = {"owner": existing_owners}
477
+
478
+ # Handle acronyms - replace or add to existing
479
+ existing_acronyms = existing_term.get("acronyms", []) or []
480
+ if args.get("--acronym"):
481
+ # Replace acronyms
482
+ payload["acronyms"] = list(args["--acronym"])
483
+ elif args.get("--add-acronym"):
484
+ # Add to existing acronyms
485
+ combined_acronyms = list(set(existing_acronyms + list(args["--add-acronym"])))
486
+ payload["acronyms"] = combined_acronyms
487
+ elif existing_acronyms:
488
+ # Keep existing acronyms
489
+ payload["acronyms"] = existing_acronyms
490
+
491
+ # Handle resources - replace with new ones if provided
492
+ existing_resources = existing_term.get("resources", []) or []
439
493
  resource_names = args.get("--resource-name", [])
440
494
  resource_urls = args.get("--resource-url", [])
441
495
  if resource_names and resource_urls:
496
+ # Replace resources
497
+ resources = []
442
498
  for i in range(min(len(resource_names), len(resource_urls))):
443
499
  resources.append({
444
500
  "name": resource_names[i],
445
501
  "url": resource_urls[i]
446
502
  })
447
-
448
- payload = {
449
- "id": term_id,
450
- "name": name,
451
- "description": description,
452
- "domain": domain_id,
453
- "status": status,
503
+ payload["resources"] = resources
504
+ elif existing_resources:
505
+ # Keep existing resources
506
+ payload["resources"] = existing_resources
507
+
508
+ # Now make the actual PUT request
509
+ http_dict = {
510
+ "app": "datagovernance",
511
+ "method": "PUT",
512
+ "endpoint": f"/datagovernance/catalog/terms/{term_id}",
513
+ "params": {},
514
+ "payload": payload,
515
+ "files": None,
516
+ "headers": {},
454
517
  }
455
518
 
456
- # Add optional fields
457
- if owners:
458
- payload["contacts"] = {"owner": owners}
459
- if acronyms:
460
- payload["acronyms"] = acronyms
461
- if resources:
462
- payload["resources"] = resources
463
-
464
- self.payload = payload
519
+ return get_data(http_dict)
465
520
 
466
521
  @decorator
467
522
  def delete_term(self, args):
@@ -5,7 +5,10 @@ Supports the latest Microsoft Purview REST API specifications with comprehensive
5
5
 
6
6
  import json
7
7
  import asyncio
8
- import aiohttp
8
+ try:
9
+ import aiohttp
10
+ except Exception:
11
+ aiohttp = None
9
12
  import pandas as pd
10
13
  from typing import Dict, List, Optional, Union, Any
11
14
  from dataclasses import dataclass
@@ -73,6 +76,11 @@ class PurviewClient:
73
76
 
74
77
  async def _initialize_session(self):
75
78
  """Initialize HTTP session and authentication"""
79
+ if aiohttp is None:
80
+ raise RuntimeError(
81
+ "The 'aiohttp' package is required for Purview async operations. "
82
+ "Install it in your environment (e.g. '.venv\\Scripts\\pip.exe install aiohttp' or 'pip install aiohttp')."
83
+ )
76
84
  self._credential = DefaultAzureCredential()
77
85
 
78
86
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pvw-cli
3
- Version: 1.0.11
3
+ Version: 1.0.12
4
4
  Summary: Microsoft Purview CLI with comprehensive automation capabilities
5
5
  Author-email: AYOUB KEBAILI <keayoub@msn.com>
6
6
  Maintainer-email: AYOUB KEBAILI <keayoub@msn.com>
@@ -56,7 +56,7 @@ Requires-Dist: pytest-asyncio>=0.20.0; extra == "test"
56
56
  Requires-Dist: pytest-cov>=4.0.0; extra == "test"
57
57
  Requires-Dist: requests-mock>=1.9.0; extra == "test"
58
58
 
59
- # PURVIEW CLI v1.0.11 - Microsoft Purview Automation & Data Governance
59
+ # PURVIEW CLI v1.0.12 - Microsoft Purview Automation & Data Governance
60
60
 
61
61
  > **LATEST UPDATE (October 2025):**
62
62
  > - **🚀 NEW: Complete Data Product CRUD Operations** - Full update and delete support with smart partial updates
@@ -73,7 +73,7 @@ Requires-Dist: requests-mock>=1.9.0; extra == "test"
73
73
 
74
74
  ## What is PVW CLI?
75
75
 
76
- **PVW CLI v1.0.11** is a modern, full-featured command-line interface and Python library for Microsoft Purview. It enables automation and management of *all major Purview APIs* including:
76
+ **PVW CLI v1.0.12** is a modern, full-featured command-line interface and Python library for Microsoft Purview. It enables automation and management of *all major Purview APIs* including:
77
77
 
78
78
  - **NEW Unified Catalog (UC) Management** - Complete governance domains, glossary terms, data products, OKRs, CDEs (NEW)
79
79
  - Entity management (create, update, bulk, import/export)
@@ -141,7 +141,7 @@ For more advanced usage, see the sections below or visit the [documentation](htt
141
141
 
142
142
  ## Overview
143
143
 
144
- **PVW CLI v1.0.11** is a modern command-line interface and Python library for Microsoft Purview, enabling:
144
+ **PVW CLI v1.0.12** is a modern command-line interface and Python library for Microsoft Purview, enabling:
145
145
 
146
146
  - Advanced data catalog search and discovery
147
147
  - Bulk import/export of entities, glossary terms, and lineage
@@ -1,4 +1,4 @@
1
- purviewcli/__init__.py,sha256=dZabjy1DRPdTFgczuBorfIVXE9tYm-8Z5MQK1meA86I,414
1
+ purviewcli/__init__.py,sha256=vZJuLbYKqxhX_kkKROUbb5uUzClrtVWClRZ7pv7eyKw,414
2
2
  purviewcli/__main__.py,sha256=n_PFo1PjW8L1OKCNLsW0vlVSo8tzac_saEYYLTu93iQ,372
3
3
  purviewcli/cli/__init__.py,sha256=UGMctZaXXsV2l2ycnmhTgyksH81_JBQjAPq3oRF2Dqk,56
4
4
  purviewcli/cli/account.py,sha256=YENHkBD0VREajDqtlkTJ-zUvq8aq7LF52HDSOSsgku8,7080
@@ -17,7 +17,7 @@ purviewcli/cli/scan.py,sha256=91iKDH8iVNJKndJAisrKx3J4HRoPH2qfmxguLZH3xHY,13807
17
17
  purviewcli/cli/search.py,sha256=B0Ae3-9JCTKICOkJrYS29tMiFTuLJzlx0ISW_23OHF0,13599
18
18
  purviewcli/cli/share.py,sha256=QRZhHM59RxdYqXOjSYLfVRZmjwMg4Y-bWxMSQVTQiIE,20197
19
19
  purviewcli/cli/types.py,sha256=zo_8rAqDQ1vqi5y-dBh_sVY6i16UaJLLx_vBJBfZrrw,23729
20
- purviewcli/cli/unified_catalog.py,sha256=N22rgUA2V36lxX1iWxqY7o5zYz5arYza9jALWn1ce-8,44668
20
+ purviewcli/cli/unified_catalog.py,sha256=KhpLpppNpv_TIqcUQwO8Mf4GMsseK5PfUYxStNpufVA,49049
21
21
  purviewcli/cli/workflow.py,sha256=wZj5p9fwiPiftKQwGqnn3asAqKuLhYvXRNDcpJQr8WY,14714
22
22
  purviewcli/client/__init__.py,sha256=qjhTkXkgxlNUY3R1HkrT_Znt03-2d8JDolPVOeVv2xI,37
23
23
  purviewcli/client/_account.py,sha256=5lacA7vvjGBLHUDRjFR7B5E8eN6T07rctVDRXR9JFTY,12397
@@ -35,9 +35,9 @@ purviewcli/client/_scan.py,sha256=2atEBD-kKWtFuBSWh2P0cwp42gfg7qgwWq-072QZMs4,15
35
35
  purviewcli/client/_search.py,sha256=vUDgjZtnNkHaCqsCXPp1Drq9Kknrkid17RNSXZhi1yw,11890
36
36
  purviewcli/client/_share.py,sha256=vKENIhePuzi3WQazNfv5U9y-6yxRk222zrFA-SGh1pc,10494
37
37
  purviewcli/client/_types.py,sha256=ONa3wh1F02QOVy51UGq54121TkqRcWczdXIvNqPIFU0,15454
38
- purviewcli/client/_unified_catalog.py,sha256=RlpcoCFOiD1dwMKMuY0Gm0qhn9ystyyJDc3Mo1hF-nc,36819
38
+ purviewcli/client/_unified_catalog.py,sha256=S2t7hIA_wmaZPRVHkIgBkixqKpO1YRduGfIwoz0hkcg,39760
39
39
  purviewcli/client/_workflow.py,sha256=po5lomq07s3d7IAzZ5ved5JO6SsBU_JUA4lQSObdJR4,17904
40
- purviewcli/client/api_client.py,sha256=ZLpNdItu8H2Rfj0HCud2A67Gml7YCS5ZuhR5lrR0r5g,22637
40
+ purviewcli/client/api_client.py,sha256=rNRUhkmZhoCHKWhUvZFUXEWj5eC8LSSnXYYNMb5lHNM,22964
41
41
  purviewcli/client/business_rules.py,sha256=VR4QqOE1Pg0nFjqAE-zbt-KqIenvzImLU-TBLki9bYc,27560
42
42
  purviewcli/client/config.py,sha256=pQIA168XxeddTSaZJ5qXI7KolIrLqDyBTgbILdDzEs0,7963
43
43
  purviewcli/client/data_quality.py,sha256=lAb-ma5MY2nyY4Dq8Q5wN9wzY0J9FikiQN8jPO2u6VU,14988
@@ -53,8 +53,8 @@ purviewcli/client/settings.py,sha256=nYdnYurTZsgv9vcgljnzVxLPtYVl9q6IplqOzi1aRvI
53
53
  purviewcli/client/sync_client.py,sha256=gwCqesJTNaXn1Q-j57O95R9mn3fIOhdP4sc8jBaBcYw,9493
54
54
  purviewcli/plugins/__init__.py,sha256=rpt3OhFt_wSE_o8Ga8AXvw1pqkdBxLmjrhYtE_-LuJo,29
55
55
  purviewcli/plugins/plugin_system.py,sha256=C-_dL4FUj90o1JS7Saxkpov6fz0GIF5PFhZTYwqBkWE,26774
56
- pvw_cli-1.0.11.dist-info/METADATA,sha256=704lbXcsxaSK_H62_8SHSmhQqKrTtSA5tclHP7KwVlc,28082
57
- pvw_cli-1.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- pvw_cli-1.0.11.dist-info/entry_points.txt,sha256=VI6AAbc6sWahOCX7sn_lhJIr9OiJM0pHF7rmw1YVGlE,82
59
- pvw_cli-1.0.11.dist-info/top_level.txt,sha256=LrADzPoKwF1xY0pGKpWauyOVruHCIWKCkT7cwIl6IuI,11
60
- pvw_cli-1.0.11.dist-info/RECORD,,
56
+ pvw_cli-1.0.12.dist-info/METADATA,sha256=r4MVe5jOF7nNC7bLNnqhTIVDF5nQ8mkbJlVK9OAXO5E,28082
57
+ pvw_cli-1.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ pvw_cli-1.0.12.dist-info/entry_points.txt,sha256=VI6AAbc6sWahOCX7sn_lhJIr9OiJM0pHF7rmw1YVGlE,82
59
+ pvw_cli-1.0.12.dist-info/top_level.txt,sha256=LrADzPoKwF1xY0pGKpWauyOVruHCIWKCkT7cwIl6IuI,11
60
+ pvw_cli-1.0.12.dist-info/RECORD,,