pvw-cli 1.0.4__py3-none-any.whl → 1.0.8__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.

@@ -33,27 +33,48 @@ class Search(Endpoint):
33
33
  self.endpoint = ENDPOINTS["discovery"]["query"]
34
34
  self.params = get_api_version_params("datamap")
35
35
 
36
- # Build search payload
36
+ # Check if direct payload is provided
37
+ if args.get("--payload"):
38
+ import json
39
+ self.payload = json.loads(args["--payload"])
40
+ return
41
+
42
+ # Check if payload file is provided
43
+ if args.get("--payloadFile"):
44
+ self.payload = get_json(args, "--payloadFile")
45
+ return
46
+
47
+ # Build search payload from individual parameters
48
+ # Support both '--keywords' and the CLI shorthand '--query'
49
+ keywords = args.get("--keywords") if args.get("--keywords") is not None else args.get("--query")
50
+ if keywords is None:
51
+ keywords = "*"
52
+
37
53
  search_request = {
38
- "keywords": args.get("--keywords", "*"),
54
+ "keywords": keywords,
39
55
  "limit": args.get("--limit", 50),
40
56
  "offset": args.get("--offset", 0),
41
- "filter": {},
42
- "facets": []
43
57
  }
44
58
 
59
+ # Only add filter if there are actual filter values
60
+ filter_obj = {}
61
+
45
62
  # Add filters if provided
46
63
  if args.get("--filter"):
47
- search_request["filter"] = self._parse_filter(args["--filter"])
64
+ filter_obj.update(self._parse_filter(args["--filter"]))
48
65
 
49
66
  if args.get("--entityType"):
50
- search_request["filter"]["entityType"] = args["--entityType"]
67
+ filter_obj["entityType"] = args["--entityType"]
51
68
 
52
69
  if args.get("--classification"):
53
- search_request["filter"]["classification"] = args["--classification"]
70
+ filter_obj["classification"] = args["--classification"]
54
71
 
55
72
  if args.get("--term"):
56
- search_request["filter"]["term"] = args["--term"]
73
+ filter_obj["term"] = args["--term"]
74
+
75
+ # Only include filter if it has content
76
+ if filter_obj:
77
+ search_request["filter"] = filter_obj
57
78
 
58
79
  # Add facets if requested
59
80
  if args.get("--facets"):
@@ -1,318 +1,269 @@
1
- import json
2
- import requests
3
- import os
4
- from .endpoint import Endpoint, get_data
5
- from .endpoints import ENDPOINTS, DATAMAP_API_VERSION
1
+ """
2
+ Microsoft Purview Unified Catalog API Client
3
+ Implements comprehensive Unified Catalog functionality
4
+ """
6
5
 
6
+ from .endpoint import Endpoint, decorator, get_json, no_api_call_decorator
7
7
 
8
- class UnifiedCatalogDataProduct:
9
- """
10
- Microsoft Purview Unified Catalog Data Product client
11
- Based on the actual Microsoft Purview Unified Catalog API structure
12
- """
8
+
9
+ class UnifiedCatalogClient(Endpoint):
10
+ """Client for Microsoft Purview Unified Catalog API."""
13
11
 
14
12
  def __init__(self):
15
- """Initialize the unified catalog data product client"""
16
- pass
17
-
18
- def _make_request(self, method, endpoint, data=None, params=None):
19
- """Make HTTP request through the existing infrastructure"""
20
- try:
21
- # Unified Catalog uses the Data Governance base path
22
- base_path = "/datagovernance/catalog"
23
- full_endpoint = base_path + endpoint
24
-
25
- http_dict = {
26
- "method": method,
27
- "endpoint": full_endpoint,
28
- "params": params or {},
29
- "payload": data
30
- }
31
-
32
- # Add Unified Catalog API version to params
33
- if "api-version" not in http_dict["params"]:
34
- http_dict["params"]["api-version"] = DATAMAP_API_VERSION
35
-
36
- return get_data(http_dict)
37
- except Exception as e:
38
- return {
39
- "status": "error",
40
- "message": str(e),
41
- "status_code": getattr(e, 'response', {}).get('status_code', 500) if hasattr(e, 'response') else 500
42
- }
43
-
44
- # === DATA PRODUCT OPERATIONS ===
45
-
46
- def dataProductList(self, args):
47
- """List data products with optional filtering"""
48
- params = {}
49
- if args.get("--limit"):
50
- params["$top"] = args["--limit"]
51
- if args.get("--skip"):
52
- params["$skip"] = args["--skip"]
53
- if args.get("--governanceDomain"):
54
- params["domainId"] = args["--governanceDomain"]
55
-
56
- return self._make_request("GET", "/dataproducts", params=params)
57
-
58
- def dataProductCreate(self, args):
59
- """Create a new data product"""
60
- payload = self._get_payload_from_args(args)
61
- return self._make_request("POST", "/dataproducts", data=payload)
62
-
63
- def dataProductRead(self, args):
64
- """Get a data product by ID"""
65
- data_product_id = args.get("--dataProductId")
66
- if not data_product_id:
67
- return {"status": "error", "message": "dataProductId is required"}
68
-
69
- endpoint = f"/dataproducts/{data_product_id}"
70
- return self._make_request("GET", endpoint)
71
-
72
- def dataProductUpdate(self, args):
73
- """Update a data product"""
74
- data_product_id = args.get("--dataProductId")
75
- if not data_product_id:
76
- return {"status": "error", "message": "dataProductId is required"}
77
-
78
- payload = self._get_payload_from_args(args)
79
- endpoint = f"/dataproducts/{data_product_id}"
80
- return self._make_request("PUT", endpoint, data=payload)
81
-
82
- def dataProductDelete(self, args):
83
- """Delete a data product"""
84
- data_product_id = args.get("--dataProductId")
85
- if not data_product_id:
86
- return {"status": "error", "message": "dataProductId is required"}
87
-
88
- endpoint = f"/dataproducts/{data_product_id}"
89
- return self._make_request("DELETE", endpoint)
90
-
91
- # === GLOSSARY OPERATIONS ===
92
-
93
- def glossaryTermsList(self, args):
94
- """List glossary terms"""
95
- params = {}
96
- if args.get("--governanceDomain"):
97
- params["domainId"] = args["--governanceDomain"]
98
-
99
- return self._make_request("GET", "/terms", params=params)
100
-
101
- def glossaryTermCreate(self, args):
102
- """Create a glossary term"""
103
- payload = self._get_payload_from_args(args)
104
- return self._make_request("POST", "/terms", data=payload)
105
-
106
- def glossaryTermRead(self, args):
107
- """Get a glossary term by ID"""
108
- term_id = args.get("--termId")
109
- if not term_id:
110
- return {"status": "error", "message": "termId is required"}
111
-
112
- endpoint = f"/terms/{term_id}"
113
- return self._make_request("GET", endpoint)
114
-
115
- def glossaryTermUpdate(self, args):
116
- """Update a glossary term"""
117
- term_id = args.get("--termId")
118
- if not term_id:
119
- return {"status": "error", "message": "termId is required"}
120
-
121
- payload = self._get_payload_from_args(args)
122
- endpoint = f"/terms/{term_id}"
123
- return self._make_request("PUT", endpoint, data=payload)
124
-
125
- def glossaryTermDelete(self, args):
126
- """Delete a glossary term"""
127
- term_id = args.get("--termId")
128
- if not term_id:
129
- return {"status": "error", "message": "termId is required"}
130
-
131
- endpoint = f"/terms/{term_id}"
132
- return self._make_request("DELETE", endpoint)
133
-
134
- # === BUSINESS DOMAIN OPERATIONS ===
135
-
136
- def businessDomainsList(self, args):
137
- """List business domains"""
138
- return self._make_request("GET", "/businessdomains")
139
-
140
- def businessDomainCreate(self, args):
141
- """Create a business domain"""
142
- payload = self._get_payload_from_args(args)
143
- return self._make_request("POST", "/businessdomains", data=payload)
144
-
145
- def businessDomainRead(self, args):
146
- """Get a business domain by ID"""
147
- domain_id = args.get("--domainId")
148
- if not domain_id:
149
- return {"status": "error", "message": "domainId is required"}
150
-
151
- endpoint = f"/businessdomains/{domain_id}"
152
- return self._make_request("GET", endpoint)
153
-
154
- def businessDomainUpdate(self, args):
155
- """Update a business domain"""
156
- domain_id = args.get("--domainId")
157
- if not domain_id:
158
- return {"status": "error", "message": "domainId is required"}
159
-
160
- payload = self._get_payload_from_args(args)
161
- endpoint = f"/businessdomains/{domain_id}"
162
- return self._make_request("PUT", endpoint, data=payload)
163
-
164
- def businessDomainDelete(self, args):
165
- """Delete a business domain"""
166
- domain_id = args.get("--domainId")
167
- if not domain_id:
168
- return {"status": "error", "message": "domainId is required"}
169
-
170
- endpoint = f"/businessdomains/{domain_id}"
171
- return self._make_request("DELETE", endpoint)
172
-
173
- # === RELATIONSHIP OPERATIONS ===
174
-
175
- def relationshipCreate(self, args):
176
- """Create a relationship between entities"""
177
- entity_type = args.get("--entityType")
178
- entity_id = args.get("--entityId")
179
-
180
- if not entity_type or not entity_id:
181
- return {"status": "error", "message": "entityType and entityId are required"}
182
-
183
- endpoint_map = {
184
- "DataProduct": "dataproducts",
185
- "Term": "terms",
186
- }
187
-
188
- if entity_type not in endpoint_map:
189
- return {"status": "error", "message": f"Unsupported entity type: {entity_type}"}
190
-
191
- endpoint = f"/{endpoint_map[entity_type]}/{entity_id}/relationships"
192
- payload = self._get_payload_from_args(args)
193
- params = {"entityType": entity_type}
194
-
195
- return self._make_request("POST", endpoint, data=payload, params=params)
196
-
197
- def relationshipDelete(self, args):
198
- """Delete a relationship between entities"""
199
- entity_type = args.get("--entityType")
200
- entity_id = args.get("--entityId")
201
- target_entity_id = args.get("--targetEntityId")
202
- relationship_type = args.get("--relationshipType")
203
-
204
- if not all([entity_type, entity_id, target_entity_id, relationship_type]):
205
- return {"status": "error", "message": "entityType, entityId, targetEntityId, and relationshipType are required"}
206
-
207
- endpoint_map = {
208
- "DataProduct": "dataproducts",
209
- "Term": "terms",
13
+ """Initialize the Unified Catalog client."""
14
+ Endpoint.__init__(self)
15
+ self.app = "datagovernance" # Use datagovernance app for UC endpoints
16
+
17
+ # ========================================
18
+ # GOVERNANCE DOMAINS
19
+ # ========================================
20
+ @decorator
21
+ def get_governance_domains(self, args):
22
+ """Get all governance domains."""
23
+ self.method = "GET"
24
+ self.endpoint = "/datagovernance/catalog/businessdomains"
25
+ self.params = {}
26
+
27
+ @decorator
28
+ def get_governance_domain_by_id(self, args):
29
+ """Get a governance domain by ID."""
30
+ domain_id = args.get("--domain-id", [""])[0]
31
+ self.method = "GET"
32
+ self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}"
33
+ self.params = {}
34
+
35
+ @decorator
36
+ def create_governance_domain(self, args):
37
+ """Create a new governance domain."""
38
+ self.method = "POST"
39
+ self.endpoint = "/datagovernance/catalog/businessdomains"
40
+ self.payload = get_json(args, "--payloadFile") or {
41
+ "name": args.get("--name", [""])[0],
42
+ "description": args.get("--description", [""])[0],
43
+ "type": args.get("--type", ["FunctionalUnit"])[0],
44
+ "status": args.get("--status", ["Draft"])[0],
210
45
  }
211
-
212
- if entity_type not in endpoint_map:
213
- return {"status": "error", "message": f"Unsupported entity type: {entity_type}"}
214
-
215
- endpoint = f"/{endpoint_map[entity_type]}/{entity_id}/relationships"
216
- params = {
217
- "entityId": target_entity_id,
218
- "entityType": entity_type,
219
- "relationshipType": relationship_type
46
+
47
+ @decorator
48
+ def update_governance_domain(self, args):
49
+ """Update a governance domain."""
50
+ domain_id = args.get("--domain-id", [""])[0]
51
+ self.method = "PUT"
52
+ self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}"
53
+ self.payload = get_json(args, "--payloadFile") or {
54
+ "name": args.get("--name", [""])[0],
55
+ "description": args.get("--description", [""])[0],
56
+ "type": args.get("--type", [""])[0],
57
+ "status": args.get("--status", [""])[0],
220
58
  }
221
-
222
- return self._make_request("DELETE", endpoint, params=params)
223
-
224
- # === HELPER METHODS ===
225
-
226
- def _get_payload_from_args(self, args):
227
- """Extract payload from args, either from --payloadFile or construct from individual args"""
228
- if args.get("--payloadFile"):
229
- # Load from file
230
- with open(args["--payloadFile"], 'r') as f:
231
- return json.load(f)
232
-
233
- # Construct from individual arguments
234
- payload = {}
235
-
236
- # Common fields
237
- if args.get("--name"):
238
- payload["name"] = args["--name"]
239
- if args.get("--description"):
240
- payload["description"] = args["--description"]
241
- if args.get("--governanceDomain"):
242
- payload["domain"] = args["--governanceDomain"]
243
- if args.get("--type"):
244
- payload["type"] = args["--type"]
245
- if args.get("--businessUse"):
246
- payload["businessUse"] = args["--businessUse"]
59
+
60
+ @decorator
61
+ def delete_governance_domain(self, args):
62
+ """Delete a governance domain."""
63
+ domain_id = args.get("--domain-id", [""])[0]
64
+ self.method = "DELETE"
65
+ self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}"
66
+ self.params = {}
67
+
68
+ # ========================================
69
+ # DATA PRODUCTS
70
+ # ========================================
71
+ @decorator
72
+ def get_data_products(self, args):
73
+ """Get all data products."""
74
+ self.method = "GET"
75
+ self.endpoint = "/datagovernance/dataProducts"
76
+ self.params = {}
77
+
78
+ # Add optional filters
79
+ if args.get("--domain-id"):
80
+ self.params["domainId"] = args["--domain-id"][0]
247
81
  if args.get("--status"):
248
- payload["status"] = args["--status"]
249
- if args.get("--endorsed"):
250
- payload["endorsed"] = args["--endorsed"] == "true"
251
-
252
- # Relationships
253
- if args.get("--entityId"):
254
- payload["entityId"] = args["--entityId"]
255
- if args.get("--relationshipType"):
256
- payload["relationshipType"] = args["--relationshipType"]
257
-
258
- return payload
259
-
260
- # === HIGH-LEVEL CONVENIENCE METHODS ===
261
- # These methods provide a simpler interface for common operations
262
-
263
- def create_data_product(self, name, description=None, domain_guid=None):
264
- """Create a data product with simple parameters"""
265
- args = {
266
- "--name": name,
267
- "--description": description or "",
268
- "--governanceDomain": domain_guid,
269
- "--type": "Dataset", # Default type
270
- "--status": "Draft" # Default status
82
+ self.params["status"] = args["--status"][0]
83
+
84
+ @decorator
85
+ def get_data_product_by_id(self, args):
86
+ """Get a data product by ID."""
87
+ product_id = args.get("--product-id", [""])[0]
88
+ self.method = "GET"
89
+ self.endpoint = f"/datagovernance/dataProducts/{product_id}"
90
+ self.params = {}
91
+
92
+ @decorator
93
+ def create_data_product(self, args):
94
+ """Create a new data product."""
95
+ self.method = "POST"
96
+ self.endpoint = "/datagovernance/dataProducts"
97
+ self.payload = get_json(args, "--payloadFile") or {
98
+ "name": args.get("--name", [""])[0],
99
+ "description": args.get("--description", [""])[0],
100
+ "domainId": args.get("--domain-id", [""])[0],
101
+ "status": args.get("--status", ["Draft"])[0],
271
102
  }
272
- return self.dataProductCreate(args)
273
-
274
- def get_data_product(self, data_product_id):
275
- """Get a data product by ID"""
276
- args = {"--dataProductId": data_product_id}
277
- return self.dataProductRead(args)
278
-
279
- def update_data_product(self, data_product_id, updates):
280
- """Update a data product with a dictionary of updates"""
281
- args = {"--dataProductId": data_product_id}
282
- args.update({f"--{k}": v for k, v in updates.items()})
283
- return self.dataProductUpdate(args)
284
-
285
- def delete_data_product(self, data_product_id):
286
- """Delete a data product by ID"""
287
- args = {"--dataProductId": data_product_id}
288
- return self.dataProductDelete(args)
289
-
290
- def list_data_products(self, limit=50, offset=0, domain_id=None):
291
- """List data products with pagination"""
292
- args = {
293
- "--limit": str(limit),
294
- "--skip": str(offset)
103
+
104
+ @decorator
105
+ def update_data_product(self, args):
106
+ """Update a data product."""
107
+ product_id = args.get("--product-id", [""])[0]
108
+ self.method = "PUT"
109
+ self.endpoint = f"/datagovernance/dataProducts/{product_id}"
110
+ self.payload = get_json(args, "--payloadFile") or {
111
+ "name": args.get("--name", [""])[0],
112
+ "description": args.get("--description", [""])[0],
113
+ "domainId": args.get("--domain-id", [""])[0],
114
+ "status": args.get("--status", [""])[0],
295
115
  }
296
- if domain_id:
297
- args["--governanceDomain"] = domain_id
298
- return self.dataProductList(args)
299
-
300
- def create_glossary_term(self, name, description, domain_id):
301
- """Create a glossary term"""
302
- args = {
303
- "--name": name,
304
- "--description": description,
305
- "--governanceDomain": domain_id,
306
- "--status": "Draft"
116
+
117
+ @decorator
118
+ def delete_data_product(self, args):
119
+ """Delete a data product."""
120
+ product_id = args.get("--product-id", [""])[0]
121
+ self.method = "DELETE"
122
+ self.endpoint = f"/datagovernance/dataProducts/{product_id}"
123
+ self.params = {}
124
+
125
+ # ========================================
126
+ # GLOSSARY TERMS
127
+ # ========================================
128
+
129
+ @decorator
130
+ def get_terms(self, args):
131
+ """Get all glossary terms in a governance domain."""
132
+ domain_id = args.get("--governance-domain-id", [""])[0]
133
+ self.method = "GET"
134
+ self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}/glossaryterms"
135
+ self.params = {}
136
+
137
+ @decorator
138
+ def get_term_by_id(self, args):
139
+ """Get a glossary term by ID."""
140
+ term_id = args.get("--term-id", [""])[0]
141
+ self.method = "GET"
142
+ self.endpoint = f"/datagovernance/catalog/glossaryterms/{term_id}"
143
+ self.params = {}
144
+
145
+ @decorator
146
+ def create_term(self, args):
147
+ """Create a new glossary term."""
148
+ self.method = "POST"
149
+ self.endpoint = "/datagovernance/catalog/glossaryterms"
150
+
151
+ # Build payload
152
+ payload = {
153
+ "name": args.get("--name", [""])[0],
154
+ "description": args.get("--description", [""])[0],
155
+ "governanceDomainId": args.get("--governance-domain-id", [""])[0],
156
+ "status": args.get("--status", ["Draft"])[0],
307
157
  }
308
- return self.glossaryTermCreate(args)
309
-
310
- def link_term_to_data_product(self, data_product_id, term_id):
311
- """Link a glossary term to a data product"""
312
- args = {
313
- "--entityType": "DataProduct",
314
- "--entityId": data_product_id,
315
- "--entityId": term_id,
316
- "--relationshipType": "Related"
158
+
159
+ # Add optional fields
160
+ if args.get("--acronyms"):
161
+ payload["acronyms"] = args["--acronyms"]
162
+ if args.get("--owner-id"):
163
+ payload["ownerIds"] = args["--owner-id"]
164
+ if args.get("--resource-name") and args.get("--resource-url"):
165
+ payload["resources"] = [
166
+ {"name": args["--resource-name"][0], "url": args["--resource-url"][0]}
167
+ ]
168
+
169
+ self.payload = payload
170
+
171
+ # ========================================
172
+ # OBJECTIVES AND KEY RESULTS (OKRs)
173
+ # ========================================
174
+
175
+ @decorator
176
+ def get_objectives(self, args):
177
+ """Get all objectives in a governance domain."""
178
+ domain_id = args.get("--governance-domain-id", [""])[0]
179
+ self.method = "GET"
180
+ self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}/objectives"
181
+ self.params = {}
182
+
183
+ @decorator
184
+ def get_objective_by_id(self, args):
185
+ """Get an objective by ID."""
186
+ objective_id = args.get("--objective-id", [""])[0]
187
+ self.method = "GET"
188
+ self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}"
189
+ self.params = {}
190
+
191
+ @decorator
192
+ def create_objective(self, args):
193
+ """Create a new objective."""
194
+ self.method = "POST"
195
+ self.endpoint = "/datagovernance/catalog/objectives"
196
+
197
+ payload = {
198
+ "definition": args.get("--definition", [""])[0],
199
+ "governanceDomainId": args.get("--governance-domain-id", [""])[0],
200
+ "status": args.get("--status", ["Draft"])[0],
317
201
  }
318
- return self.relationshipCreate(args)
202
+
203
+ if args.get("--owner-id"):
204
+ payload["ownerIds"] = args["--owner-id"]
205
+ if args.get("--target-date"):
206
+ payload["targetDate"] = args["--target-date"][0]
207
+
208
+ self.payload = payload
209
+
210
+ # ========================================
211
+ # CRITICAL DATA ELEMENTS (CDEs)
212
+ # ========================================
213
+
214
+ @decorator
215
+ def get_critical_data_elements(self, args):
216
+ """Get all critical data elements in a governance domain."""
217
+ domain_id = args.get("--governance-domain-id", [""])[0]
218
+ self.method = "GET"
219
+ self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}/criticaldataelements"
220
+ self.params = {}
221
+
222
+ @decorator
223
+ def get_critical_data_element_by_id(self, args):
224
+ """Get a critical data element by ID."""
225
+ cde_id = args.get("--cde-id", [""])[0]
226
+ self.method = "GET"
227
+ self.endpoint = f"/datagovernance/catalog/criticaldataelements/{cde_id}"
228
+ self.params = {}
229
+
230
+ @decorator
231
+ def create_critical_data_element(self, args):
232
+ """Create a new critical data element."""
233
+ self.method = "POST"
234
+ self.endpoint = "/datagovernance/catalog/criticaldataelements"
235
+
236
+ payload = {
237
+ "name": args.get("--name", [""])[0],
238
+ "description": args.get("--description", [""])[0],
239
+ "governanceDomainId": args.get("--governance-domain-id", [""])[0],
240
+ "dataType": args.get("--data-type", ["String"])[0],
241
+ "status": args.get("--status", ["Draft"])[0],
242
+ }
243
+
244
+ if args.get("--owner-id"):
245
+ payload["ownerIds"] = args["--owner-id"]
246
+
247
+ self.payload = payload
248
+
249
+ # ========================================
250
+ # UTILITY METHODS
251
+ # ========================================
252
+
253
+ @no_api_call_decorator
254
+ def help(self, args):
255
+ """Display help information for Unified Catalog operations."""
256
+ help_text = """
257
+ Microsoft Purview Unified Catalog Client
258
+
259
+ Available Operations:
260
+ - Governance Domains: list, get, create, update, delete
261
+ - Data Products: list, get, create, update, delete
262
+ - Glossary Terms: list, get, create
263
+ - Objectives (OKRs): list, get, create
264
+ - Critical Data Elements: list, get, create
265
+
266
+ Use --payloadFile to provide JSON payload for create/update operations.
267
+ Use individual flags like --name, --description for simple operations.
268
+ """
269
+ return {"message": help_text}