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.
- purviewcli/__init__.py +1 -1
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/search.py +201 -10
- purviewcli/cli/unified_catalog.py +678 -142
- purviewcli/cli/workflow.py +44 -4
- purviewcli/client/_health.py +192 -0
- purviewcli/client/_unified_catalog.py +728 -62
- purviewcli/client/_workflow.py +3 -3
- purviewcli/client/endpoint.py +21 -0
- purviewcli/client/sync_client.py +13 -8
- pvw_cli-1.0.10.dist-info/METADATA +888 -0
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/RECORD +15 -13
- pvw_cli-1.0.8.dist-info/METADATA +0 -458
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/WHEEL +0 -0
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.0.8.dist-info → pvw_cli-1.0.10.dist-info}/top_level.txt +0 -0
|
@@ -4,6 +4,8 @@ Implements comprehensive Unified Catalog functionality
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from .endpoint import Endpoint, decorator, get_json, no_api_call_decorator
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class UnifiedCatalogClient(Endpoint):
|
|
@@ -72,12 +74,12 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
72
74
|
def get_data_products(self, args):
|
|
73
75
|
"""Get all data products."""
|
|
74
76
|
self.method = "GET"
|
|
75
|
-
self.endpoint = "/datagovernance/
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
self.endpoint = "/datagovernance/catalog/dataproducts"
|
|
78
|
+
|
|
78
79
|
# Add optional filters
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
domain_id = args.get("--governance-domain-id", [""])[0] or args.get("--domain-id", [""])[0]
|
|
81
|
+
self.params = {"domainId": domain_id} if domain_id else {}
|
|
82
|
+
|
|
81
83
|
if args.get("--status"):
|
|
82
84
|
self.params["status"] = args["--status"][0]
|
|
83
85
|
|
|
@@ -86,40 +88,110 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
86
88
|
"""Get a data product by ID."""
|
|
87
89
|
product_id = args.get("--product-id", [""])[0]
|
|
88
90
|
self.method = "GET"
|
|
89
|
-
self.endpoint = f"/datagovernance/
|
|
91
|
+
self.endpoint = f"/datagovernance/catalog/dataproducts/{product_id}"
|
|
90
92
|
self.params = {}
|
|
91
93
|
|
|
92
94
|
@decorator
|
|
93
95
|
def create_data_product(self, args):
|
|
94
96
|
"""Create a new data product."""
|
|
95
97
|
self.method = "POST"
|
|
96
|
-
self.endpoint = "/datagovernance/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
self.endpoint = "/datagovernance/catalog/dataproducts"
|
|
99
|
+
|
|
100
|
+
# Get domain ID
|
|
101
|
+
domain_id = args.get("--governance-domain-id", [""])[0] or args.get("--domain-id", [""])[0]
|
|
102
|
+
name = args.get("--name", [""])[0]
|
|
103
|
+
description = args.get("--description", [""])[0]
|
|
104
|
+
business_use = args.get("--business-use", [""])[0]
|
|
105
|
+
status = args.get("--status", ["Draft"])[0]
|
|
106
|
+
|
|
107
|
+
# Type mapping for data products
|
|
108
|
+
dp_type = args.get("--type", ["Dataset"])[0]
|
|
109
|
+
|
|
110
|
+
# Build contacts field
|
|
111
|
+
owner_ids = args.get("--owner-id", [])
|
|
112
|
+
owners = []
|
|
113
|
+
if owner_ids:
|
|
114
|
+
for owner_id in owner_ids:
|
|
115
|
+
owners.append({"id": owner_id, "description": ""})
|
|
116
|
+
|
|
117
|
+
payload = {
|
|
118
|
+
"name": name,
|
|
119
|
+
"description": description,
|
|
120
|
+
"domain": domain_id,
|
|
121
|
+
"type": dp_type,
|
|
122
|
+
"businessUse": business_use,
|
|
123
|
+
"status": status,
|
|
102
124
|
}
|
|
125
|
+
|
|
126
|
+
if owners:
|
|
127
|
+
payload["contacts"] = {"owner": owners}
|
|
128
|
+
|
|
129
|
+
# Optional fields
|
|
130
|
+
if args.get("--audience"):
|
|
131
|
+
payload["audience"] = args["--audience"]
|
|
132
|
+
if args.get("--terms-of-use"):
|
|
133
|
+
payload["termsOfUse"] = args["--terms-of-use"]
|
|
134
|
+
if args.get("--documentation"):
|
|
135
|
+
payload["documentation"] = args["--documentation"]
|
|
136
|
+
if args.get("--update-frequency"):
|
|
137
|
+
payload["updateFrequency"] = args["--update-frequency"][0]
|
|
138
|
+
if args.get("--endorsed"):
|
|
139
|
+
payload["endorsed"] = args["--endorsed"][0]
|
|
140
|
+
|
|
141
|
+
self.payload = payload
|
|
103
142
|
|
|
104
143
|
@decorator
|
|
105
144
|
def update_data_product(self, args):
|
|
106
|
-
"""Update a data product."""
|
|
145
|
+
"""Update a data product - fetches current state first, then applies updates."""
|
|
107
146
|
product_id = args.get("--product-id", [""])[0]
|
|
147
|
+
|
|
148
|
+
# First, get the current data product
|
|
149
|
+
get_args = {"--product-id": [product_id]}
|
|
150
|
+
current_product = self.get_data_product_by_id(get_args)
|
|
151
|
+
|
|
152
|
+
if not current_product or (isinstance(current_product, dict) and current_product.get("error")):
|
|
153
|
+
raise ValueError(f"Failed to retrieve data product {product_id} for update")
|
|
154
|
+
|
|
155
|
+
# Start with current product as base
|
|
156
|
+
payload = dict(current_product)
|
|
157
|
+
|
|
158
|
+
# Update only the fields that were provided
|
|
159
|
+
if args.get("--name"):
|
|
160
|
+
payload["name"] = args.get("--name")[0]
|
|
161
|
+
if "--description" in args:
|
|
162
|
+
payload["description"] = args.get("--description")[0]
|
|
163
|
+
if args.get("--domain-id") or args.get("--governance-domain-id"):
|
|
164
|
+
payload["domain"] = args.get("--governance-domain-id", [""])[0] or args.get("--domain-id", [""])[0]
|
|
165
|
+
if args.get("--type"):
|
|
166
|
+
payload["type"] = args.get("--type")[0]
|
|
167
|
+
if args.get("--status"):
|
|
168
|
+
payload["status"] = args.get("--status")[0]
|
|
169
|
+
if "--business-use" in args:
|
|
170
|
+
payload["businessUse"] = args.get("--business-use")[0]
|
|
171
|
+
if args.get("--update-frequency"):
|
|
172
|
+
payload["updateFrequency"] = args.get("--update-frequency")[0]
|
|
173
|
+
if args.get("--endorsed"):
|
|
174
|
+
payload["endorsed"] = args.get("--endorsed")[0] == "true"
|
|
175
|
+
|
|
176
|
+
# Handle owner updates
|
|
177
|
+
owner_ids = args.get("--owner-id", [])
|
|
178
|
+
if owner_ids:
|
|
179
|
+
owners = [{"id": owner_id, "description": "Owner"} for owner_id in owner_ids]
|
|
180
|
+
if "contacts" not in payload:
|
|
181
|
+
payload["contacts"] = {}
|
|
182
|
+
payload["contacts"]["owner"] = owners
|
|
183
|
+
|
|
184
|
+
# Now perform the PUT request
|
|
108
185
|
self.method = "PUT"
|
|
109
|
-
self.endpoint = f"/datagovernance/
|
|
110
|
-
self.payload =
|
|
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],
|
|
115
|
-
}
|
|
186
|
+
self.endpoint = f"/datagovernance/catalog/dataproducts/{product_id}"
|
|
187
|
+
self.payload = payload
|
|
116
188
|
|
|
117
189
|
@decorator
|
|
118
190
|
def delete_data_product(self, args):
|
|
119
191
|
"""Delete a data product."""
|
|
120
192
|
product_id = args.get("--product-id", [""])[0]
|
|
121
193
|
self.method = "DELETE"
|
|
122
|
-
self.endpoint = f"/datagovernance/
|
|
194
|
+
self.endpoint = f"/datagovernance/catalog/dataproducts/{product_id}"
|
|
123
195
|
self.params = {}
|
|
124
196
|
|
|
125
197
|
# ========================================
|
|
@@ -128,46 +200,347 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
128
200
|
|
|
129
201
|
@decorator
|
|
130
202
|
def get_terms(self, args):
|
|
131
|
-
"""Get all
|
|
203
|
+
"""Get all Unified Catalog terms in a governance domain.
|
|
204
|
+
|
|
205
|
+
Uses the Unified Catalog /terms endpoint which is separate from
|
|
206
|
+
Data Map glossary terms. These are business terms managed through
|
|
207
|
+
the Governance Domains interface.
|
|
208
|
+
"""
|
|
132
209
|
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
210
|
+
|
|
133
211
|
self.method = "GET"
|
|
134
|
-
|
|
135
|
-
|
|
212
|
+
|
|
213
|
+
if domain_id:
|
|
214
|
+
# Use Unified Catalog terms API with domainId filter
|
|
215
|
+
self.endpoint = "/datagovernance/catalog/terms"
|
|
216
|
+
self.params = {"domainId": domain_id}
|
|
217
|
+
else:
|
|
218
|
+
# List all UC terms
|
|
219
|
+
self.endpoint = "/datagovernance/catalog/terms"
|
|
220
|
+
self.params = {}
|
|
221
|
+
|
|
222
|
+
# Keeping old Data Map glossary-based implementation for reference/fallback
|
|
223
|
+
def get_terms_from_glossary(self, args):
|
|
224
|
+
"""Get glossary terms from Data Map API (Classic Types view).
|
|
225
|
+
|
|
226
|
+
This is the OLD implementation that queries Data Map glossaries.
|
|
227
|
+
Use get_terms() for Unified Catalog (Governance Domain) terms.
|
|
228
|
+
"""
|
|
229
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
230
|
+
|
|
231
|
+
# If no domain provided, list all glossaries via the Glossary client
|
|
232
|
+
from ._glossary import Glossary
|
|
233
|
+
|
|
234
|
+
gclient = Glossary()
|
|
235
|
+
|
|
236
|
+
# Helper to normalize glossary list responses
|
|
237
|
+
def _normalize_glossary_list(resp):
|
|
238
|
+
if isinstance(resp, dict):
|
|
239
|
+
return resp.get("value", []) or []
|
|
240
|
+
elif isinstance(resp, (list, tuple)):
|
|
241
|
+
return resp
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
if not domain_id:
|
|
246
|
+
glossaries = gclient.glossaryRead({})
|
|
247
|
+
normalized = _normalize_glossary_list(glossaries)
|
|
248
|
+
if os.getenv("PURVIEWCLI_DEBUG"):
|
|
249
|
+
try:
|
|
250
|
+
print("[PURVIEWCLI DEBUG] get_terms returning (no domain_id):", json.dumps(normalized, default=str, indent=2))
|
|
251
|
+
except Exception:
|
|
252
|
+
print("[PURVIEWCLI DEBUG] get_terms returning (no domain_id): (could not serialize)")
|
|
253
|
+
return normalized
|
|
254
|
+
|
|
255
|
+
# 1) Get governance domain info to obtain a human-readable name
|
|
256
|
+
# Note: Nested domains may not be directly fetchable via /businessdomains/{id}
|
|
257
|
+
# If fetch fails, we'll match by domain_id in qualifiedName
|
|
258
|
+
domain_info = None
|
|
259
|
+
domain_name = None
|
|
260
|
+
try:
|
|
261
|
+
domain_info = self.get_governance_domain_by_id({"--domain-id": [domain_id]})
|
|
262
|
+
if isinstance(domain_info, dict):
|
|
263
|
+
domain_name = domain_info.get("name") or domain_info.get("displayName") or domain_info.get("qualifiedName")
|
|
264
|
+
except Exception as e:
|
|
265
|
+
if os.getenv("PURVIEWCLI_DEBUG"):
|
|
266
|
+
print(f"[PURVIEWCLI DEBUG] Could not fetch domain by ID (may be nested): {e}")
|
|
267
|
+
# Continue without domain_name; will match by domain_id in qualifiedName
|
|
268
|
+
|
|
269
|
+
# If explicit glossary GUID provided, fetch that glossary directly
|
|
270
|
+
explicit_guid_list = args.get("--glossary-guid")
|
|
271
|
+
if explicit_guid_list:
|
|
272
|
+
# Extract the GUID string from the list
|
|
273
|
+
explicit_guid = explicit_guid_list[0] if isinstance(explicit_guid_list, list) else explicit_guid_list
|
|
274
|
+
if os.getenv("PURVIEWCLI_DEBUG"):
|
|
275
|
+
print(f"[PURVIEWCLI DEBUG] get_terms: Using explicit glossary GUID: {explicit_guid}")
|
|
276
|
+
# Pass as string, not list, to glossary client
|
|
277
|
+
detailed = gclient.glossaryReadDetailed({"--glossaryGuid": explicit_guid})
|
|
278
|
+
if isinstance(detailed, dict):
|
|
279
|
+
return [{
|
|
280
|
+
"guid": explicit_guid,
|
|
281
|
+
"name": detailed.get("name") or detailed.get("qualifiedName"),
|
|
282
|
+
"terms": detailed.get("terms") or [],
|
|
283
|
+
}]
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
# 2) List all glossaries and try to find ones that look associated
|
|
287
|
+
all_glossaries_resp = gclient.glossaryRead({})
|
|
288
|
+
all_glossaries = _normalize_glossary_list(all_glossaries_resp)
|
|
289
|
+
|
|
290
|
+
if os.getenv("PURVIEWCLI_DEBUG"):
|
|
291
|
+
try:
|
|
292
|
+
print("[PURVIEWCLI DEBUG] get_terms: domain_id=", domain_id, "domain_name=", domain_name)
|
|
293
|
+
print("[PURVIEWCLI DEBUG] all_glossaries:", json.dumps(all_glossaries, default=str, indent=2))
|
|
294
|
+
except Exception:
|
|
295
|
+
print("[PURVIEWCLI DEBUG] get_terms: (could not serialize glossary list)")
|
|
296
|
+
|
|
297
|
+
matched = []
|
|
298
|
+
for g in all_glossaries:
|
|
299
|
+
if not isinstance(g, dict):
|
|
300
|
+
continue
|
|
301
|
+
g_name = g.get("name") or g.get("qualifiedName") or ""
|
|
302
|
+
g_guid = g.get("guid") or g.get("id") or g.get("glossaryGuid")
|
|
303
|
+
qn = str(g.get("qualifiedName", ""))
|
|
304
|
+
|
|
305
|
+
# For nested domains, look for domain_id in qualifiedName
|
|
306
|
+
# Pattern: "Domain Name@domain-id" or similar
|
|
307
|
+
if domain_id and domain_id in qn:
|
|
308
|
+
matched.append((g_guid, g))
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
# Match by exact name if we have domain_name
|
|
312
|
+
if domain_name and domain_name.lower() == str(g_name).lower():
|
|
313
|
+
matched.append((g_guid, g))
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# Match if domain_name appears in qualifiedName
|
|
317
|
+
if domain_name and domain_name.lower() in qn.lower():
|
|
318
|
+
matched.append((g_guid, g))
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# 3) For matched glossaries, fetch detailed glossary (which contains terms)
|
|
322
|
+
results = []
|
|
323
|
+
for guid, base_g in matched:
|
|
324
|
+
if not guid:
|
|
325
|
+
continue
|
|
326
|
+
detailed = gclient.glossaryReadDetailed({"--glossaryGuid": [guid]})
|
|
327
|
+
# glossaryReadDetailed should return a dict representing the glossary
|
|
328
|
+
if isinstance(detailed, dict):
|
|
329
|
+
# some endpoints return the glossary inside 'data' or as raw dict
|
|
330
|
+
glossary_obj = detailed
|
|
331
|
+
else:
|
|
332
|
+
glossary_obj = None
|
|
333
|
+
|
|
334
|
+
# Ensure 'terms' key exists and is a list of term objects
|
|
335
|
+
terms = []
|
|
336
|
+
if isinstance(glossary_obj, dict):
|
|
337
|
+
terms = glossary_obj.get("terms") or []
|
|
338
|
+
results.append({
|
|
339
|
+
"guid": guid,
|
|
340
|
+
"name": base_g.get("name") or base_g.get("qualifiedName"),
|
|
341
|
+
"terms": terms,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
if os.getenv("PURVIEWCLI_DEBUG"):
|
|
345
|
+
try:
|
|
346
|
+
print("[PURVIEWCLI DEBUG] get_terms matched results:", json.dumps(results, default=str, indent=2))
|
|
347
|
+
except Exception:
|
|
348
|
+
print("[PURVIEWCLI DEBUG] get_terms matched results: (could not serialize)")
|
|
349
|
+
return results
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
# If anything fails, return an empty list rather than crashing
|
|
353
|
+
print(f"Warning: failed to list glossaries/terms for domain {domain_id}: {e}")
|
|
354
|
+
return []
|
|
136
355
|
|
|
137
356
|
@decorator
|
|
138
357
|
def get_term_by_id(self, args):
|
|
139
|
-
"""Get a
|
|
358
|
+
"""Get a Unified Catalog term by ID."""
|
|
140
359
|
term_id = args.get("--term-id", [""])[0]
|
|
141
360
|
self.method = "GET"
|
|
142
|
-
self.endpoint = f"/datagovernance/catalog/
|
|
361
|
+
self.endpoint = f"/datagovernance/catalog/terms/{term_id}"
|
|
143
362
|
self.params = {}
|
|
144
363
|
|
|
145
364
|
@decorator
|
|
146
365
|
def create_term(self, args):
|
|
147
|
-
"""Create a new
|
|
366
|
+
"""Create a new Unified Catalog term (Governance Domain term)."""
|
|
148
367
|
self.method = "POST"
|
|
149
|
-
self.endpoint = "/datagovernance/catalog/
|
|
368
|
+
self.endpoint = "/datagovernance/catalog/terms"
|
|
150
369
|
|
|
151
|
-
# Build payload
|
|
370
|
+
# Build Unified Catalog term payload
|
|
371
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
372
|
+
name = args.get("--name", [""])[0]
|
|
373
|
+
description = args.get("--description", [""])[0]
|
|
374
|
+
status = args.get("--status", ["Draft"])[0]
|
|
375
|
+
|
|
376
|
+
# Get owner IDs if provided
|
|
377
|
+
owner_ids = args.get("--owner-id", [])
|
|
378
|
+
owners = []
|
|
379
|
+
if owner_ids:
|
|
380
|
+
for owner_id in owner_ids:
|
|
381
|
+
owners.append({"id": owner_id})
|
|
382
|
+
|
|
383
|
+
# Get acronyms if provided
|
|
384
|
+
acronyms = args.get("--acronym", [])
|
|
385
|
+
|
|
386
|
+
# Get resources if provided
|
|
387
|
+
resources = []
|
|
388
|
+
resource_names = args.get("--resource-name", [])
|
|
389
|
+
resource_urls = args.get("--resource-url", [])
|
|
390
|
+
if resource_names and resource_urls:
|
|
391
|
+
for i in range(min(len(resource_names), len(resource_urls))):
|
|
392
|
+
resources.append({
|
|
393
|
+
"name": resource_names[i],
|
|
394
|
+
"url": resource_urls[i]
|
|
395
|
+
})
|
|
396
|
+
|
|
152
397
|
payload = {
|
|
153
|
-
"name":
|
|
154
|
-
"description":
|
|
155
|
-
"
|
|
156
|
-
"status":
|
|
398
|
+
"name": name,
|
|
399
|
+
"description": description,
|
|
400
|
+
"domain": domain_id,
|
|
401
|
+
"status": status,
|
|
157
402
|
}
|
|
403
|
+
|
|
404
|
+
# Add optional fields
|
|
405
|
+
if owners:
|
|
406
|
+
payload["contacts"] = {"owner": owners}
|
|
407
|
+
if acronyms:
|
|
408
|
+
payload["acronyms"] = acronyms
|
|
409
|
+
if resources:
|
|
410
|
+
payload["resources"] = resources
|
|
411
|
+
|
|
412
|
+
self.payload = payload
|
|
413
|
+
|
|
414
|
+
@decorator
|
|
415
|
+
def update_term(self, args):
|
|
416
|
+
"""Update an existing Unified Catalog term."""
|
|
417
|
+
term_id = args.get("--term-id", [""])[0]
|
|
418
|
+
self.method = "PUT"
|
|
419
|
+
self.endpoint = f"/datagovernance/catalog/terms/{term_id}"
|
|
158
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
|
+
|
|
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})
|
|
433
|
+
|
|
434
|
+
# Get acronyms if provided
|
|
435
|
+
acronyms = args.get("--acronym", [])
|
|
436
|
+
|
|
437
|
+
# Get resources if provided
|
|
438
|
+
resources = []
|
|
439
|
+
resource_names = args.get("--resource-name", [])
|
|
440
|
+
resource_urls = args.get("--resource-url", [])
|
|
441
|
+
if resource_names and resource_urls:
|
|
442
|
+
for i in range(min(len(resource_names), len(resource_urls))):
|
|
443
|
+
resources.append({
|
|
444
|
+
"name": resource_names[i],
|
|
445
|
+
"url": resource_urls[i]
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
payload = {
|
|
449
|
+
"id": term_id,
|
|
450
|
+
"name": name,
|
|
451
|
+
"description": description,
|
|
452
|
+
"domain": domain_id,
|
|
453
|
+
"status": status,
|
|
454
|
+
}
|
|
455
|
+
|
|
159
456
|
# Add optional fields
|
|
160
|
-
if
|
|
161
|
-
payload["
|
|
162
|
-
if
|
|
163
|
-
payload["
|
|
164
|
-
if
|
|
165
|
-
payload["resources"] =
|
|
166
|
-
{"name": args["--resource-name"][0], "url": args["--resource-url"][0]}
|
|
167
|
-
]
|
|
457
|
+
if owners:
|
|
458
|
+
payload["contacts"] = {"owner": owners}
|
|
459
|
+
if acronyms:
|
|
460
|
+
payload["acronyms"] = acronyms
|
|
461
|
+
if resources:
|
|
462
|
+
payload["resources"] = resources
|
|
168
463
|
|
|
169
464
|
self.payload = payload
|
|
170
465
|
|
|
466
|
+
@decorator
|
|
467
|
+
def delete_term(self, args):
|
|
468
|
+
"""Delete a Unified Catalog term."""
|
|
469
|
+
term_id = args.get("--term-id", [""])[0]
|
|
470
|
+
self.method = "DELETE"
|
|
471
|
+
self.endpoint = f"/datagovernance/catalog/terms/{term_id}"
|
|
472
|
+
self.params = {}
|
|
473
|
+
|
|
474
|
+
def _get_or_create_glossary_for_domain(self, domain_id):
|
|
475
|
+
"""Get or create a default glossary for the domain."""
|
|
476
|
+
# Improved implementation:
|
|
477
|
+
# 1. Try to find existing glossaries associated with the domain using get_terms()
|
|
478
|
+
# 2. If none found, attempt to create a new glossary (using the Glossary client) and return its GUID
|
|
479
|
+
# 3. If anything fails, return None so callers don't send an invalid GUID to the API
|
|
480
|
+
if not domain_id:
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
# Try to list glossaries for this domain using the existing get_terms API
|
|
485
|
+
glossaries = self.get_terms({"--governance-domain-id": [domain_id]})
|
|
486
|
+
|
|
487
|
+
# Normalize response to a list of glossary objects
|
|
488
|
+
if isinstance(glossaries, dict):
|
|
489
|
+
candidates = glossaries.get("value", []) or []
|
|
490
|
+
elif isinstance(glossaries, (list, tuple)):
|
|
491
|
+
candidates = glossaries
|
|
492
|
+
else:
|
|
493
|
+
candidates = []
|
|
494
|
+
|
|
495
|
+
# If we have candidate glossaries, prefer the first valid GUID we find
|
|
496
|
+
for g in candidates:
|
|
497
|
+
if not isinstance(g, dict):
|
|
498
|
+
continue
|
|
499
|
+
guid = g.get("guid") or g.get("glossaryGuid") or g.get("id")
|
|
500
|
+
if guid:
|
|
501
|
+
return guid
|
|
502
|
+
|
|
503
|
+
# Nothing found -> attempt to create a glossary for this domain.
|
|
504
|
+
# Try to fetch domain metadata to produce a sensible glossary name.
|
|
505
|
+
# Note: For nested domains, the direct fetch may fail with 404
|
|
506
|
+
domain_info = None
|
|
507
|
+
domain_name = None
|
|
508
|
+
try:
|
|
509
|
+
domain_info = self.get_governance_domain_by_id({"--domain-id": [domain_id]})
|
|
510
|
+
if isinstance(domain_info, dict):
|
|
511
|
+
domain_name = domain_info.get("name") or domain_info.get("displayName")
|
|
512
|
+
except Exception as e:
|
|
513
|
+
if os.getenv("PURVIEWCLI_DEBUG"):
|
|
514
|
+
print(f"[PURVIEWCLI DEBUG] Could not fetch domain for glossary creation (may be nested): {e}")
|
|
515
|
+
# Continue without domain_name
|
|
516
|
+
|
|
517
|
+
glossary_name = domain_name or f"Glossary for domain {domain_id[:8]}"
|
|
518
|
+
payload = {
|
|
519
|
+
"name": glossary_name,
|
|
520
|
+
"qualifiedName": f"{glossary_name}@{domain_id}",
|
|
521
|
+
"shortDescription": f"Auto-created glossary for governance domain {domain_name or domain_id}",
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Import Glossary client lazily to avoid circular imports
|
|
525
|
+
from ._glossary import Glossary
|
|
526
|
+
|
|
527
|
+
gclient = Glossary()
|
|
528
|
+
created = gclient.glossaryCreate({"--payloadFile": payload})
|
|
529
|
+
|
|
530
|
+
# Attempt to extract GUID from the created response
|
|
531
|
+
if isinstance(created, dict):
|
|
532
|
+
new_guid = created.get("guid") or created.get("id") or created.get("glossaryGuid")
|
|
533
|
+
if new_guid:
|
|
534
|
+
return new_guid
|
|
535
|
+
|
|
536
|
+
except Exception as e:
|
|
537
|
+
# Log a helpful warning and continue to safe fallback
|
|
538
|
+
print(f"Warning: error looking up/creating glossary for domain {domain_id}: {e}")
|
|
539
|
+
|
|
540
|
+
# Final safe fallback: return None so create_term doesn't send an invalid GUID
|
|
541
|
+
print(f"Warning: No glossary found or created for domain {domain_id}")
|
|
542
|
+
return None
|
|
543
|
+
|
|
171
544
|
# ========================================
|
|
172
545
|
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
173
546
|
# ========================================
|
|
@@ -177,8 +550,8 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
177
550
|
"""Get all objectives in a governance domain."""
|
|
178
551
|
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
179
552
|
self.method = "GET"
|
|
180
|
-
self.endpoint =
|
|
181
|
-
self.params = {}
|
|
553
|
+
self.endpoint = "/datagovernance/catalog/objectives"
|
|
554
|
+
self.params = {"domainId": domain_id} if domain_id else {}
|
|
182
555
|
|
|
183
556
|
@decorator
|
|
184
557
|
def get_objective_by_id(self, args):
|
|
@@ -194,19 +567,152 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
194
567
|
self.method = "POST"
|
|
195
568
|
self.endpoint = "/datagovernance/catalog/objectives"
|
|
196
569
|
|
|
570
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
571
|
+
definition = args.get("--definition", [""])[0]
|
|
572
|
+
status = args.get("--status", ["Draft"])[0]
|
|
573
|
+
|
|
574
|
+
# Get owner IDs if provided
|
|
575
|
+
owner_ids = args.get("--owner-id", [])
|
|
576
|
+
owners = []
|
|
577
|
+
if owner_ids:
|
|
578
|
+
for owner_id in owner_ids:
|
|
579
|
+
owners.append({"id": owner_id})
|
|
580
|
+
|
|
197
581
|
payload = {
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"status":
|
|
582
|
+
"domain": domain_id,
|
|
583
|
+
"definition": definition,
|
|
584
|
+
"status": status,
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if owners:
|
|
588
|
+
payload["contacts"] = {"owner": owners}
|
|
589
|
+
if args.get("--target-date"):
|
|
590
|
+
payload["targetDate"] = args["--target-date"][0]
|
|
591
|
+
|
|
592
|
+
self.payload = payload
|
|
593
|
+
|
|
594
|
+
@decorator
|
|
595
|
+
def update_objective(self, args):
|
|
596
|
+
"""Update an existing objective."""
|
|
597
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
598
|
+
self.method = "PUT"
|
|
599
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}"
|
|
600
|
+
|
|
601
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
602
|
+
definition = args.get("--definition", [""])[0]
|
|
603
|
+
status = args.get("--status", ["Draft"])[0]
|
|
604
|
+
|
|
605
|
+
# Get owner IDs if provided
|
|
606
|
+
owner_ids = args.get("--owner-id", [])
|
|
607
|
+
owners = []
|
|
608
|
+
if owner_ids:
|
|
609
|
+
for owner_id in owner_ids:
|
|
610
|
+
owners.append({"id": owner_id})
|
|
611
|
+
|
|
612
|
+
payload = {
|
|
613
|
+
"id": objective_id,
|
|
614
|
+
"domain": domain_id,
|
|
615
|
+
"definition": definition,
|
|
616
|
+
"status": status,
|
|
201
617
|
}
|
|
202
618
|
|
|
203
|
-
if
|
|
204
|
-
payload["
|
|
619
|
+
if owners:
|
|
620
|
+
payload["contacts"] = {"owner": owners}
|
|
205
621
|
if args.get("--target-date"):
|
|
206
622
|
payload["targetDate"] = args["--target-date"][0]
|
|
207
623
|
|
|
208
624
|
self.payload = payload
|
|
209
625
|
|
|
626
|
+
@decorator
|
|
627
|
+
def delete_objective(self, args):
|
|
628
|
+
"""Delete an objective."""
|
|
629
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
630
|
+
self.method = "DELETE"
|
|
631
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}"
|
|
632
|
+
self.params = {}
|
|
633
|
+
|
|
634
|
+
# ========================================
|
|
635
|
+
# KEY RESULTS (Part of OKRs)
|
|
636
|
+
# ========================================
|
|
637
|
+
|
|
638
|
+
@decorator
|
|
639
|
+
def get_key_results(self, args):
|
|
640
|
+
"""Get all key results for an objective."""
|
|
641
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
642
|
+
self.method = "GET"
|
|
643
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}/keyResults"
|
|
644
|
+
self.params = {}
|
|
645
|
+
|
|
646
|
+
@decorator
|
|
647
|
+
def get_key_result_by_id(self, args):
|
|
648
|
+
"""Get a key result by ID."""
|
|
649
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
650
|
+
key_result_id = args.get("--key-result-id", [""])[0]
|
|
651
|
+
self.method = "GET"
|
|
652
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}/keyResults/{key_result_id}"
|
|
653
|
+
self.params = {}
|
|
654
|
+
|
|
655
|
+
@decorator
|
|
656
|
+
def create_key_result(self, args):
|
|
657
|
+
"""Create a new key result."""
|
|
658
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
659
|
+
self.method = "POST"
|
|
660
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}/keyResults"
|
|
661
|
+
|
|
662
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
663
|
+
progress = int(args.get("--progress", ["0"])[0])
|
|
664
|
+
goal = int(args.get("--goal", ["100"])[0])
|
|
665
|
+
max_value = int(args.get("--max", ["100"])[0])
|
|
666
|
+
status = args.get("--status", ["OnTrack"])[0]
|
|
667
|
+
definition = args.get("--definition", [""])[0]
|
|
668
|
+
|
|
669
|
+
payload = {
|
|
670
|
+
"progress": progress,
|
|
671
|
+
"goal": goal,
|
|
672
|
+
"max": max_value,
|
|
673
|
+
"status": status,
|
|
674
|
+
"definition": definition,
|
|
675
|
+
"domainId": domain_id,
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
self.payload = payload
|
|
679
|
+
|
|
680
|
+
@decorator
|
|
681
|
+
def update_key_result(self, args):
|
|
682
|
+
"""Update an existing key result."""
|
|
683
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
684
|
+
key_result_id = args.get("--key-result-id", [""])[0]
|
|
685
|
+
self.method = "PUT"
|
|
686
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}/keyResults/{key_result_id}"
|
|
687
|
+
|
|
688
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
689
|
+
progress = int(args.get("--progress", ["0"])[0])
|
|
690
|
+
goal = int(args.get("--goal", ["100"])[0])
|
|
691
|
+
max_value = int(args.get("--max", ["100"])[0])
|
|
692
|
+
status = args.get("--status", ["OnTrack"])[0]
|
|
693
|
+
definition = args.get("--definition", [""])[0]
|
|
694
|
+
|
|
695
|
+
payload = {
|
|
696
|
+
"id": key_result_id,
|
|
697
|
+
"progress": progress,
|
|
698
|
+
"goal": goal,
|
|
699
|
+
"max": max_value,
|
|
700
|
+
"status": status,
|
|
701
|
+
"definition": definition,
|
|
702
|
+
"domainId": domain_id,
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
self.payload = payload
|
|
706
|
+
|
|
707
|
+
@decorator
|
|
708
|
+
def delete_key_result(self, args):
|
|
709
|
+
"""Delete a key result."""
|
|
710
|
+
objective_id = args.get("--objective-id", [""])[0]
|
|
711
|
+
key_result_id = args.get("--key-result-id", [""])[0]
|
|
712
|
+
self.method = "DELETE"
|
|
713
|
+
self.endpoint = f"/datagovernance/catalog/objectives/{objective_id}/keyResults/{key_result_id}"
|
|
714
|
+
self.params = {}
|
|
715
|
+
|
|
210
716
|
# ========================================
|
|
211
717
|
# CRITICAL DATA ELEMENTS (CDEs)
|
|
212
718
|
# ========================================
|
|
@@ -216,36 +722,191 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
216
722
|
"""Get all critical data elements in a governance domain."""
|
|
217
723
|
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
218
724
|
self.method = "GET"
|
|
219
|
-
self.endpoint =
|
|
220
|
-
self.params = {}
|
|
725
|
+
self.endpoint = "/datagovernance/catalog/criticalDataElements"
|
|
726
|
+
self.params = {"domainId": domain_id} if domain_id else {}
|
|
221
727
|
|
|
222
728
|
@decorator
|
|
223
729
|
def get_critical_data_element_by_id(self, args):
|
|
224
730
|
"""Get a critical data element by ID."""
|
|
225
731
|
cde_id = args.get("--cde-id", [""])[0]
|
|
226
732
|
self.method = "GET"
|
|
227
|
-
self.endpoint = f"/datagovernance/catalog/
|
|
733
|
+
self.endpoint = f"/datagovernance/catalog/criticalDataElements/{cde_id}"
|
|
228
734
|
self.params = {}
|
|
229
735
|
|
|
230
736
|
@decorator
|
|
231
737
|
def create_critical_data_element(self, args):
|
|
232
738
|
"""Create a new critical data element."""
|
|
233
739
|
self.method = "POST"
|
|
234
|
-
self.endpoint = "/datagovernance/catalog/
|
|
740
|
+
self.endpoint = "/datagovernance/catalog/criticalDataElements"
|
|
741
|
+
|
|
742
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
743
|
+
name = args.get("--name", [""])[0]
|
|
744
|
+
description = args.get("--description", [""])[0]
|
|
745
|
+
data_type = args.get("--data-type", ["Number"])[0]
|
|
746
|
+
status = args.get("--status", ["Draft"])[0]
|
|
747
|
+
|
|
748
|
+
# Get owner IDs if provided
|
|
749
|
+
owner_ids = args.get("--owner-id", [])
|
|
750
|
+
owners = []
|
|
751
|
+
if owner_ids:
|
|
752
|
+
for owner_id in owner_ids:
|
|
753
|
+
owners.append({"id": owner_id})
|
|
235
754
|
|
|
236
755
|
payload = {
|
|
237
|
-
"name":
|
|
238
|
-
"description":
|
|
239
|
-
"
|
|
240
|
-
"dataType":
|
|
241
|
-
"status":
|
|
756
|
+
"name": name,
|
|
757
|
+
"description": description,
|
|
758
|
+
"domain": domain_id,
|
|
759
|
+
"dataType": data_type,
|
|
760
|
+
"status": status,
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if owners:
|
|
764
|
+
payload["contacts"] = {"owner": owners}
|
|
765
|
+
|
|
766
|
+
self.payload = payload
|
|
767
|
+
|
|
768
|
+
@decorator
|
|
769
|
+
def update_critical_data_element(self, args):
|
|
770
|
+
"""Update an existing critical data element."""
|
|
771
|
+
cde_id = args.get("--cde-id", [""])[0]
|
|
772
|
+
self.method = "PUT"
|
|
773
|
+
self.endpoint = f"/datagovernance/catalog/criticalDataElements/{cde_id}"
|
|
774
|
+
|
|
775
|
+
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
776
|
+
name = args.get("--name", [""])[0]
|
|
777
|
+
description = args.get("--description", [""])[0]
|
|
778
|
+
data_type = args.get("--data-type", ["Number"])[0]
|
|
779
|
+
status = args.get("--status", ["Draft"])[0]
|
|
780
|
+
|
|
781
|
+
# Get owner IDs if provided
|
|
782
|
+
owner_ids = args.get("--owner-id", [])
|
|
783
|
+
owners = []
|
|
784
|
+
if owner_ids:
|
|
785
|
+
for owner_id in owner_ids:
|
|
786
|
+
owners.append({"id": owner_id})
|
|
787
|
+
|
|
788
|
+
payload = {
|
|
789
|
+
"id": cde_id,
|
|
790
|
+
"name": name,
|
|
791
|
+
"description": description,
|
|
792
|
+
"domain": domain_id,
|
|
793
|
+
"dataType": data_type,
|
|
794
|
+
"status": status,
|
|
242
795
|
}
|
|
243
796
|
|
|
244
|
-
if
|
|
245
|
-
payload["
|
|
797
|
+
if owners:
|
|
798
|
+
payload["contacts"] = {"owner": owners}
|
|
246
799
|
|
|
247
800
|
self.payload = payload
|
|
248
801
|
|
|
802
|
+
@decorator
|
|
803
|
+
def delete_critical_data_element(self, args):
|
|
804
|
+
"""Delete a critical data element."""
|
|
805
|
+
cde_id = args.get("--cde-id", [""])[0]
|
|
806
|
+
self.method = "DELETE"
|
|
807
|
+
self.endpoint = f"/datagovernance/catalog/criticalDataElements/{cde_id}"
|
|
808
|
+
self.params = {}
|
|
809
|
+
|
|
810
|
+
# ========================================
|
|
811
|
+
# RELATIONSHIPS
|
|
812
|
+
# ========================================
|
|
813
|
+
|
|
814
|
+
@decorator
|
|
815
|
+
def get_relationships(self, args):
|
|
816
|
+
"""Get all relationships for an entity (term, data product, or CDE).
|
|
817
|
+
|
|
818
|
+
Supported entity types for filtering:
|
|
819
|
+
- CustomMetadata: Custom attributes attached to the entity
|
|
820
|
+
- DataAsset: Data assets (tables, columns) linked to the entity
|
|
821
|
+
- DataProduct: Data products related to the entity
|
|
822
|
+
- CriticalDataColumn: Critical data columns related to the entity
|
|
823
|
+
- CriticalDataElement: Critical data elements related to the entity
|
|
824
|
+
- Term: Other terms related to this entity
|
|
825
|
+
"""
|
|
826
|
+
entity_type = args.get("--entity-type", [""])[0] # Term, DataProduct, CriticalDataElement
|
|
827
|
+
entity_id = args.get("--entity-id", [""])[0]
|
|
828
|
+
filter_type = args.get("--filter-type", [""])[0] # Optional: CustomMetadata, DataAsset, DataProduct, etc.
|
|
829
|
+
|
|
830
|
+
# Map entity type to endpoint
|
|
831
|
+
endpoint_map = {
|
|
832
|
+
"Term": "terms",
|
|
833
|
+
"DataProduct": "dataproducts",
|
|
834
|
+
"CriticalDataElement": "criticalDataElements",
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
endpoint_base = endpoint_map.get(entity_type)
|
|
838
|
+
if not endpoint_base:
|
|
839
|
+
raise ValueError(f"Invalid entity type: {entity_type}. Must be Term, DataProduct, or CriticalDataElement")
|
|
840
|
+
|
|
841
|
+
self.method = "GET"
|
|
842
|
+
self.endpoint = f"/datagovernance/catalog/{endpoint_base}/{entity_id}/relationships"
|
|
843
|
+
|
|
844
|
+
# Add optional entity type filter
|
|
845
|
+
if filter_type:
|
|
846
|
+
valid_filters = ["CustomMetadata", "DataAsset", "DataProduct", "CriticalDataColumn", "CriticalDataElement", "Term"]
|
|
847
|
+
if filter_type not in valid_filters:
|
|
848
|
+
raise ValueError(f"Invalid filter type: {filter_type}. Must be one of: {', '.join(valid_filters)}")
|
|
849
|
+
self.params = {"entityType": filter_type}
|
|
850
|
+
else:
|
|
851
|
+
self.params = {}
|
|
852
|
+
|
|
853
|
+
@decorator
|
|
854
|
+
def create_relationship(self, args):
|
|
855
|
+
"""Create a relationship between entities (terms, data products, CDEs)."""
|
|
856
|
+
entity_type = args.get("--entity-type", [""])[0] # Term, DataProduct, CriticalDataElement
|
|
857
|
+
entity_id = args.get("--entity-id", [""])[0]
|
|
858
|
+
target_entity_id = args.get("--target-entity-id", [""])[0]
|
|
859
|
+
relationship_type = args.get("--relationship-type", ["Related"])[0] # Synonym, Related
|
|
860
|
+
description = args.get("--description", [""])[0]
|
|
861
|
+
|
|
862
|
+
# Map entity type to endpoint
|
|
863
|
+
endpoint_map = {
|
|
864
|
+
"Term": "terms",
|
|
865
|
+
"DataProduct": "dataproducts",
|
|
866
|
+
"CriticalDataElement": "criticalDataElements",
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
endpoint_base = endpoint_map.get(entity_type)
|
|
870
|
+
if not endpoint_base:
|
|
871
|
+
raise ValueError(f"Invalid entity type: {entity_type}. Must be Term, DataProduct, or CriticalDataElement")
|
|
872
|
+
|
|
873
|
+
self.method = "POST"
|
|
874
|
+
self.endpoint = f"/datagovernance/catalog/{endpoint_base}/{entity_id}/relationships"
|
|
875
|
+
self.params = {"entityType": entity_type}
|
|
876
|
+
|
|
877
|
+
self.payload = {
|
|
878
|
+
"entityId": target_entity_id,
|
|
879
|
+
"relationshipType": relationship_type,
|
|
880
|
+
"description": description,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
@decorator
|
|
884
|
+
def delete_relationship(self, args):
|
|
885
|
+
"""Delete a relationship between entities."""
|
|
886
|
+
entity_type = args.get("--entity-type", [""])[0]
|
|
887
|
+
entity_id = args.get("--entity-id", [""])[0]
|
|
888
|
+
target_entity_id = args.get("--target-entity-id", [""])[0]
|
|
889
|
+
relationship_type = args.get("--relationship-type", ["Related"])[0]
|
|
890
|
+
|
|
891
|
+
# Map entity type to endpoint
|
|
892
|
+
endpoint_map = {
|
|
893
|
+
"Term": "terms",
|
|
894
|
+
"DataProduct": "dataproducts",
|
|
895
|
+
"CriticalDataElement": "criticalDataElements",
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
endpoint_base = endpoint_map.get(entity_type)
|
|
899
|
+
if not endpoint_base:
|
|
900
|
+
raise ValueError(f"Invalid entity type: {entity_type}")
|
|
901
|
+
|
|
902
|
+
self.method = "DELETE"
|
|
903
|
+
self.endpoint = f"/datagovernance/catalog/{endpoint_base}/{entity_id}/relationships"
|
|
904
|
+
self.params = {
|
|
905
|
+
"entityId": target_entity_id,
|
|
906
|
+
"entityType": entity_type,
|
|
907
|
+
"relationshipType": relationship_type,
|
|
908
|
+
}
|
|
909
|
+
|
|
249
910
|
# ========================================
|
|
250
911
|
# UTILITY METHODS
|
|
251
912
|
# ========================================
|
|
@@ -259,11 +920,16 @@ Microsoft Purview Unified Catalog Client
|
|
|
259
920
|
Available Operations:
|
|
260
921
|
- Governance Domains: list, get, create, update, delete
|
|
261
922
|
- Data Products: list, get, create, update, delete
|
|
262
|
-
-
|
|
263
|
-
- Objectives (OKRs): list, get, create
|
|
264
|
-
-
|
|
923
|
+
- Terms: list, get, create, update, delete
|
|
924
|
+
- Objectives (OKRs): list, get, create, update, delete
|
|
925
|
+
- Key Results: list, get, create, update, delete
|
|
926
|
+
- Critical Data Elements: list, get, create, update, delete
|
|
927
|
+
- Relationships: create, delete (between terms, data products, CDEs)
|
|
265
928
|
|
|
266
929
|
Use --payloadFile to provide JSON payload for create/update operations.
|
|
267
930
|
Use individual flags like --name, --description for simple operations.
|
|
931
|
+
|
|
932
|
+
Note: This client uses the Unified Catalog API (/datagovernance/catalog/*)
|
|
933
|
+
which is separate from the Data Map API (/catalog/api/atlas/*).
|
|
268
934
|
"""
|
|
269
935
|
return {"message": help_text}
|