pvw-cli 1.0.6__py3-none-any.whl → 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pvw-cli might be problematic. Click here for more details.
- purviewcli/__init__.py +2 -2
- purviewcli/cli/__init__.py +2 -2
- purviewcli/cli/cli.py +11 -13
- purviewcli/cli/collections.py +363 -0
- purviewcli/cli/entity.py +464 -0
- purviewcli/cli/unified_catalog.py +766 -0
- purviewcli/client/_search.py +7 -2
- purviewcli/client/_unified_catalog.py +258 -307
- purviewcli/client/endpoint.py +13 -1
- purviewcli/client/sync_client.py +70 -13
- {pvw_cli-1.0.6.dist-info → pvw_cli-1.0.8.dist-info}/METADATA +93 -34
- {pvw_cli-1.0.6.dist-info → pvw_cli-1.0.8.dist-info}/RECORD +15 -16
- {pvw_cli-1.0.6.dist-info → pvw_cli-1.0.8.dist-info}/WHEEL +1 -1
- purviewcli/cli/data_product.py +0 -278
- purviewcli/client/_data_product.py +0 -168
- {pvw_cli-1.0.6.dist-info → pvw_cli-1.0.8.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.0.6.dist-info → pvw_cli-1.0.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Purview Unified Catalog CLI Commands
|
|
3
|
+
Replaces data_product functionality with comprehensive Unified Catalog operations
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import tempfile
|
|
10
|
+
import os
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from purviewcli.client._unified_catalog import UnifiedCatalogClient
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group()
|
|
20
|
+
def uc():
|
|
21
|
+
"""Manage Unified Catalog in Microsoft Purview (domains, terms, data products, OKRs, CDEs)."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ========================================
|
|
26
|
+
# GOVERNANCE DOMAINS
|
|
27
|
+
# ========================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@uc.group()
|
|
31
|
+
def domain():
|
|
32
|
+
"""Manage governance domains."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@domain.command()
|
|
37
|
+
@click.option("--name", required=True, help="Name of the governance domain")
|
|
38
|
+
@click.option(
|
|
39
|
+
"--description", required=False, default="", help="Description of the governance domain"
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--type",
|
|
43
|
+
required=False,
|
|
44
|
+
default="FunctionalUnit",
|
|
45
|
+
type=click.Choice(["FunctionalUnit", "BusinessUnit", "Department"]),
|
|
46
|
+
help="Type of governance domain",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--owner-id",
|
|
50
|
+
required=False,
|
|
51
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
52
|
+
multiple=True,
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--status",
|
|
56
|
+
required=False,
|
|
57
|
+
default="Draft",
|
|
58
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
59
|
+
help="Status of the governance domain",
|
|
60
|
+
)
|
|
61
|
+
def create(name, description, type, owner_id, status):
|
|
62
|
+
"""Create a new governance domain."""
|
|
63
|
+
try:
|
|
64
|
+
client = UnifiedCatalogClient()
|
|
65
|
+
|
|
66
|
+
# Build args dictionary in Purview CLI format
|
|
67
|
+
args = {
|
|
68
|
+
"--name": [name],
|
|
69
|
+
"--description": [description],
|
|
70
|
+
"--type": [type],
|
|
71
|
+
"--status": [status],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result = client.create_governance_domain(args)
|
|
75
|
+
|
|
76
|
+
if not result:
|
|
77
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
78
|
+
return
|
|
79
|
+
if isinstance(result, dict) and "error" in result:
|
|
80
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
console.print(f"[green]✅ SUCCESS:[/green] Created governance domain '{name}'")
|
|
84
|
+
console.print(json.dumps(result, indent=2))
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@domain.command(name="list")
|
|
91
|
+
def list_domains():
|
|
92
|
+
"""List all governance domains."""
|
|
93
|
+
try:
|
|
94
|
+
client = UnifiedCatalogClient()
|
|
95
|
+
args = {} # No arguments needed for list operation
|
|
96
|
+
result = client.get_governance_domains(args)
|
|
97
|
+
|
|
98
|
+
if not result:
|
|
99
|
+
console.print("[yellow]No governance domains found.[/yellow]")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Handle both list and dict responses
|
|
103
|
+
if isinstance(result, (list, tuple)):
|
|
104
|
+
domains = result
|
|
105
|
+
elif isinstance(result, dict):
|
|
106
|
+
domains = result.get("value", [])
|
|
107
|
+
else:
|
|
108
|
+
domains = []
|
|
109
|
+
|
|
110
|
+
if not domains:
|
|
111
|
+
console.print("[yellow]No governance domains found.[/yellow]")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
table = Table(title="Governance Domains")
|
|
115
|
+
table.add_column("ID", style="cyan")
|
|
116
|
+
table.add_column("Name", style="green")
|
|
117
|
+
table.add_column("Type", style="blue")
|
|
118
|
+
table.add_column("Status", style="yellow")
|
|
119
|
+
table.add_column("Owners", style="magenta")
|
|
120
|
+
|
|
121
|
+
for domain in domains:
|
|
122
|
+
owners = ", ".join(
|
|
123
|
+
[o.get("name", o.get("id", "Unknown")) for o in domain.get("owners", [])]
|
|
124
|
+
)
|
|
125
|
+
table.add_row(
|
|
126
|
+
domain.get("id", "N/A"),
|
|
127
|
+
domain.get("name", "N/A"),
|
|
128
|
+
domain.get("type", "N/A"),
|
|
129
|
+
domain.get("status", "N/A"),
|
|
130
|
+
owners or "None",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
console.print(table)
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@domain.command()
|
|
140
|
+
@click.option("--domain-id", required=True, help="ID of the governance domain")
|
|
141
|
+
def show(domain_id):
|
|
142
|
+
"""Show details of a governance domain."""
|
|
143
|
+
try:
|
|
144
|
+
client = UnifiedCatalogClient()
|
|
145
|
+
args = {"--domain-id": [domain_id]}
|
|
146
|
+
result = client.get_governance_domain_by_id(args)
|
|
147
|
+
|
|
148
|
+
if not result:
|
|
149
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
150
|
+
return
|
|
151
|
+
if isinstance(result, dict) and result.get("error"):
|
|
152
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Domain not found')}")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
console.print(json.dumps(result, indent=2))
|
|
156
|
+
except Exception as e:
|
|
157
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ========================================
|
|
161
|
+
# DATA PRODUCTS (for backwards compatibility)
|
|
162
|
+
# ========================================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@uc.group()
|
|
166
|
+
def dataproduct():
|
|
167
|
+
"""Manage data products."""
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataproduct.command()
|
|
172
|
+
@click.option("--name", required=True, help="Name of the data product")
|
|
173
|
+
@click.option("--description", required=False, default="", help="Description of the data product")
|
|
174
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
175
|
+
@click.option(
|
|
176
|
+
"--type",
|
|
177
|
+
required=False,
|
|
178
|
+
default="Operational",
|
|
179
|
+
type=click.Choice(["Operational", "Analytical", "Reference"]),
|
|
180
|
+
help="Type of data product",
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
"--owner-id",
|
|
184
|
+
required=False,
|
|
185
|
+
help="Owner Entra ID (can be specified multiple times)",
|
|
186
|
+
multiple=True,
|
|
187
|
+
)
|
|
188
|
+
@click.option("--business-use", required=False, default="", help="Business use description")
|
|
189
|
+
@click.option(
|
|
190
|
+
"--update-frequency",
|
|
191
|
+
required=False,
|
|
192
|
+
default="Weekly",
|
|
193
|
+
type=click.Choice(["Daily", "Weekly", "Monthly", "Quarterly", "Annually"]),
|
|
194
|
+
help="Update frequency",
|
|
195
|
+
)
|
|
196
|
+
@click.option("--endorsed", is_flag=True, help="Mark as endorsed")
|
|
197
|
+
@click.option(
|
|
198
|
+
"--status",
|
|
199
|
+
required=False,
|
|
200
|
+
default="Draft",
|
|
201
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
202
|
+
help="Status of the data product",
|
|
203
|
+
)
|
|
204
|
+
def create(
|
|
205
|
+
name, description, domain_id, type, owner_id, business_use, update_frequency, endorsed, status
|
|
206
|
+
):
|
|
207
|
+
"""Create a new data product."""
|
|
208
|
+
try:
|
|
209
|
+
client = UnifiedCatalogClient()
|
|
210
|
+
owners = [{"id": oid} for oid in owner_id] if owner_id else []
|
|
211
|
+
|
|
212
|
+
# Build args dictionary in Purview CLI format
|
|
213
|
+
args = {
|
|
214
|
+
"--governance-domain-id": [domain_id],
|
|
215
|
+
"--name": [name],
|
|
216
|
+
"--description": [description],
|
|
217
|
+
"--type": [type],
|
|
218
|
+
"--status": [status],
|
|
219
|
+
"--business-use": [business_use],
|
|
220
|
+
"--update-frequency": [update_frequency],
|
|
221
|
+
}
|
|
222
|
+
if endorsed:
|
|
223
|
+
args["--endorsed"] = ["true"]
|
|
224
|
+
if owners:
|
|
225
|
+
args["--owner-id"] = [owner["id"] for owner in owners]
|
|
226
|
+
|
|
227
|
+
result = client.create_data_product(args)
|
|
228
|
+
|
|
229
|
+
if not result:
|
|
230
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
231
|
+
return
|
|
232
|
+
if isinstance(result, dict) and "error" in result:
|
|
233
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
console.print(f"[green]✅ SUCCESS:[/green] Created data product '{name}'")
|
|
237
|
+
console.print(json.dumps(result, indent=2))
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataproduct.command(name="list")
|
|
244
|
+
@click.option("--domain-id", required=False, help="Governance domain ID (optional filter)")
|
|
245
|
+
@click.option("--status", required=False, help="Status filter (Draft, Published, Archived)")
|
|
246
|
+
def list_data_products(domain_id, status):
|
|
247
|
+
"""List all data products (optionally filtered by domain or status)."""
|
|
248
|
+
try:
|
|
249
|
+
client = UnifiedCatalogClient()
|
|
250
|
+
|
|
251
|
+
# Build args dictionary in Purview CLI format
|
|
252
|
+
args = {}
|
|
253
|
+
if domain_id:
|
|
254
|
+
args["--domain-id"] = [domain_id]
|
|
255
|
+
if status:
|
|
256
|
+
args["--status"] = [status]
|
|
257
|
+
|
|
258
|
+
result = client.get_data_products(args)
|
|
259
|
+
|
|
260
|
+
# Handle both list and dict responses
|
|
261
|
+
if isinstance(result, (list, tuple)):
|
|
262
|
+
products = result
|
|
263
|
+
elif isinstance(result, dict):
|
|
264
|
+
products = result.get("value", [])
|
|
265
|
+
else:
|
|
266
|
+
products = []
|
|
267
|
+
|
|
268
|
+
if not products:
|
|
269
|
+
filter_msg = ""
|
|
270
|
+
if domain_id:
|
|
271
|
+
filter_msg += f" in domain '{domain_id}'"
|
|
272
|
+
if status:
|
|
273
|
+
filter_msg += f" with status '{status}'"
|
|
274
|
+
console.print(f"[yellow]No data products found{filter_msg}.[/yellow]")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
table = Table(title="Data Products")
|
|
278
|
+
table.add_column("ID", style="cyan")
|
|
279
|
+
table.add_column("Name", style="green")
|
|
280
|
+
table.add_column("Domain ID", style="blue")
|
|
281
|
+
table.add_column("Status", style="yellow")
|
|
282
|
+
table.add_column("Description", style="white")
|
|
283
|
+
|
|
284
|
+
for product in products:
|
|
285
|
+
table.add_row(
|
|
286
|
+
product.get("id", "N/A"),
|
|
287
|
+
product.get("name", "N/A"),
|
|
288
|
+
product.get("domainId", "N/A"),
|
|
289
|
+
product.get("status", "N/A"),
|
|
290
|
+
(
|
|
291
|
+
(product.get("description", "")[:50] + "...")
|
|
292
|
+
if len(product.get("description", "")) > 50
|
|
293
|
+
else product.get("description", "")
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
console.print(table)
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataproduct.command()
|
|
304
|
+
@click.option("--product-id", required=True, help="ID of the data product")
|
|
305
|
+
def show(product_id):
|
|
306
|
+
"""Show details of a data product."""
|
|
307
|
+
try:
|
|
308
|
+
client = UnifiedCatalogClient()
|
|
309
|
+
args = {"--product-id": [product_id]}
|
|
310
|
+
result = client.get_data_product_by_id(args)
|
|
311
|
+
|
|
312
|
+
if not result:
|
|
313
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
314
|
+
return
|
|
315
|
+
if isinstance(result, dict) and "error" in result:
|
|
316
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Data product not found')}")
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
console.print(json.dumps(result, indent=2))
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ========================================
|
|
326
|
+
# GLOSSARY TERMS
|
|
327
|
+
# ========================================
|
|
328
|
+
|
|
329
|
+
@uc.group()
|
|
330
|
+
def term():
|
|
331
|
+
"""Manage glossary terms."""
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@term.command()
|
|
336
|
+
@click.option("--name", required=True, help="Name of the glossary term")
|
|
337
|
+
@click.option("--description", required=False, default="", help="Rich text description of the term")
|
|
338
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
339
|
+
@click.option("--status", required=False, default="Draft",
|
|
340
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
341
|
+
help="Status of the term")
|
|
342
|
+
@click.option("--acronym", required=False, help="Acronyms for the term (can be specified multiple times)", multiple=True)
|
|
343
|
+
@click.option("--owner-id", required=False, help="Owner Entra ID (can be specified multiple times)", multiple=True)
|
|
344
|
+
@click.option("--resource-name", required=False, help="Resource name for additional reading")
|
|
345
|
+
@click.option("--resource-url", required=False, help="Resource URL for additional reading")
|
|
346
|
+
def create(name, description, domain_id, status, acronym, owner_id, resource_name, resource_url):
|
|
347
|
+
"""Create a new glossary term."""
|
|
348
|
+
try:
|
|
349
|
+
client = UnifiedCatalogClient()
|
|
350
|
+
|
|
351
|
+
# Build args dictionary
|
|
352
|
+
args = {
|
|
353
|
+
"--name": [name],
|
|
354
|
+
"--description": [description],
|
|
355
|
+
"--governance-domain-id": [domain_id],
|
|
356
|
+
"--status": [status]
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if acronym:
|
|
360
|
+
args["--acronyms"] = list(acronym)
|
|
361
|
+
if owner_id:
|
|
362
|
+
args["--owner-id"] = list(owner_id)
|
|
363
|
+
if resource_name and resource_url:
|
|
364
|
+
args["--resource-name"] = [resource_name]
|
|
365
|
+
args["--resource-url"] = [resource_url]
|
|
366
|
+
|
|
367
|
+
result = client.create_term(args)
|
|
368
|
+
|
|
369
|
+
if not result:
|
|
370
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
371
|
+
return
|
|
372
|
+
if isinstance(result, dict) and "error" in result:
|
|
373
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
console.print(f"[green]✅ SUCCESS:[/green] Created glossary term '{name}'")
|
|
377
|
+
console.print(json.dumps(result, indent=2))
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@term.command(name="list")
|
|
384
|
+
@click.option("--domain-id", required=True, help="Governance domain ID to list terms from")
|
|
385
|
+
def list_terms(domain_id):
|
|
386
|
+
"""List all glossary terms in a governance domain."""
|
|
387
|
+
try:
|
|
388
|
+
client = UnifiedCatalogClient()
|
|
389
|
+
args = {"--governance-domain-id": [domain_id]}
|
|
390
|
+
result = client.get_terms(args)
|
|
391
|
+
|
|
392
|
+
if not result:
|
|
393
|
+
console.print("[yellow]No glossary terms found.[/yellow]")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# Handle both list and dict responses
|
|
397
|
+
if isinstance(result, (list, tuple)):
|
|
398
|
+
terms = result
|
|
399
|
+
elif isinstance(result, dict):
|
|
400
|
+
terms = result.get("value", [])
|
|
401
|
+
else:
|
|
402
|
+
terms = []
|
|
403
|
+
|
|
404
|
+
if not terms:
|
|
405
|
+
console.print("[yellow]No glossary terms found.[/yellow]")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
table = Table(title="Glossary Terms")
|
|
409
|
+
table.add_column("ID", style="cyan")
|
|
410
|
+
table.add_column("Name", style="green")
|
|
411
|
+
table.add_column("Status", style="yellow")
|
|
412
|
+
table.add_column("Acronyms", style="blue")
|
|
413
|
+
table.add_column("Description", style="white")
|
|
414
|
+
|
|
415
|
+
for term in terms:
|
|
416
|
+
acronyms = ", ".join(term.get("acronyms", []))
|
|
417
|
+
desc = term.get("description", "")
|
|
418
|
+
if len(desc) > 50:
|
|
419
|
+
desc = desc[:50] + "..."
|
|
420
|
+
|
|
421
|
+
table.add_row(
|
|
422
|
+
term.get("id", "N/A"),
|
|
423
|
+
term.get("name", "N/A"),
|
|
424
|
+
term.get("status", "N/A"),
|
|
425
|
+
acronyms or "None",
|
|
426
|
+
desc
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
console.print(table)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@term.command()
|
|
436
|
+
@click.option("--term-id", required=True, help="ID of the glossary term")
|
|
437
|
+
def show(term_id):
|
|
438
|
+
"""Show details of a glossary term."""
|
|
439
|
+
try:
|
|
440
|
+
client = UnifiedCatalogClient()
|
|
441
|
+
args = {"--term-id": [term_id]}
|
|
442
|
+
result = client.get_term_by_id(args)
|
|
443
|
+
|
|
444
|
+
if not result:
|
|
445
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
446
|
+
return
|
|
447
|
+
if isinstance(result, dict) and "error" in result:
|
|
448
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Term not found')}")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
console.print(json.dumps(result, indent=2))
|
|
452
|
+
|
|
453
|
+
except Exception as e:
|
|
454
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ========================================
|
|
458
|
+
# OBJECTIVES AND KEY RESULTS (OKRs)
|
|
459
|
+
# ========================================
|
|
460
|
+
|
|
461
|
+
@uc.group()
|
|
462
|
+
def objective():
|
|
463
|
+
"""Manage objectives and key results (OKRs)."""
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@objective.command()
|
|
468
|
+
@click.option("--definition", required=True, help="Definition of the objective")
|
|
469
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
470
|
+
@click.option("--status", required=False, default="Draft",
|
|
471
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
472
|
+
help="Status of the objective")
|
|
473
|
+
@click.option("--owner-id", required=False, help="Owner Entra ID (can be specified multiple times)", multiple=True)
|
|
474
|
+
@click.option("--target-date", required=False, help="Target date (ISO format: 2025-12-30T14:00:00.000Z)")
|
|
475
|
+
def create(definition, domain_id, status, owner_id, target_date):
|
|
476
|
+
"""Create a new objective."""
|
|
477
|
+
try:
|
|
478
|
+
client = UnifiedCatalogClient()
|
|
479
|
+
|
|
480
|
+
args = {
|
|
481
|
+
"--definition": [definition],
|
|
482
|
+
"--governance-domain-id": [domain_id],
|
|
483
|
+
"--status": [status]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if owner_id:
|
|
487
|
+
args["--owner-id"] = list(owner_id)
|
|
488
|
+
if target_date:
|
|
489
|
+
args["--target-date"] = [target_date]
|
|
490
|
+
|
|
491
|
+
result = client.create_objective(args)
|
|
492
|
+
|
|
493
|
+
if not result:
|
|
494
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
495
|
+
return
|
|
496
|
+
if isinstance(result, dict) and "error" in result:
|
|
497
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
console.print(f"[green]✅ SUCCESS:[/green] Created objective")
|
|
501
|
+
console.print(json.dumps(result, indent=2))
|
|
502
|
+
|
|
503
|
+
except Exception as e:
|
|
504
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@objective.command(name="list")
|
|
508
|
+
@click.option("--domain-id", required=True, help="Governance domain ID to list objectives from")
|
|
509
|
+
def list_objectives(domain_id):
|
|
510
|
+
"""List all objectives in a governance domain."""
|
|
511
|
+
try:
|
|
512
|
+
client = UnifiedCatalogClient()
|
|
513
|
+
args = {"--governance-domain-id": [domain_id]}
|
|
514
|
+
result = client.get_objectives(args)
|
|
515
|
+
|
|
516
|
+
if not result:
|
|
517
|
+
console.print("[yellow]No objectives found.[/yellow]")
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
# Handle response format
|
|
521
|
+
if isinstance(result, (list, tuple)):
|
|
522
|
+
objectives = result
|
|
523
|
+
elif isinstance(result, dict):
|
|
524
|
+
objectives = result.get("value", [])
|
|
525
|
+
else:
|
|
526
|
+
objectives = []
|
|
527
|
+
|
|
528
|
+
if not objectives:
|
|
529
|
+
console.print("[yellow]No objectives found.[/yellow]")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
table = Table(title="Objectives")
|
|
533
|
+
table.add_column("ID", style="cyan")
|
|
534
|
+
table.add_column("Definition", style="green")
|
|
535
|
+
table.add_column("Status", style="yellow")
|
|
536
|
+
table.add_column("Target Date", style="blue")
|
|
537
|
+
|
|
538
|
+
for obj in objectives:
|
|
539
|
+
definition = obj.get("definition", "")
|
|
540
|
+
if len(definition) > 50:
|
|
541
|
+
definition = definition[:50] + "..."
|
|
542
|
+
|
|
543
|
+
table.add_row(
|
|
544
|
+
obj.get("id", "N/A"),
|
|
545
|
+
definition,
|
|
546
|
+
obj.get("status", "N/A"),
|
|
547
|
+
obj.get("targetDate", "N/A")
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
console.print(table)
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@objective.command()
|
|
557
|
+
@click.option("--objective-id", required=True, help="ID of the objective")
|
|
558
|
+
def show(objective_id):
|
|
559
|
+
"""Show details of an objective."""
|
|
560
|
+
try:
|
|
561
|
+
client = UnifiedCatalogClient()
|
|
562
|
+
args = {"--objective-id": [objective_id]}
|
|
563
|
+
result = client.get_objective_by_id(args)
|
|
564
|
+
|
|
565
|
+
if not result:
|
|
566
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
567
|
+
return
|
|
568
|
+
if isinstance(result, dict) and "error" in result:
|
|
569
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Objective not found')}")
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
console.print(json.dumps(result, indent=2))
|
|
573
|
+
|
|
574
|
+
except Exception as e:
|
|
575
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ========================================
|
|
579
|
+
# CRITICAL DATA ELEMENTS (CDEs)
|
|
580
|
+
# ========================================
|
|
581
|
+
|
|
582
|
+
@uc.group()
|
|
583
|
+
def cde():
|
|
584
|
+
"""Manage critical data elements."""
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@cde.command()
|
|
589
|
+
@click.option("--name", required=True, help="Name of the critical data element")
|
|
590
|
+
@click.option("--description", required=False, default="", help="Description of the CDE")
|
|
591
|
+
@click.option("--domain-id", required=True, help="Governance domain ID")
|
|
592
|
+
@click.option("--data-type", required=True,
|
|
593
|
+
type=click.Choice(["String", "Number", "Boolean", "Date", "DateTime"]),
|
|
594
|
+
help="Data type of the CDE")
|
|
595
|
+
@click.option("--status", required=False, default="Draft",
|
|
596
|
+
type=click.Choice(["Draft", "Published", "Archived"]),
|
|
597
|
+
help="Status of the CDE")
|
|
598
|
+
@click.option("--owner-id", required=False, help="Owner Entra ID (can be specified multiple times)", multiple=True)
|
|
599
|
+
def create(name, description, domain_id, data_type, status, owner_id):
|
|
600
|
+
"""Create a new critical data element."""
|
|
601
|
+
try:
|
|
602
|
+
client = UnifiedCatalogClient()
|
|
603
|
+
|
|
604
|
+
args = {
|
|
605
|
+
"--name": [name],
|
|
606
|
+
"--description": [description],
|
|
607
|
+
"--governance-domain-id": [domain_id],
|
|
608
|
+
"--data-type": [data_type],
|
|
609
|
+
"--status": [status]
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if owner_id:
|
|
613
|
+
args["--owner-id"] = list(owner_id)
|
|
614
|
+
|
|
615
|
+
result = client.create_critical_data_element(args)
|
|
616
|
+
|
|
617
|
+
if not result:
|
|
618
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
619
|
+
return
|
|
620
|
+
if isinstance(result, dict) and "error" in result:
|
|
621
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'Unknown error')}")
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
console.print(f"[green]✅ SUCCESS:[/green] Created critical data element '{name}'")
|
|
625
|
+
console.print(json.dumps(result, indent=2))
|
|
626
|
+
|
|
627
|
+
except Exception as e:
|
|
628
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
@cde.command(name="list")
|
|
632
|
+
@click.option("--domain-id", required=True, help="Governance domain ID to list CDEs from")
|
|
633
|
+
def list_cdes(domain_id):
|
|
634
|
+
"""List all critical data elements in a governance domain."""
|
|
635
|
+
try:
|
|
636
|
+
client = UnifiedCatalogClient()
|
|
637
|
+
args = {"--governance-domain-id": [domain_id]}
|
|
638
|
+
result = client.get_critical_data_elements(args)
|
|
639
|
+
|
|
640
|
+
if not result:
|
|
641
|
+
console.print("[yellow]No critical data elements found.[/yellow]")
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
# Handle response format
|
|
645
|
+
if isinstance(result, (list, tuple)):
|
|
646
|
+
cdes = result
|
|
647
|
+
elif isinstance(result, dict):
|
|
648
|
+
cdes = result.get("value", [])
|
|
649
|
+
else:
|
|
650
|
+
cdes = []
|
|
651
|
+
|
|
652
|
+
if not cdes:
|
|
653
|
+
console.print("[yellow]No critical data elements found.[/yellow]")
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
table = Table(title="Critical Data Elements")
|
|
657
|
+
table.add_column("ID", style="cyan")
|
|
658
|
+
table.add_column("Name", style="green")
|
|
659
|
+
table.add_column("Data Type", style="blue")
|
|
660
|
+
table.add_column("Status", style="yellow")
|
|
661
|
+
table.add_column("Description", style="white")
|
|
662
|
+
|
|
663
|
+
for cde_item in cdes:
|
|
664
|
+
desc = cde_item.get("description", "")
|
|
665
|
+
if len(desc) > 30:
|
|
666
|
+
desc = desc[:30] + "..."
|
|
667
|
+
|
|
668
|
+
table.add_row(
|
|
669
|
+
cde_item.get("id", "N/A"),
|
|
670
|
+
cde_item.get("name", "N/A"),
|
|
671
|
+
cde_item.get("dataType", "N/A"),
|
|
672
|
+
cde_item.get("status", "N/A"),
|
|
673
|
+
desc
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
console.print(table)
|
|
677
|
+
|
|
678
|
+
except Exception as e:
|
|
679
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@cde.command()
|
|
683
|
+
@click.option("--cde-id", required=True, help="ID of the critical data element")
|
|
684
|
+
def show(cde_id):
|
|
685
|
+
"""Show details of a critical data element."""
|
|
686
|
+
try:
|
|
687
|
+
client = UnifiedCatalogClient()
|
|
688
|
+
args = {"--cde-id": [cde_id]}
|
|
689
|
+
result = client.get_critical_data_element_by_id(args)
|
|
690
|
+
|
|
691
|
+
if not result:
|
|
692
|
+
console.print("[red]ERROR:[/red] No response received")
|
|
693
|
+
return
|
|
694
|
+
if isinstance(result, dict) and "error" in result:
|
|
695
|
+
console.print(f"[red]ERROR:[/red] {result.get('error', 'CDE not found')}")
|
|
696
|
+
return
|
|
697
|
+
|
|
698
|
+
console.print(json.dumps(result, indent=2))
|
|
699
|
+
|
|
700
|
+
except Exception as e:
|
|
701
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ========================================
|
|
705
|
+
# HEALTH MANAGEMENT (Preview)
|
|
706
|
+
# ========================================
|
|
707
|
+
|
|
708
|
+
@uc.group()
|
|
709
|
+
def health():
|
|
710
|
+
"""Manage health controls and actions (preview)."""
|
|
711
|
+
pass
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
@health.command(name="controls")
|
|
715
|
+
def list_controls():
|
|
716
|
+
"""List health controls (preview - not yet implemented)."""
|
|
717
|
+
console.print("[yellow]🚧 Health Controls are not yet implemented in the API[/yellow]")
|
|
718
|
+
console.print("This feature is coming soon to Microsoft Purview Unified Catalog")
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@health.command(name="actions")
|
|
722
|
+
def list_actions():
|
|
723
|
+
"""List health actions (preview - not yet implemented)."""
|
|
724
|
+
console.print("[yellow]🚧 Health Actions are not yet implemented in the API[/yellow]")
|
|
725
|
+
console.print("This feature is coming soon to Microsoft Purview Unified Catalog")
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@health.command(name="quality")
|
|
729
|
+
def data_quality():
|
|
730
|
+
"""Data quality management (not yet supported by API)."""
|
|
731
|
+
console.print("[yellow]🚧 Data Quality management is not yet supported by the API[/yellow]")
|
|
732
|
+
console.print("This feature requires complex API interactions not yet available")
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# ========================================
|
|
736
|
+
# CUSTOM ATTRIBUTES (Coming Soon)
|
|
737
|
+
# ========================================
|
|
738
|
+
|
|
739
|
+
@uc.group()
|
|
740
|
+
def attribute():
|
|
741
|
+
"""Manage custom attributes (coming soon)."""
|
|
742
|
+
pass
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@attribute.command(name="list")
|
|
746
|
+
def list_attributes():
|
|
747
|
+
"""List custom attributes (coming soon)."""
|
|
748
|
+
console.print("[yellow]🚧 Custom Attributes are coming soon[/yellow]")
|
|
749
|
+
console.print("This feature is under development in Microsoft Purview")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# ========================================
|
|
753
|
+
# REQUESTS (Coming Soon)
|
|
754
|
+
# ========================================
|
|
755
|
+
|
|
756
|
+
@uc.group()
|
|
757
|
+
def request():
|
|
758
|
+
"""Manage access requests (coming soon)."""
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@request.command(name="list")
|
|
763
|
+
def list_requests():
|
|
764
|
+
"""List access requests (coming soon)."""
|
|
765
|
+
console.print("[yellow]🚧 Access Requests are coming soon[/yellow]")
|
|
766
|
+
console.print("This feature is under development for data access workflows")
|