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.

@@ -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/dataProducts"
76
- self.params = {}
77
-
77
+ self.endpoint = "/datagovernance/catalog/dataproducts"
78
+
78
79
  # Add optional filters
79
- if args.get("--domain-id"):
80
- self.params["domainId"] = args["--domain-id"][0]
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/dataProducts/{product_id}"
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/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],
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/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],
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/dataProducts/{product_id}"
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 glossary terms in a governance domain."""
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
- self.endpoint = f"/datagovernance/catalog/businessdomains/{domain_id}/glossaryterms"
135
- self.params = {}
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 glossary term by ID."""
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/glossaryterms/{term_id}"
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 glossary term."""
366
+ """Create a new Unified Catalog term (Governance Domain term)."""
148
367
  self.method = "POST"
149
- self.endpoint = "/datagovernance/catalog/glossaryterms"
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": 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],
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 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
- ]
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 = f"/datagovernance/catalog/businessdomains/{domain_id}/objectives"
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
- "definition": args.get("--definition", [""])[0],
199
- "governanceDomainId": args.get("--governance-domain-id", [""])[0],
200
- "status": args.get("--status", ["Draft"])[0],
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 args.get("--owner-id"):
204
- payload["ownerIds"] = args["--owner-id"]
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 = f"/datagovernance/catalog/businessdomains/{domain_id}/criticaldataelements"
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/criticaldataelements/{cde_id}"
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/criticaldataelements"
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": 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],
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 args.get("--owner-id"):
245
- payload["ownerIds"] = args["--owner-id"]
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
- - Glossary Terms: list, get, create
263
- - Objectives (OKRs): list, get, create
264
- - Critical Data Elements: list, get, create
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}