pvw-cli 1.0.9__py3-none-any.whl → 1.0.11__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/entity.py +34 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/unified_catalog.py +519 -74
- purviewcli/cli/workflow.py +44 -4
- purviewcli/client/_health.py +192 -0
- purviewcli/client/_unified_catalog.py +715 -95
- purviewcli/client/_workflow.py +3 -3
- purviewcli/client/endpoint.py +21 -0
- purviewcli/client/sync_client.py +7 -2
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/METADATA +110 -22
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/RECORD +15 -13
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/WHEEL +0 -0
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.0.9.dist-info → pvw_cli-1.0.11.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/catalog/
|
|
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,65 +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/catalog/
|
|
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/catalog/
|
|
98
|
+
self.endpoint = "/datagovernance/catalog/dataproducts"
|
|
97
99
|
|
|
98
|
-
# Get domain ID
|
|
100
|
+
# Get domain ID
|
|
99
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]
|
|
100
106
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
"Operational": "Dataset",
|
|
104
|
-
"Analytical": "Dataset",
|
|
105
|
-
"Reference": "MasterDataAndReferenceData"
|
|
106
|
-
}
|
|
107
|
-
cli_type = args.get("--type", ["Dataset"])[0]
|
|
108
|
-
api_type = type_mapping.get(cli_type, cli_type) # Use mapping or pass through
|
|
107
|
+
# Type mapping for data products
|
|
108
|
+
dp_type = args.get("--type", ["Dataset"])[0]
|
|
109
109
|
|
|
110
|
-
# Build contacts field
|
|
110
|
+
# Build contacts field
|
|
111
111
|
owner_ids = args.get("--owner-id", [])
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
owners = []
|
|
113
|
+
if owner_ids:
|
|
114
|
+
for owner_id in owner_ids:
|
|
115
|
+
owners.append({"id": owner_id, "description": ""})
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self.payload = get_json(args, "--payloadFile") or {
|
|
121
|
-
"name": args.get("--name", [""])[0],
|
|
122
|
-
"description": args.get("--description", [""])[0],
|
|
117
|
+
payload = {
|
|
118
|
+
"name": name,
|
|
119
|
+
"description": description,
|
|
123
120
|
"domain": domain_id,
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
121
|
+
"type": dp_type,
|
|
122
|
+
"businessUse": business_use,
|
|
123
|
+
"status": status,
|
|
127
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
|
|
128
142
|
|
|
129
143
|
@decorator
|
|
130
144
|
def update_data_product(self, args):
|
|
131
|
-
"""Update a data product."""
|
|
145
|
+
"""Update a data product - fetches current state first, then applies updates."""
|
|
132
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
|
|
133
185
|
self.method = "PUT"
|
|
134
|
-
self.endpoint = f"/datagovernance/catalog/
|
|
135
|
-
self.payload =
|
|
136
|
-
"name": args.get("--name", [""])[0],
|
|
137
|
-
"description": args.get("--description", [""])[0],
|
|
138
|
-
"domainId": args.get("--domain-id", [""])[0],
|
|
139
|
-
"status": args.get("--status", [""])[0],
|
|
140
|
-
}
|
|
186
|
+
self.endpoint = f"/datagovernance/catalog/dataproducts/{product_id}"
|
|
187
|
+
self.payload = payload
|
|
141
188
|
|
|
142
189
|
@decorator
|
|
143
190
|
def delete_data_product(self, args):
|
|
144
191
|
"""Delete a data product."""
|
|
145
192
|
product_id = args.get("--product-id", [""])[0]
|
|
146
193
|
self.method = "DELETE"
|
|
147
|
-
self.endpoint = f"/datagovernance/catalog/
|
|
194
|
+
self.endpoint = f"/datagovernance/catalog/dataproducts/{product_id}"
|
|
148
195
|
self.params = {}
|
|
149
196
|
|
|
150
197
|
# ========================================
|
|
@@ -153,66 +200,346 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
153
200
|
|
|
154
201
|
@decorator
|
|
155
202
|
def get_terms(self, args):
|
|
156
|
-
"""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
|
+
"""
|
|
157
209
|
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
210
|
+
|
|
158
211
|
self.method = "GET"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 []
|
|
163
355
|
|
|
164
356
|
@decorator
|
|
165
357
|
def get_term_by_id(self, args):
|
|
166
|
-
"""Get a
|
|
358
|
+
"""Get a Unified Catalog term by ID."""
|
|
167
359
|
term_id = args.get("--term-id", [""])[0]
|
|
168
360
|
self.method = "GET"
|
|
169
|
-
self.endpoint = f"/catalog/
|
|
361
|
+
self.endpoint = f"/datagovernance/catalog/terms/{term_id}"
|
|
170
362
|
self.params = {}
|
|
171
363
|
|
|
172
364
|
@decorator
|
|
173
365
|
def create_term(self, args):
|
|
174
|
-
"""Create a new
|
|
366
|
+
"""Create a new Unified Catalog term (Governance Domain term)."""
|
|
175
367
|
self.method = "POST"
|
|
176
|
-
self.endpoint = "/catalog/
|
|
368
|
+
self.endpoint = "/datagovernance/catalog/terms"
|
|
177
369
|
|
|
178
|
-
# Build
|
|
370
|
+
# Build Unified Catalog term payload
|
|
179
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]
|
|
180
375
|
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
})
|
|
184
396
|
|
|
185
397
|
payload = {
|
|
186
|
-
"name":
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"status":
|
|
190
|
-
"qualifiedName": f"{args.get('--name', [''])[0]}@{glossary_guid}",
|
|
398
|
+
"name": name,
|
|
399
|
+
"description": description,
|
|
400
|
+
"domain": domain_id,
|
|
401
|
+
"status": status,
|
|
191
402
|
}
|
|
192
|
-
|
|
403
|
+
|
|
193
404
|
# Add optional fields
|
|
194
|
-
if
|
|
195
|
-
payload["
|
|
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}"
|
|
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
|
+
})
|
|
196
447
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
448
|
+
payload = {
|
|
449
|
+
"id": term_id,
|
|
450
|
+
"name": name,
|
|
451
|
+
"description": description,
|
|
452
|
+
"domain": domain_id,
|
|
453
|
+
"status": status,
|
|
454
|
+
}
|
|
455
|
+
|
|
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
|
|
200
463
|
|
|
201
464
|
self.payload = payload
|
|
202
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
|
+
|
|
203
474
|
def _get_or_create_glossary_for_domain(self, domain_id):
|
|
204
475
|
"""Get or create a default glossary for the domain."""
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
#
|
|
209
|
-
if domain_id
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
216
543
|
|
|
217
544
|
# ========================================
|
|
218
545
|
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
@@ -223,8 +550,8 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
223
550
|
"""Get all objectives in a governance domain."""
|
|
224
551
|
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
225
552
|
self.method = "GET"
|
|
226
|
-
self.endpoint =
|
|
227
|
-
self.params = {}
|
|
553
|
+
self.endpoint = "/datagovernance/catalog/objectives"
|
|
554
|
+
self.params = {"domainId": domain_id} if domain_id else {}
|
|
228
555
|
|
|
229
556
|
@decorator
|
|
230
557
|
def get_objective_by_id(self, args):
|
|
@@ -240,19 +567,152 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
240
567
|
self.method = "POST"
|
|
241
568
|
self.endpoint = "/datagovernance/catalog/objectives"
|
|
242
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
|
+
|
|
243
581
|
payload = {
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"status":
|
|
582
|
+
"domain": domain_id,
|
|
583
|
+
"definition": definition,
|
|
584
|
+
"status": status,
|
|
247
585
|
}
|
|
248
586
|
|
|
249
|
-
if
|
|
250
|
-
payload["
|
|
587
|
+
if owners:
|
|
588
|
+
payload["contacts"] = {"owner": owners}
|
|
251
589
|
if args.get("--target-date"):
|
|
252
590
|
payload["targetDate"] = args["--target-date"][0]
|
|
253
591
|
|
|
254
592
|
self.payload = payload
|
|
255
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,
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if owners:
|
|
620
|
+
payload["contacts"] = {"owner": owners}
|
|
621
|
+
if args.get("--target-date"):
|
|
622
|
+
payload["targetDate"] = args["--target-date"][0]
|
|
623
|
+
|
|
624
|
+
self.payload = payload
|
|
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
|
+
|
|
256
716
|
# ========================================
|
|
257
717
|
# CRITICAL DATA ELEMENTS (CDEs)
|
|
258
718
|
# ========================================
|
|
@@ -262,36 +722,191 @@ class UnifiedCatalogClient(Endpoint):
|
|
|
262
722
|
"""Get all critical data elements in a governance domain."""
|
|
263
723
|
domain_id = args.get("--governance-domain-id", [""])[0]
|
|
264
724
|
self.method = "GET"
|
|
265
|
-
self.endpoint =
|
|
266
|
-
self.params = {}
|
|
725
|
+
self.endpoint = "/datagovernance/catalog/criticalDataElements"
|
|
726
|
+
self.params = {"domainId": domain_id} if domain_id else {}
|
|
267
727
|
|
|
268
728
|
@decorator
|
|
269
729
|
def get_critical_data_element_by_id(self, args):
|
|
270
730
|
"""Get a critical data element by ID."""
|
|
271
731
|
cde_id = args.get("--cde-id", [""])[0]
|
|
272
732
|
self.method = "GET"
|
|
273
|
-
self.endpoint = f"/datagovernance/catalog/
|
|
733
|
+
self.endpoint = f"/datagovernance/catalog/criticalDataElements/{cde_id}"
|
|
274
734
|
self.params = {}
|
|
275
735
|
|
|
276
736
|
@decorator
|
|
277
737
|
def create_critical_data_element(self, args):
|
|
278
738
|
"""Create a new critical data element."""
|
|
279
739
|
self.method = "POST"
|
|
280
|
-
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})
|
|
281
754
|
|
|
282
755
|
payload = {
|
|
283
|
-
"name":
|
|
284
|
-
"description":
|
|
285
|
-
"
|
|
286
|
-
"dataType":
|
|
287
|
-
"status":
|
|
756
|
+
"name": name,
|
|
757
|
+
"description": description,
|
|
758
|
+
"domain": domain_id,
|
|
759
|
+
"dataType": data_type,
|
|
760
|
+
"status": status,
|
|
288
761
|
}
|
|
289
762
|
|
|
290
|
-
if
|
|
291
|
-
payload["
|
|
763
|
+
if owners:
|
|
764
|
+
payload["contacts"] = {"owner": owners}
|
|
292
765
|
|
|
293
766
|
self.payload = payload
|
|
294
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,
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if owners:
|
|
798
|
+
payload["contacts"] = {"owner": owners}
|
|
799
|
+
|
|
800
|
+
self.payload = payload
|
|
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
|
+
|
|
295
910
|
# ========================================
|
|
296
911
|
# UTILITY METHODS
|
|
297
912
|
# ========================================
|
|
@@ -305,11 +920,16 @@ Microsoft Purview Unified Catalog Client
|
|
|
305
920
|
Available Operations:
|
|
306
921
|
- Governance Domains: list, get, create, update, delete
|
|
307
922
|
- Data Products: list, get, create, update, delete
|
|
308
|
-
-
|
|
309
|
-
- Objectives (OKRs): list, get, create
|
|
310
|
-
-
|
|
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)
|
|
311
928
|
|
|
312
929
|
Use --payloadFile to provide JSON payload for create/update operations.
|
|
313
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/*).
|
|
314
934
|
"""
|
|
315
935
|
return {"message": help_text}
|