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.

@@ -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/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,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/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/catalog/dataProducts"
98
+ self.endpoint = "/datagovernance/catalog/dataproducts"
97
99
 
98
- # Get domain ID from either parameter name (CLI uses --governance-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
- # Map CLI type values to API type values
102
- type_mapping = {
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 (required)
110
+ # Build contacts field
111
111
  owner_ids = args.get("--owner-id", [])
112
- if not owner_ids:
113
- # Default to current user if no owner specified
114
- owner_ids = ["75d058e8-ac84-4d33-b01c-54a8d3cbbac1"] # Current authenticated user
112
+ owners = []
113
+ if owner_ids:
114
+ for owner_id in owner_ids:
115
+ owners.append({"id": owner_id, "description": ""})
115
116
 
116
- contacts = {
117
- "owner": [{"id": owner_id, "description": "Owner"} for owner_id in owner_ids]
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
- "status": args.get("--status", ["Draft"])[0],
125
- "type": api_type,
126
- "contacts": contacts,
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/dataProducts/{product_id}"
135
- self.payload = get_json(args, "--payloadFile") or {
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/dataProducts/{product_id}"
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 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
+ """
157
209
  domain_id = args.get("--governance-domain-id", [""])[0]
210
+
158
211
  self.method = "GET"
159
- self.endpoint = "/catalog/api/atlas/v2/glossary"
160
- self.params = {
161
- "domainId": domain_id
162
- } if domain_id else {}
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 glossary term by ID."""
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/api/atlas/v2/glossary/term/{term_id}"
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 glossary term."""
366
+ """Create a new Unified Catalog term (Governance Domain term)."""
175
367
  self.method = "POST"
176
- self.endpoint = "/catalog/api/atlas/v2/glossary/term"
368
+ self.endpoint = "/datagovernance/catalog/terms"
177
369
 
178
- # Build Atlas-compatible payload
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
- # For now, we need to find a glossary in this domain
182
- # This is a temporary solution - ideally CLI should accept glossary-id
183
- glossary_guid = self._get_or_create_glossary_for_domain(domain_id)
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": args.get("--name", [""])[0],
187
- "shortDescription": args.get("--description", [""])[0],
188
- "longDescription": args.get("--description", [""])[0],
189
- "status": args.get("--status", ["ACTIVE"])[0].upper(),
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 args.get("--acronyms"):
195
- payload["abbreviation"] = ",".join(args["--acronyms"])
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
- # Associate with glossary
198
- if glossary_guid:
199
- payload["anchor"] = {"glossaryGuid": glossary_guid}
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
- # Temporary solution: Use the known glossary GUID we created earlier
206
- # In a real implementation, this would query the API to find/create glossaries
207
-
208
- # For now, hardcode the glossary we know exists
209
- if domain_id == "d4cdd762-eeca-4401-81b1-e93d8aff3fe4":
210
- return "69a6aff1-e7d9-4cd4-8d8c-08d6fa95594d" # HR Domain Glossary
211
-
212
- # For other domains, fall back to domain_id (will likely fail)
213
- # TODO: Implement proper glossary lookup/creation
214
- print(f"Warning: Using domain_id as glossary_id for domain {domain_id} - this may fail")
215
- return domain_id
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 = f"/datagovernance/catalog/businessdomains/{domain_id}/objectives"
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
- "definition": args.get("--definition", [""])[0],
245
- "governanceDomainId": args.get("--governance-domain-id", [""])[0],
246
- "status": args.get("--status", ["Draft"])[0],
582
+ "domain": domain_id,
583
+ "definition": definition,
584
+ "status": status,
247
585
  }
248
586
 
249
- if args.get("--owner-id"):
250
- payload["ownerIds"] = args["--owner-id"]
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 = f"/datagovernance/catalog/businessdomains/{domain_id}/criticaldataelements"
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/criticaldataelements/{cde_id}"
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/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})
281
754
 
282
755
  payload = {
283
- "name": args.get("--name", [""])[0],
284
- "description": args.get("--description", [""])[0],
285
- "governanceDomainId": args.get("--governance-domain-id", [""])[0],
286
- "dataType": args.get("--data-type", ["String"])[0],
287
- "status": args.get("--status", ["Draft"])[0],
756
+ "name": name,
757
+ "description": description,
758
+ "domain": domain_id,
759
+ "dataType": data_type,
760
+ "status": status,
288
761
  }
289
762
 
290
- if args.get("--owner-id"):
291
- payload["ownerIds"] = args["--owner-id"]
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
- - Glossary Terms: list, get, create
309
- - Objectives (OKRs): list, get, create
310
- - 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)
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}