pvw-cli 1.2.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.

Files changed (60) hide show
  1. purviewcli/__init__.py +27 -0
  2. purviewcli/__main__.py +15 -0
  3. purviewcli/cli/__init__.py +5 -0
  4. purviewcli/cli/account.py +199 -0
  5. purviewcli/cli/cli.py +170 -0
  6. purviewcli/cli/collections.py +502 -0
  7. purviewcli/cli/domain.py +361 -0
  8. purviewcli/cli/entity.py +2436 -0
  9. purviewcli/cli/glossary.py +533 -0
  10. purviewcli/cli/health.py +250 -0
  11. purviewcli/cli/insight.py +113 -0
  12. purviewcli/cli/lineage.py +1103 -0
  13. purviewcli/cli/management.py +141 -0
  14. purviewcli/cli/policystore.py +103 -0
  15. purviewcli/cli/relationship.py +75 -0
  16. purviewcli/cli/scan.py +357 -0
  17. purviewcli/cli/search.py +527 -0
  18. purviewcli/cli/share.py +478 -0
  19. purviewcli/cli/types.py +831 -0
  20. purviewcli/cli/unified_catalog.py +3540 -0
  21. purviewcli/cli/workflow.py +402 -0
  22. purviewcli/client/__init__.py +21 -0
  23. purviewcli/client/_account.py +1877 -0
  24. purviewcli/client/_collections.py +1761 -0
  25. purviewcli/client/_domain.py +414 -0
  26. purviewcli/client/_entity.py +3545 -0
  27. purviewcli/client/_glossary.py +3233 -0
  28. purviewcli/client/_health.py +501 -0
  29. purviewcli/client/_insight.py +2873 -0
  30. purviewcli/client/_lineage.py +2138 -0
  31. purviewcli/client/_management.py +2202 -0
  32. purviewcli/client/_policystore.py +2915 -0
  33. purviewcli/client/_relationship.py +1351 -0
  34. purviewcli/client/_scan.py +2607 -0
  35. purviewcli/client/_search.py +1472 -0
  36. purviewcli/client/_share.py +272 -0
  37. purviewcli/client/_types.py +2708 -0
  38. purviewcli/client/_unified_catalog.py +5112 -0
  39. purviewcli/client/_workflow.py +2734 -0
  40. purviewcli/client/api_client.py +1295 -0
  41. purviewcli/client/business_rules.py +675 -0
  42. purviewcli/client/config.py +231 -0
  43. purviewcli/client/data_quality.py +433 -0
  44. purviewcli/client/endpoint.py +123 -0
  45. purviewcli/client/endpoints.py +554 -0
  46. purviewcli/client/exceptions.py +38 -0
  47. purviewcli/client/lineage_visualization.py +797 -0
  48. purviewcli/client/monitoring_dashboard.py +712 -0
  49. purviewcli/client/rate_limiter.py +30 -0
  50. purviewcli/client/retry_handler.py +125 -0
  51. purviewcli/client/scanning_operations.py +523 -0
  52. purviewcli/client/settings.py +1 -0
  53. purviewcli/client/sync_client.py +250 -0
  54. purviewcli/plugins/__init__.py +1 -0
  55. purviewcli/plugins/plugin_system.py +709 -0
  56. pvw_cli-1.2.8.dist-info/METADATA +1618 -0
  57. pvw_cli-1.2.8.dist-info/RECORD +60 -0
  58. pvw_cli-1.2.8.dist-info/WHEEL +5 -0
  59. pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
  60. pvw_cli-1.2.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,831 @@
1
+ """
2
+ usage:
3
+ pvw types createTypeDefs --payloadFile=<val>
4
+ pvw types deleteTypeDef --name=<val>
5
+ pvw types deleteTypeDefs --payloadFile=<val>
6
+ pvw types putTypeDefs --payloadFile=<val>
7
+ pvw types readClassificationDef (--guid=<val> | --name=<val>)
8
+ pvw types readEntityDef (--guid=<val> | --name=<val>)
9
+ pvw types readEnumDef (--guid=<val> | --name=<val>)
10
+ pvw types readRelationshipDef (--guid=<val> | --name=<val>)
11
+ pvw types readStatistics
12
+ pvw types readStructDef (--guid=<val> | --name=<val>)
13
+ pvw types readBusinessMetadataDef (--guid=<val> | --name=<val>)
14
+ pvw types readTermTemplateDef (--guid=<val> | --name=<val>)
15
+ pvw types readTypeDef (--guid=<val> | --name=<val>)
16
+ pvw types readTypeDefs [--includeTermTemplate --type=<val>]
17
+ pvw types readTypeDefsHeaders [--includeTermTemplate --type=<val>]
18
+
19
+ options:
20
+ --purviewName=<val> [string] Microsoft Purview account name.
21
+ --guid=<val> [string] The globally unique identifier.
22
+ --includeTermTemplate [boolean] Whether to include termtemplatedef [default: false].
23
+ --name=<val> [string] The name of the definition.
24
+ --payloadFile=<val> [string] File path to a valid JSON document.
25
+ --type=<val> [string] Typedef name as search filter (classification | entity | enum | relationship | struct).
26
+
27
+ Advanced Workflows & API Mapping:
28
+ ---------------------------------
29
+ - Bulk Operations: Use `create_typedefs`, `put_typedefs`, and `delete_typedefs` to manage multiple type definitions at once via JSON files. These map to Atlas v2 Data Map API bulk endpoints (typesCreateTypeDefs, typesPutTypeDefs, typesDeleteTypeDefs).
30
+ - Per-Type Reads: Use `read_classification_def`, `read_entity_def`, `read_enum_def`, `read_relationship_def`, `read_struct_def`, `read_business_metadata_def`, `read_term_template_def` for fine-grained inspection of type definitions. These map to Atlas v2 endpoints for each type.
31
+ - Filtering: Use `read_typedefs` and `read_typedefs_headers` with `--type` and `--include-term-template` to filter results, mapping to Atlas v2's flexible type listing APIs.
32
+ - Statistics: Use `read_statistics` to get a summary of type system state (maps to typesReadStatistics).
33
+ - Error Handling: For bulk operations, errors are reported in the CLI output. For advanced error reporting (e.g., failed items to file), see future roadmap.
34
+ - API Coverage: This CLI covers all read operations and bulk create/update/delete. For per-type create/update/delete, use JSON payloads with the bulk endpoints. For advanced features (versioning, validation, dry-run), monitor API updates and CLI roadmap.
35
+
36
+ Examples:
37
+ ---------
38
+ # Bulk create/update type definitions from a JSON file
39
+ pvw types createTypeDefs --payloadFile=types.json
40
+
41
+ # Delete a single type definition by name
42
+ pvw types deleteTypeDef --name=MyEntityType
43
+
44
+ # Read all entity type definitions
45
+ pvw types readTypeDefs --type=entity
46
+
47
+ # Read a classification definition by GUID
48
+ pvw types readClassificationDef --guid=1234-5678
49
+
50
+ # Read type system statistics
51
+ pvw types readStatistics
52
+
53
+ For more advanced examples and templates, see the documentation in `doc/commands/types/` and sample JSON in `samples/json/`.
54
+ """
55
+
56
+ import json
57
+ import click
58
+ from purviewcli.client._types import Types
59
+
60
+ @click.group()
61
+ def types():
62
+ """Manage types (schemas, entity types, relationship types, etc.)"""
63
+ pass
64
+
65
+ @types.command()
66
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
67
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
68
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
69
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
70
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
71
+ def create_typedefs(payload_file, dry_run, validate, output_file, error_file):
72
+ """Create type definitions from a JSON file"""
73
+ try:
74
+ with open(payload_file, 'r', encoding='utf-8') as f:
75
+ payload = json.load(f)
76
+ if validate:
77
+ click.echo('[VALIDATION] Payload is valid JSON.')
78
+ if dry_run:
79
+ click.echo('[DRY-RUN] Would send the following payload:')
80
+ click.echo(json.dumps(payload, indent=2))
81
+ return
82
+ args = {'--payloadFile': payload_file}
83
+ client = Types()
84
+ result = client.typesCreateTypeDefs(args)
85
+ if output_file:
86
+ with open(output_file, 'w', encoding='utf-8') as outf:
87
+ outf.write(json.dumps(result, indent=2))
88
+ else:
89
+ click.echo(json.dumps(result, indent=2))
90
+ except Exception as e:
91
+ if error_file:
92
+ with open(error_file, 'w', encoding='utf-8') as errf:
93
+ errf.write(str(e))
94
+ else:
95
+ click.echo(f"Error: {e}")
96
+
97
+ @types.command()
98
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
99
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
100
+ @click.option('--name', required=True, help='Name of the type definition to delete')
101
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
102
+ def delete_typedef(name, dry_run, output_file, error_file):
103
+ """Delete a type definition by name"""
104
+ try:
105
+ if dry_run:
106
+ click.echo(f'[DRY-RUN] Would delete type definition with name: {name}')
107
+ return
108
+ args = {'--name': name}
109
+ client = Types()
110
+ result = client.typesDeleteTypeDef(args)
111
+ if output_file:
112
+ with open(output_file, 'w', encoding='utf-8') as outf:
113
+ outf.write(json.dumps(result, indent=2))
114
+ else:
115
+ click.echo(json.dumps(result, indent=2))
116
+ except Exception as e:
117
+ if error_file:
118
+ with open(error_file, 'w', encoding='utf-8') as errf:
119
+ errf.write(str(e))
120
+ else:
121
+ click.echo(f"Error: {e}")
122
+
123
+ @types.command()
124
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
125
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
126
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
127
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
128
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
129
+ def put_typedefs(payload_file, dry_run, validate, output_file, error_file):
130
+ """Update or create type definitions from a JSON file"""
131
+ try:
132
+ with open(payload_file, 'r', encoding='utf-8') as f:
133
+ payload = json.load(f)
134
+ if validate:
135
+ click.echo('[VALIDATION] Payload is valid JSON.')
136
+ if dry_run:
137
+ click.echo('[DRY-RUN] Would send the following payload:')
138
+ click.echo(json.dumps(payload, indent=2))
139
+ return
140
+ args = {'--payloadFile': payload_file}
141
+ client = Types()
142
+ result = client.typesPutTypeDefs(args)
143
+ if output_file:
144
+ with open(output_file, 'w', encoding='utf-8') as outf:
145
+ outf.write(json.dumps(result, indent=2))
146
+ else:
147
+ click.echo(json.dumps(result, indent=2))
148
+ except Exception as e:
149
+ if error_file:
150
+ with open(error_file, 'w', encoding='utf-8') as errf:
151
+ errf.write(str(e))
152
+ else:
153
+ click.echo(f"Error: {e}")
154
+
155
+ @types.command()
156
+ @click.option('--guid', required=False, help='The globally unique identifier')
157
+ @click.option('--name', required=False, help='The name of the classification definition')
158
+ def read_classification_def(guid, name):
159
+ """Read a classification definition by GUID or name"""
160
+ try:
161
+ args = {'--guid': guid, '--name': name}
162
+ client = Types()
163
+ result = client.typesReadClassificationDef(args)
164
+ click.echo(json.dumps(result, indent=2))
165
+ except Exception as e:
166
+ click.echo(f"Error: {e}")
167
+
168
+ @types.command()
169
+ @click.option('--guid', required=False, help='The globally unique identifier')
170
+ @click.option('--name', required=False, help='The name of the entity definition')
171
+ def read_entity_def(guid, name):
172
+ """Read an entity definition by GUID or name"""
173
+ try:
174
+ args = {'--guid': guid, '--name': name}
175
+ client = Types()
176
+ result = client.typesReadEntityDef(args)
177
+ click.echo(json.dumps(result, indent=2))
178
+ except Exception as e:
179
+ click.echo(f"Error: {e}")
180
+
181
+ @types.command()
182
+ @click.option('--guid', required=False, help='The globally unique identifier')
183
+ @click.option('--name', required=False, help='The name of the enum definition')
184
+ def read_enum_def(guid, name):
185
+ """Read an enum definition by GUID or name"""
186
+ try:
187
+ args = {'--guid': guid, '--name': name}
188
+ client = Types()
189
+ result = client.typesReadEnumDef(args)
190
+ click.echo(json.dumps(result, indent=2))
191
+ except Exception as e:
192
+ click.echo(f"Error: {e}")
193
+
194
+ @types.command()
195
+ @click.option('--guid', required=False, help='The globally unique identifier')
196
+ @click.option('--name', required=False, help='The name of the relationship definition')
197
+ def read_relationship_def(guid, name):
198
+ """Read a relationship definition by GUID or name"""
199
+ try:
200
+ args = {'--guid': guid, '--name': name}
201
+ client = Types()
202
+ result = client.typesReadRelationshipDef(args)
203
+ click.echo(json.dumps(result, indent=2))
204
+ except Exception as e:
205
+ click.echo(f"Error: {e}")
206
+
207
+ @types.command()
208
+ def read_statistics():
209
+ """Read type statistics"""
210
+ try:
211
+ args = {}
212
+ client = Types()
213
+ result = client.typesReadStatistics(args)
214
+ click.echo(json.dumps(result, indent=2))
215
+ except Exception as e:
216
+ click.echo(f"Error: {e}")
217
+
218
+ @types.command()
219
+ @click.option('--guid', required=False, help='The globally unique identifier')
220
+ @click.option('--name', required=False, help='The name of the struct definition')
221
+ def read_struct_def(guid, name):
222
+ """Read a struct definition by GUID or name"""
223
+ try:
224
+ args = {'--guid': guid, '--name': name}
225
+ client = Types()
226
+ result = client.typesReadStructDef(args)
227
+ click.echo(json.dumps(result, indent=2))
228
+ except Exception as e:
229
+ click.echo(f"Error: {e}")
230
+
231
+ @types.command()
232
+ @click.option('--guid', required=False, help='The globally unique identifier')
233
+ @click.option('--name', required=False, help='The name of the business metadata definition')
234
+ def read_business_metadata_def(guid, name):
235
+ """Read a business metadata definition by GUID or name"""
236
+ try:
237
+ args = {'--guid': guid, '--name': name}
238
+ client = Types()
239
+ result = client.typesReadBusinessMetadataDef(args)
240
+ click.echo(json.dumps(result, indent=2))
241
+ except Exception as e:
242
+ click.echo(f"Error: {e}")
243
+
244
+ @types.command()
245
+ @click.option('--guid', required=False, help='The globally unique identifier')
246
+ @click.option('--name', required=False, help='The name of the term template definition')
247
+ def read_term_template_def(guid, name):
248
+ """Read a term template definition by GUID or name"""
249
+ try:
250
+ args = {'--guid': guid, '--name': name}
251
+ client = Types()
252
+ result = client.typesReadTermTemplateDef(args)
253
+ click.echo(json.dumps(result, indent=2))
254
+ except Exception as e:
255
+ click.echo(f"Error: {e}")
256
+
257
+ @types.command()
258
+ @click.option('--guid', required=False, help='The globally unique identifier')
259
+ @click.option('--name', required=False, help='The name of the type definition')
260
+ def read_typedef(guid, name):
261
+ """Read a type definition by GUID or name"""
262
+ try:
263
+ args = {'--guid': guid, '--name': name}
264
+ client = Types()
265
+ result = client.typesReadTypeDef(args)
266
+ click.echo(json.dumps(result, indent=2))
267
+ except Exception as e:
268
+ click.echo(f"Error: {e}")
269
+
270
+ @types.command()
271
+ @click.option('--include-term-template', is_flag=True, default=False, help='Include term template definitions')
272
+ @click.option('--type', 'type_', required=False, help='Typedef name as search filter (classification | entity | enum | relationship | struct)')
273
+ def read_typedefs(include_term_template, type_):
274
+ """Read all type definitions, optionally filtered by type or including term templates"""
275
+ try:
276
+ args = {'--includeTermTemplate': include_term_template, '--type': type_}
277
+ client = Types()
278
+ result = client.typesRead(args)
279
+ click.echo(json.dumps(result, indent=2))
280
+ except Exception as e:
281
+ click.echo(f"Error: {e}")
282
+
283
+ @types.command()
284
+ @click.option('--include-term-template', is_flag=True, default=False, help='Include term template definitions')
285
+ @click.option('--type', 'type_', required=False, help='Typedef name as search filter (classification | entity | enum | relationship | struct)')
286
+ def read_typedefs_headers(include_term_template, type_):
287
+ """Read type definition headers, optionally filtered by type or including term templates"""
288
+ try:
289
+ args = {'--includeTermTemplate': include_term_template, '--type': type_}
290
+ client = Types()
291
+ result = client.typesReadHeaders(args)
292
+ click.echo(json.dumps(result, indent=2))
293
+ except Exception as e:
294
+ click.echo(f"Error: {e}")
295
+
296
+ @types.command()
297
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
298
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
299
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
300
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
301
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
302
+ def create_business_metadata_def(payload_file, dry_run, validate, output_file, error_file):
303
+ """Create business metadata definition from a JSON file"""
304
+ try:
305
+ with open(payload_file, 'r', encoding='utf-8') as f:
306
+ payload = json.load(f)
307
+ if validate:
308
+ click.echo('[VALIDATION] Payload is valid JSON.')
309
+ # Optionally, add more schema validation here
310
+ if dry_run:
311
+ click.echo('[DRY-RUN] Would send the following payload:')
312
+ click.echo(json.dumps(payload, indent=2))
313
+ return
314
+ args = {'--payloadFile': payload_file}
315
+ client = Types()
316
+ result = client.createBusinessMetadataDef(args)
317
+ if output_file:
318
+ with open(output_file, 'w', encoding='utf-8') as outf:
319
+ outf.write(json.dumps(result, indent=2))
320
+ else:
321
+ click.echo(json.dumps(result, indent=2))
322
+ except Exception as e:
323
+ if error_file:
324
+ with open(error_file, 'w', encoding='utf-8') as errf:
325
+ errf.write(str(e))
326
+ else:
327
+ click.echo(f"Error: {e}")
328
+
329
+ @types.command()
330
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
331
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
332
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
333
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
334
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
335
+ def update_business_metadata_def(payload_file, dry_run, validate, output_file, error_file):
336
+ """Update business metadata definition from a JSON file"""
337
+ try:
338
+ with open(payload_file, 'r', encoding='utf-8') as f:
339
+ payload = json.load(f)
340
+ if validate:
341
+ click.echo('[VALIDATION] Payload is valid JSON.')
342
+ # Optionally, add more schema validation here
343
+ if dry_run:
344
+ click.echo('[DRY-RUN] Would send the following payload:')
345
+ click.echo(json.dumps(payload, indent=2))
346
+ return
347
+ args = {'--payloadFile': payload_file}
348
+ client = Types()
349
+ result = client.updateBusinessMetadataDef(args)
350
+ if output_file:
351
+ with open(output_file, 'w', encoding='utf-8') as outf:
352
+ outf.write(json.dumps(result, indent=2))
353
+ else:
354
+ click.echo(json.dumps(result, indent=2))
355
+ except Exception as e:
356
+ if error_file:
357
+ with open(error_file, 'w', encoding='utf-8') as errf:
358
+ errf.write(str(e))
359
+ else:
360
+ click.echo(f"Error: {e}")
361
+
362
+ @types.command()
363
+ @click.option('--name', required=True, help='Name of the business metadata definition to delete')
364
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
365
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
366
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
367
+ def delete_business_metadata_def(name, dry_run, output_file, error_file):
368
+ """Delete a business metadata definition by name"""
369
+ try:
370
+ if dry_run:
371
+ click.echo(f'[DRY-RUN] Would delete business metadata definition with name: {name}')
372
+ return
373
+ args = {'--name': name}
374
+ client = Types()
375
+ result = client.deleteBusinessMetadataDef(args)
376
+ if output_file:
377
+ with open(output_file, 'w', encoding='utf-8') as outf:
378
+ outf.write(json.dumps(result, indent=2))
379
+ else:
380
+ click.echo(json.dumps(result, indent=2))
381
+ except Exception as e:
382
+ if error_file:
383
+ with open(error_file, 'w', encoding='utf-8') as errf:
384
+ errf.write(str(e))
385
+ else:
386
+ click.echo(f"Error: {e}")
387
+
388
+ @types.command()
389
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
390
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
391
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
392
+ def create_term_template_def(payload_file, dry_run, validate, output_file, error_file):
393
+ """Create term template definition from a JSON file"""
394
+ try:
395
+ with open(payload_file, 'r', encoding='utf-8') as f:
396
+ payload = json.load(f)
397
+ if validate:
398
+ click.echo('[VALIDATION] Payload is valid JSON.')
399
+ # Optionally, add more schema validation here
400
+ if dry_run:
401
+ click.echo('[DRY-RUN] Would send the following payload:')
402
+ click.echo(json.dumps(payload, indent=2))
403
+ return
404
+ args = {'--payloadFile': payload_file}
405
+ client = Types()
406
+ result = client.createTermTemplateDef(args)
407
+ if output_file:
408
+ with open(output_file, 'w', encoding='utf-8') as outf:
409
+ outf.write(json.dumps(result, indent=2))
410
+ else:
411
+ click.echo(json.dumps(result, indent=2))
412
+ except Exception as e:
413
+ if error_file:
414
+ with open(error_file, 'w', encoding='utf-8') as errf:
415
+ errf.write(str(e))
416
+ else:
417
+ click.echo(f"Error: {e}")
418
+
419
+ @types.command()
420
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
421
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
422
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
423
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
424
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
425
+ def update_term_template_def(payload_file, dry_run, validate, output_file, error_file):
426
+ """Update term template definition from a JSON file"""
427
+ try:
428
+ with open(payload_file, 'r', encoding='utf-8') as f:
429
+ payload = json.load(f)
430
+ if validate:
431
+ click.echo('[VALIDATION] Payload is valid JSON.')
432
+ # Optionally, add more schema validation here
433
+ if dry_run:
434
+ click.echo('[DRY-RUN] Would send the following payload:')
435
+ click.echo(json.dumps(payload, indent=2))
436
+ return
437
+ args = {'--payloadFile': payload_file}
438
+ client = Types()
439
+ result = client.updateTermTemplateDef(args)
440
+ if output_file:
441
+ with open(output_file, 'w', encoding='utf-8') as outf:
442
+ outf.write(json.dumps(result, indent=2))
443
+ else:
444
+ click.echo(json.dumps(result, indent=2))
445
+ except Exception as e:
446
+ if error_file:
447
+ with open(error_file, 'w', encoding='utf-8') as errf:
448
+ errf.write(str(e))
449
+ else:
450
+ click.echo(f"Error: {e}")
451
+
452
+ @types.command()
453
+ @click.option('--output-file', type=click.Path(), required=False, help='Write result to this file instead of stdout')
454
+ @click.option('--error-file', type=click.Path(), required=False, help='Write errors to this file instead of stdout')
455
+ @click.option('--name', required=True, help='Name of the term template definition to delete')
456
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
457
+ def delete_term_template_def(name, dry_run, output_file, error_file):
458
+ """Delete a term template definition by name"""
459
+ try:
460
+ if dry_run:
461
+ click.echo(f'[DRY-RUN] Would delete term template definition with name: {name}')
462
+ return
463
+ args = {'--name': name}
464
+ client = Types()
465
+ result = client.deleteTermTemplateDef(args)
466
+ if output_file:
467
+ with open(output_file, 'w', encoding='utf-8') as outf:
468
+ outf.write(json.dumps(result, indent=2))
469
+ else:
470
+ click.echo(json.dumps(result, indent=2))
471
+ except Exception as e:
472
+ if error_file:
473
+ with open(error_file, 'w', encoding='utf-8') as errf:
474
+ errf.write(str(e))
475
+ else:
476
+ click.echo(f"Error: {e}")
477
+
478
+ @types.command()
479
+ @click.option('--payload-file', type=click.Path(exists=True), required=True, help='File path to a valid JSON document')
480
+ @click.option('--dry-run/--no-dry-run', default=False, help='Simulate the operation without making changes')
481
+ @click.option('--validate/--no-validate', default=False, help='Validate the payload without making changes')
482
+ def update_enum_def(payload_file, dry_run, validate):
483
+ """Update enum definition from a JSON file (example for extensibility)"""
484
+ try:
485
+ with open(payload_file, 'r', encoding='utf-8') as f:
486
+ payload = json.load(f)
487
+ if validate:
488
+ click.echo('[VALIDATION] Payload is valid JSON.')
489
+ if dry_run:
490
+ click.echo('[DRY-RUN] Would send the following payload:')
491
+ click.echo(json.dumps(payload, indent=2))
492
+ return
493
+ # args and client logic would go here
494
+ click.echo('[NOT IMPLEMENTED] This is a placeholder for extensibility.')
495
+ except Exception as e:
496
+ click.echo(f"Error: {e}")
497
+
498
+
499
+ @types.command(name="list-business-attributes")
500
+ @click.option('--output', type=click.Choice(['table', 'json']), default='table', help='Output format')
501
+ @click.option('--show-empty-groups/--hide-empty-groups', default=True, help='Show groups with no attributes')
502
+ def list_business_attributes(output, show_empty_groups):
503
+ """List all business metadata attributes (Custom metadata in Purview UI)
504
+
505
+ This command displays individual attributes organized by their parent groups,
506
+ matching what you see in the Purview "Custom metadata (preview)" interface
507
+ under "Business concept attributes" tab.
508
+
509
+ Examples:
510
+ pvw types list-business-attributes
511
+ pvw types list-business-attributes --output json
512
+ pvw types list-business-attributes --hide-empty-groups
513
+ """
514
+ from rich.console import Console
515
+ from rich.table import Table
516
+
517
+ console = Console()
518
+
519
+ try:
520
+ client = Types()
521
+ result = client.typesRead({})
522
+
523
+ if not result:
524
+ console.print("[red]ERROR:[/red] Failed to retrieve type definitions")
525
+ return
526
+
527
+ biz = result.get('businessMetadataDefs', [])
528
+
529
+ if not biz:
530
+ console.print("[yellow]No business metadata found[/yellow]")
531
+ return
532
+
533
+ if output == 'json':
534
+ # JSON output: list of attributes with their group info
535
+ attributes_list = []
536
+ for group in biz:
537
+ group_name = group.get('name', 'N/A')
538
+ group_guid = group.get('guid', 'N/A')
539
+
540
+ attributes = group.get('attributeDefs', [])
541
+ if not attributes and not show_empty_groups:
542
+ continue
543
+
544
+ for attr in attributes:
545
+ attributes_list.append({
546
+ 'attributeName': attr.get('name'),
547
+ 'group': group_name,
548
+ 'groupGuid': group_guid,
549
+ 'type': attr.get('typeName'),
550
+ 'description': attr.get('description', ''),
551
+ 'isOptional': attr.get('isOptional', True),
552
+ 'isIndexable': attr.get('isIndexable', False)
553
+ })
554
+
555
+ click.echo(json.dumps(attributes_list, indent=2))
556
+ return
557
+
558
+ # Table output
559
+ table = Table(title="[bold cyan]Business Concept Attributes (Custom Metadata)[/bold cyan]")
560
+ table.add_column("Attribute Name", style="green", no_wrap=True)
561
+ table.add_column("Group", style="cyan")
562
+ table.add_column("Type", style="yellow")
563
+ table.add_column("Description", style="white", max_width=40)
564
+ table.add_column("Scope", style="magenta")
565
+
566
+ total_attributes = 0
567
+
568
+ for group in biz:
569
+ group_name = group.get('name', 'N/A')
570
+
571
+ # Parse data governance options for scope
572
+ scope_info = "N/A"
573
+ options = group.get('options', {})
574
+ if 'dataGovernanceOptions' in options:
575
+ try:
576
+ dg_opts_str = options.get('dataGovernanceOptions', '{}')
577
+ dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
578
+ applicable = dg_opts.get('applicableConstructs', [])
579
+ if applicable:
580
+ # Extract construct types (domain, businessConcept, etc.)
581
+ scope_parts = []
582
+ for construct in applicable[:2]: # Show first 2
583
+ if ':' in construct:
584
+ scope_parts.append(construct.split(':')[0])
585
+ else:
586
+ scope_parts.append(construct)
587
+ scope_info = ', '.join(scope_parts)
588
+ if len(applicable) > 2:
589
+ scope_info += f", +{len(applicable)-2} more"
590
+ except:
591
+ pass
592
+
593
+ # List all attributes in this group
594
+ attributes = group.get('attributeDefs', [])
595
+
596
+ if attributes:
597
+ for attr in attributes:
598
+ total_attributes += 1
599
+ attr_name = attr.get('name', 'N/A')
600
+ attr_type = attr.get('typeName', 'N/A')
601
+
602
+ # Simplify enum types for display
603
+ if 'ATTRIBUTE_ENUM_' in attr_type:
604
+ attr_type = 'Enum (Single choice)'
605
+
606
+ attr_desc = attr.get('description', '')
607
+
608
+ # Get scope from attribute if it overrides group
609
+ attr_scope = scope_info
610
+ attr_opts = attr.get('options', {})
611
+ if 'dataGovernanceOptions' in attr_opts:
612
+ try:
613
+ attr_dg_str = attr_opts.get('dataGovernanceOptions', '{}')
614
+ attr_dg = json.loads(attr_dg_str) if isinstance(attr_dg_str, str) else attr_dg_str
615
+ inherit = attr_dg.get('inheritApplicableConstructsFromGroup', True)
616
+ if not inherit:
617
+ attr_applicable = attr_dg.get('applicableConstructs', [])
618
+ if attr_applicable:
619
+ attr_scope = f"{len(attr_applicable)} custom scope(s)"
620
+ except:
621
+ pass
622
+
623
+ table.add_row(
624
+ attr_name,
625
+ group_name,
626
+ attr_type,
627
+ attr_desc[:40] + "..." if len(attr_desc) > 40 else attr_desc,
628
+ attr_scope
629
+ )
630
+ elif show_empty_groups:
631
+ # Show group with no attributes
632
+ table.add_row(
633
+ f"[dim](no attributes)[/dim]",
634
+ group_name,
635
+ "-",
636
+ f"[dim]Empty group[/dim]",
637
+ scope_info
638
+ )
639
+
640
+ console.print(table)
641
+ console.print(f"\n[cyan]Total:[/cyan] {total_attributes} attribute(s) in {len(biz)} group(s)")
642
+
643
+ if total_attributes > 0:
644
+ console.print("\n[dim]Tip: Use 'pvw types read-business-metadata-def --name <GroupName>' for details[/dim]")
645
+ console.print("[dim]Tip: Use 'pvw types list-business-metadata-groups' to see group-level summary[/dim]")
646
+
647
+ except Exception as e:
648
+ console.print(f"[red]ERROR:[/red] {str(e)}")
649
+
650
+
651
+ @types.command(name="list-business-metadata-groups")
652
+ @click.option('--output', type=click.Choice(['table', 'json']), default='table', help='Output format')
653
+ def list_business_metadata_groups(output):
654
+ """List business metadata groups with their scope (Business Concept vs Data Asset).
655
+
656
+ Shows a summary view of metadata groups to distinguish which apply to:
657
+ - Business Concepts (Terms, Domains, Business Rules)
658
+ - Data Assets (Tables, Files, Databases)
659
+ - Universal (Both)
660
+
661
+ Examples:
662
+ pvw types list-business-metadata-groups
663
+ pvw types list-business-metadata-groups --output json
664
+ """
665
+ from rich.console import Console
666
+ from rich.table import Table
667
+ import json
668
+
669
+ console = Console()
670
+
671
+ try:
672
+ client = Types()
673
+ result = client.typesRead({})
674
+
675
+ if not result:
676
+ console.print("[red]ERROR:[/red] Failed to retrieve type definitions")
677
+ return
678
+
679
+ biz = result.get('businessMetadataDefs', [])
680
+
681
+ if not biz:
682
+ console.print("[yellow]No business metadata groups found[/yellow]")
683
+ return
684
+
685
+ if output == 'json':
686
+ # JSON output
687
+ groups_list = []
688
+ for group in biz:
689
+ group_name = group.get('name', 'N/A')
690
+ group_guid = group.get('guid', 'N/A')
691
+ attr_count = len(group.get('attributeDefs', []))
692
+
693
+ # Determine scope
694
+ scope = "N/A"
695
+ scope_type = "unknown"
696
+ options = group.get('options', {})
697
+
698
+ if 'dataGovernanceOptions' in options:
699
+ try:
700
+ dg_opts_str = options.get('dataGovernanceOptions', '{}')
701
+ dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
702
+ applicable = dg_opts.get('applicableConstructs', [])
703
+
704
+ if applicable:
705
+ has_business_concept = any('businessConcept' in c or 'domain' in c for c in applicable)
706
+ has_dataset = any('dataset' in c.lower() for c in applicable)
707
+
708
+ if has_business_concept and has_dataset:
709
+ scope = "Universal (Concept + Dataset)"
710
+ scope_type = "universal"
711
+ elif has_business_concept:
712
+ scope = "Business Concept"
713
+ scope_type = "business_concept"
714
+ elif has_dataset:
715
+ scope = "Data Asset"
716
+ scope_type = "data_asset"
717
+ else:
718
+ scope = ', '.join([c.split(':')[0] if ':' in c else c for c in applicable[:3]])
719
+ scope_type = "custom"
720
+ except:
721
+ pass
722
+
723
+ # Check for legacy applicableEntityTypes in attributes
724
+ if scope == "N/A":
725
+ for attr in group.get('attributeDefs', []):
726
+ attr_opts = attr.get('options', {})
727
+ if 'applicableEntityTypes' in attr_opts:
728
+ try:
729
+ entity_types_str = attr_opts.get('applicableEntityTypes', '[]')
730
+ entity_types = json.loads(entity_types_str) if isinstance(entity_types_str, str) else entity_types_str
731
+ if entity_types and isinstance(entity_types, list):
732
+ if any('table' in et.lower() or 'database' in et.lower() for et in entity_types):
733
+ scope = "Data Asset (Legacy)"
734
+ scope_type = "data_asset_legacy"
735
+ break
736
+ except:
737
+ pass
738
+
739
+ groups_list.append({
740
+ 'groupName': group_name,
741
+ 'groupGuid': group_guid,
742
+ 'scope': scope,
743
+ 'scopeType': scope_type,
744
+ 'attributeCount': attr_count,
745
+ 'description': group.get('description', '')
746
+ })
747
+
748
+ click.echo(json.dumps(groups_list, indent=2))
749
+ return
750
+
751
+ # Table output
752
+ table = Table(title="[bold cyan]Business Metadata Groups[/bold cyan]", show_header=True)
753
+ table.add_column("Group Name", style="cyan", no_wrap=True)
754
+ table.add_column("Scope", style="magenta", max_width=30)
755
+ table.add_column("Attributes", style="yellow", justify="center")
756
+ table.add_column("Description", style="white", max_width=40)
757
+
758
+ for group in biz:
759
+ group_name = group.get('name', 'N/A')
760
+ attr_count = len(group.get('attributeDefs', []))
761
+ group_desc = group.get('description', '')
762
+
763
+ # Determine scope
764
+ scope = "N/A"
765
+ scope_style = "white"
766
+ options = group.get('options', {})
767
+
768
+ if 'dataGovernanceOptions' in options:
769
+ try:
770
+ dg_opts_str = options.get('dataGovernanceOptions', '{}')
771
+ dg_opts = json.loads(dg_opts_str) if isinstance(dg_opts_str, str) else dg_opts_str
772
+ applicable = dg_opts.get('applicableConstructs', [])
773
+
774
+ if applicable:
775
+ has_business_concept = any('businessConcept' in c or 'domain' in c for c in applicable)
776
+ has_dataset = any('dataset' in c.lower() for c in applicable)
777
+
778
+ if has_business_concept and has_dataset:
779
+ scope = "Universal"
780
+ scope_style = "magenta bold"
781
+ elif has_business_concept:
782
+ scope = "Business Concept"
783
+ scope_style = "green"
784
+ elif has_dataset:
785
+ scope = "Data Asset"
786
+ scope_style = "blue"
787
+ else:
788
+ scope = ', '.join([c.split(':')[0] if ':' in c else c for c in applicable[:2]])
789
+ scope_style = "yellow"
790
+ except:
791
+ pass
792
+
793
+ # Check for legacy applicableEntityTypes
794
+ if scope == "N/A":
795
+ for attr in group.get('attributeDefs', []):
796
+ attr_opts = attr.get('options', {})
797
+ if 'applicableEntityTypes' in attr_opts:
798
+ try:
799
+ entity_types_str = attr_opts.get('applicableEntityTypes', '[]')
800
+ entity_types = json.loads(entity_types_str) if isinstance(entity_types_str, str) else entity_types_str
801
+ if entity_types and isinstance(entity_types, list):
802
+ if any('table' in et.lower() or 'database' in et.lower() for et in entity_types):
803
+ scope = "Data Asset (Legacy)"
804
+ scope_style = "blue dim"
805
+ break
806
+ except:
807
+ pass
808
+
809
+ table.add_row(
810
+ group_name,
811
+ f"[{scope_style}]{scope}[/{scope_style}]",
812
+ str(attr_count),
813
+ group_desc[:40] + "..." if len(group_desc) > 40 else group_desc
814
+ )
815
+
816
+ console.print(table)
817
+ console.print(f"\n[cyan]Total:[/cyan] {len(biz)} group(s)")
818
+
819
+ console.print("\n[bold]Legend:[/bold]")
820
+ console.print(" [green]Business Concept[/green] = Applies to Glossary Terms, Domains, Business Rules")
821
+ console.print(" [blue]Data Asset[/blue] = Applies to Tables, Files, Databases, etc.")
822
+ console.print(" [magenta bold]Universal[/magenta bold] = Applies to both Concepts and Assets")
823
+
824
+ console.print("\n[dim]Tip: Use 'pvw types list-business-attributes' to see individual attributes[/dim]")
825
+ console.print("[dim]Tip: Use 'pvw types read-business-metadata-def --name <GroupName>' for full details[/dim]")
826
+
827
+ except Exception as e:
828
+ console.print(f"[red]ERROR:[/red] {str(e)}")
829
+
830
+
831
+ __all__ = ['types']