pvw-cli 1.2.0__py3-none-any.whl → 1.2.3__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/search.py +242 -34
- purviewcli/client/_search.py +8 -8
- {pvw_cli-1.2.0.dist-info → pvw_cli-1.2.3.dist-info}/METADATA +27 -10
- {pvw_cli-1.2.0.dist-info → pvw_cli-1.2.3.dist-info}/RECORD +8 -8
- {pvw_cli-1.2.0.dist-info → pvw_cli-1.2.3.dist-info}/WHEEL +0 -0
- {pvw_cli-1.2.0.dist-info → pvw_cli-1.2.3.dist-info}/entry_points.txt +0 -0
- {pvw_cli-1.2.0.dist-info → pvw_cli-1.2.3.dist-info}/top_level.txt +0 -0
purviewcli/__init__.py
CHANGED
purviewcli/cli/search.py
CHANGED
|
@@ -139,12 +139,8 @@ def _format_search_results(data, show_ids=False):
|
|
|
139
139
|
table = Table(title=f"Search Results ({len(items)} of {count} total)")
|
|
140
140
|
table.add_column("Name", style="cyan", min_width=15, max_width=25)
|
|
141
141
|
table.add_column("Type", style="green", min_width=15, max_width=20)
|
|
142
|
+
table.add_column("ID", style="yellow", min_width=36, max_width=36)
|
|
142
143
|
table.add_column("Collection", style="blue", min_width=12, max_width=20)
|
|
143
|
-
table.add_column("Classifications", style="magenta", min_width=15, max_width=30)
|
|
144
|
-
|
|
145
|
-
if show_ids:
|
|
146
|
-
table.add_column("ID", style="yellow", min_width=36, max_width=36)
|
|
147
|
-
|
|
148
144
|
table.add_column("Qualified Name", style="white", min_width=30)
|
|
149
145
|
|
|
150
146
|
for item in items:
|
|
@@ -158,34 +154,23 @@ def _format_search_results(data, show_ids=False):
|
|
|
158
154
|
if len(qualified_name) > 60:
|
|
159
155
|
qualified_name = qualified_name[:57] + "..."
|
|
160
156
|
|
|
161
|
-
# Handle collection
|
|
157
|
+
# Handle collection - try multiple sources
|
|
162
158
|
collection = 'N/A'
|
|
163
159
|
if 'collection' in item and item['collection']:
|
|
164
|
-
|
|
160
|
+
if isinstance(item['collection'], dict):
|
|
161
|
+
collection = item['collection'].get('name', 'N/A')
|
|
162
|
+
else:
|
|
163
|
+
collection = str(item['collection'])
|
|
164
|
+
elif 'collectionId' in item and item['collectionId']:
|
|
165
|
+
collection = item.get('collectionId', 'N/A')
|
|
166
|
+
elif 'assetName' in item and item['assetName']:
|
|
167
|
+
# Try to extract collection from asset name
|
|
168
|
+
asset_name = item.get('assetName', '')
|
|
169
|
+
if asset_name and asset_name != 'N/A':
|
|
170
|
+
collection = asset_name
|
|
165
171
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
if 'classification' in item and item['classification']:
|
|
169
|
-
for cls in item['classification']:
|
|
170
|
-
if isinstance(cls, dict):
|
|
171
|
-
cls_name = cls.get('typeName', str(cls))
|
|
172
|
-
# Simplify Microsoft classifications for display
|
|
173
|
-
if cls_name.startswith('MICROSOFT.'):
|
|
174
|
-
cls_name = cls_name.replace('MICROSOFT.', 'MS.')
|
|
175
|
-
classifications.append(cls_name)
|
|
176
|
-
else:
|
|
177
|
-
classifications.append(str(cls))
|
|
178
|
-
|
|
179
|
-
# Truncate classifications if too long
|
|
180
|
-
cls_display = ", ".join(classifications) if classifications else ""
|
|
181
|
-
if len(cls_display) > 40:
|
|
182
|
-
cls_display = cls_display[:37] + "..."
|
|
183
|
-
|
|
184
|
-
# Build row data
|
|
185
|
-
row_data = [name, entity_type, collection, cls_display]
|
|
186
|
-
if show_ids:
|
|
187
|
-
row_data.append(entity_id)
|
|
188
|
-
row_data.append(qualified_name)
|
|
172
|
+
# Build row data with ID always shown
|
|
173
|
+
row_data = [name, entity_type, entity_id, collection, qualified_name]
|
|
189
174
|
|
|
190
175
|
# Add row to table
|
|
191
176
|
table.add_row(*row_data)
|
|
@@ -214,9 +199,9 @@ def _invoke_search_method(method_name, **kwargs):
|
|
|
214
199
|
# Choose output format
|
|
215
200
|
if output_json:
|
|
216
201
|
_format_json_output(result)
|
|
217
|
-
elif detailed and method_name in ['searchQuery', 'searchBrowse', 'searchSuggest', '
|
|
202
|
+
elif detailed and method_name in ['searchQuery', 'searchBrowse', 'searchSuggest', 'searchAutocomplete', 'searchFaceted']:
|
|
218
203
|
_format_detailed_output(result)
|
|
219
|
-
elif method_name in ['searchQuery', 'searchBrowse', 'searchSuggest', '
|
|
204
|
+
elif method_name in ['searchQuery', 'searchBrowse', 'searchSuggest', 'searchAutocomplete', 'searchFaceted']:
|
|
220
205
|
_format_search_results(result, show_ids=show_ids)
|
|
221
206
|
else:
|
|
222
207
|
_format_json_output(result)
|
|
@@ -230,7 +215,7 @@ def _invoke_search_method(method_name, **kwargs):
|
|
|
230
215
|
@click.option('--json', 'output_json', is_flag=True, help='Show full JSON details instead of table')
|
|
231
216
|
def autocomplete(keywords, limit, filterfile, output_json):
|
|
232
217
|
"""Autocomplete search suggestions"""
|
|
233
|
-
_invoke_search_method('
|
|
218
|
+
_invoke_search_method('searchAutocomplete', keywords=keywords, limit=limit, filterFile=filterfile, output_json=output_json)
|
|
234
219
|
|
|
235
220
|
@search.command()
|
|
236
221
|
@click.option('--entityType', required=False)
|
|
@@ -305,7 +290,7 @@ def advanced(keywords, limit, offset, filterfile, facets_file, businessmetadata,
|
|
|
305
290
|
with open(businessmetadata, 'r', encoding='utf-8') as f:
|
|
306
291
|
business_metadata_content = json.load(f)
|
|
307
292
|
_invoke_search_method(
|
|
308
|
-
'
|
|
293
|
+
'searchAdvanced',
|
|
309
294
|
keywords=keywords,
|
|
310
295
|
limit=limit,
|
|
311
296
|
offset=offset,
|
|
@@ -316,4 +301,227 @@ def advanced(keywords, limit, offset, filterfile, facets_file, businessmetadata,
|
|
|
316
301
|
termAssignments=termassignments
|
|
317
302
|
)
|
|
318
303
|
|
|
304
|
+
@search.command('find-table')
|
|
305
|
+
@click.option('--name', required=False, help='Table name (e.g., Address)')
|
|
306
|
+
@click.option('--schema', required=False, help='Schema name (e.g., SalesLT, dbo)')
|
|
307
|
+
@click.option('--database', required=False, help='Database name (e.g., Adventureworks)')
|
|
308
|
+
@click.option('--server', required=False, help='Server name (e.g., fabricdemos001.database.windows.net)')
|
|
309
|
+
@click.option('--qualified-name', required=False, help='Full qualified name from Purview (e.g., mssql://server/database/schema/table)')
|
|
310
|
+
@click.option('--entity-type', required=False, help='Entity type to search for (e.g., azure_sql_table, mssql_table)')
|
|
311
|
+
@click.option('--limit', required=False, type=int, default=25, help='Maximum number of results to return')
|
|
312
|
+
@click.option('--show-ids', is_flag=True, help='Show entity IDs in the results')
|
|
313
|
+
@click.option('--json', 'output_json', is_flag=True, help='Show full JSON details')
|
|
314
|
+
@click.option('--detailed', is_flag=True, help='Show detailed information')
|
|
315
|
+
@click.option('--id-only', is_flag=True, help='Output only the GUID (useful for scripts and automation)')
|
|
316
|
+
def find_table(name, schema, database, server, qualified_name, entity_type, limit, show_ids, output_json, detailed, id_only):
|
|
317
|
+
"""Find a table by name, schema, database, or get all tables in a schema/database.
|
|
318
|
+
|
|
319
|
+
Perfect for getting the GUID of a data asset before updating it.
|
|
320
|
+
You can search for ONE specific table or ALL tables in a schema/database.
|
|
321
|
+
|
|
322
|
+
\b
|
|
323
|
+
SEARCH ONE SPECIFIC TABLE:
|
|
324
|
+
pvw search find-table --name Address --schema SalesLT --database Adventureworks
|
|
325
|
+
pvw search find-table --qualified-name "mssql://server/database/schema/table"
|
|
326
|
+
|
|
327
|
+
\b
|
|
328
|
+
SEARCH MULTIPLE TABLES:
|
|
329
|
+
pvw search find-table --schema SalesLT --database Adventureworks
|
|
330
|
+
pvw search find-table --database Adventureworks
|
|
331
|
+
pvw search find-table --schema SalesLT
|
|
332
|
+
|
|
333
|
+
\b
|
|
334
|
+
GET GUIDS FOR AUTOMATION:
|
|
335
|
+
pvw search find-table --name Address --schema SalesLT --database Adventureworks --id-only
|
|
336
|
+
pvw search find-table --schema SalesLT --database Adventureworks --id-only
|
|
337
|
+
|
|
338
|
+
\b
|
|
339
|
+
USE IN SCRIPTS (PowerShell):
|
|
340
|
+
$guid = pvw search find-table --name Address --schema SalesLT --database Adventureworks --id-only
|
|
341
|
+
pvw entity update --guid $guid --payload update.json
|
|
342
|
+
|
|
343
|
+
$guids = pvw search find-table --schema SalesLT --database Adventureworks --id-only
|
|
344
|
+
foreach ($guid in $guids) { pvw entity update --guid $guid --payload update.json }
|
|
345
|
+
"""
|
|
346
|
+
search_client = Search()
|
|
347
|
+
|
|
348
|
+
# Validate that at least some search criteria is provided
|
|
349
|
+
if not name and not qualified_name and not schema and not database:
|
|
350
|
+
console.print("[red]ERROR:[/red] You must provide at least --name, --qualified-name, --schema, or --database")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Build search pattern
|
|
354
|
+
search_pattern = qualified_name
|
|
355
|
+
if not search_pattern:
|
|
356
|
+
# Build pattern from components
|
|
357
|
+
# Try to build a full qualified name pattern that matches Purview's format
|
|
358
|
+
if server and database and schema and name:
|
|
359
|
+
# Full path with server: mssql://server/database/schema/table
|
|
360
|
+
search_pattern = f"mssql://{server}/{database}/{schema}/{name}"
|
|
361
|
+
elif database and schema and name:
|
|
362
|
+
# Database.schema.table format
|
|
363
|
+
search_pattern = f"{database}/{schema}/{name}"
|
|
364
|
+
elif database and schema:
|
|
365
|
+
# Database.schema format (all tables in schema)
|
|
366
|
+
search_pattern = f"{database}/{schema}"
|
|
367
|
+
elif schema and name:
|
|
368
|
+
# Schema.table format
|
|
369
|
+
search_pattern = f"{schema}/{name}"
|
|
370
|
+
elif database:
|
|
371
|
+
# Just database (all tables in database)
|
|
372
|
+
search_pattern = database
|
|
373
|
+
elif schema:
|
|
374
|
+
# Just schema (all tables in schema)
|
|
375
|
+
search_pattern = schema
|
|
376
|
+
elif name:
|
|
377
|
+
# Just the table name
|
|
378
|
+
search_pattern = name
|
|
379
|
+
else:
|
|
380
|
+
console.print("[red]ERROR:[/red] You must provide at least one search criterion")
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
# For keyword search, use different strategies based on what we have
|
|
384
|
+
if name:
|
|
385
|
+
search_keywords = name
|
|
386
|
+
elif schema:
|
|
387
|
+
search_keywords = schema
|
|
388
|
+
elif database:
|
|
389
|
+
search_keywords = database
|
|
390
|
+
else:
|
|
391
|
+
search_keywords = search_pattern.split('/')[-1]
|
|
392
|
+
|
|
393
|
+
# Build search arguments - use keywords that will match
|
|
394
|
+
args = {
|
|
395
|
+
'--keywords': search_keywords,
|
|
396
|
+
'--limit': limit,
|
|
397
|
+
'--offset': 0
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# Create filter for entity type if specified
|
|
401
|
+
import tempfile
|
|
402
|
+
import json
|
|
403
|
+
import os
|
|
404
|
+
|
|
405
|
+
temp_filter_file = None
|
|
406
|
+
|
|
407
|
+
if entity_type:
|
|
408
|
+
filter_obj = {
|
|
409
|
+
'entityType': entity_type
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# Write filter to temp file
|
|
413
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
|
|
414
|
+
json.dump(filter_obj, f)
|
|
415
|
+
temp_filter_file = f.name
|
|
416
|
+
|
|
417
|
+
args['--filterFile'] = temp_filter_file
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
# Execute search
|
|
421
|
+
result = search_client.searchQuery(args)
|
|
422
|
+
|
|
423
|
+
if not result:
|
|
424
|
+
console.print("[yellow]No results returned from search[/yellow]")
|
|
425
|
+
if temp_filter_file:
|
|
426
|
+
os.unlink(temp_filter_file)
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Filter results by qualified name match if provided
|
|
430
|
+
if result and 'value' in result and result['value']:
|
|
431
|
+
filtered_results = []
|
|
432
|
+
search_lower = search_pattern.lower()
|
|
433
|
+
|
|
434
|
+
for item in result.get('value', []):
|
|
435
|
+
item_qn = item.get('qualifiedName', '').lower()
|
|
436
|
+
item_name = item.get('name', '').lower()
|
|
437
|
+
|
|
438
|
+
# Build matching criteria
|
|
439
|
+
matches = False
|
|
440
|
+
|
|
441
|
+
# If we have all components, do strict matching
|
|
442
|
+
if name and schema and database:
|
|
443
|
+
# Exact name match (not substring) - critical for precision
|
|
444
|
+
name_match = name.lower() == item_name
|
|
445
|
+
schema_match = schema.lower() in item_qn
|
|
446
|
+
database_match = database.lower() in item_qn
|
|
447
|
+
server_match = not server or server.lower() in item_qn
|
|
448
|
+
matches = name_match and schema_match and database_match and server_match
|
|
449
|
+
|
|
450
|
+
# If we have database and schema (all tables in this schema)
|
|
451
|
+
elif database and schema and not name:
|
|
452
|
+
schema_match = schema.lower() in item_qn
|
|
453
|
+
database_match = database.lower() in item_qn
|
|
454
|
+
server_match = not server or server.lower() in item_qn
|
|
455
|
+
matches = schema_match and database_match and server_match
|
|
456
|
+
|
|
457
|
+
# If we have schema and name
|
|
458
|
+
elif name and schema:
|
|
459
|
+
# Exact name match
|
|
460
|
+
name_match = name.lower() == item_name
|
|
461
|
+
schema_match = schema.lower() in item_qn
|
|
462
|
+
matches = name_match and schema_match
|
|
463
|
+
|
|
464
|
+
# If we have just database (all tables in this database)
|
|
465
|
+
elif database and not name and not schema:
|
|
466
|
+
database_match = database.lower() in item_qn
|
|
467
|
+
server_match = not server or server.lower() in item_qn
|
|
468
|
+
matches = database_match and server_match
|
|
469
|
+
|
|
470
|
+
# If we have just schema (all tables in this schema)
|
|
471
|
+
elif schema and not name and not database:
|
|
472
|
+
schema_match = schema.lower() in item_qn
|
|
473
|
+
matches = schema_match
|
|
474
|
+
|
|
475
|
+
# If we have just name or a qualified name pattern
|
|
476
|
+
elif name or qualified_name:
|
|
477
|
+
# If qualified_name was provided, do exact match
|
|
478
|
+
if qualified_name:
|
|
479
|
+
# Check for exact match of the qualified name
|
|
480
|
+
matches = search_lower == item_qn or item_qn.endswith('/' + search_keywords.lower())
|
|
481
|
+
else:
|
|
482
|
+
# Just name provided, match by name
|
|
483
|
+
matches = search_keywords.lower() == item_name
|
|
484
|
+
|
|
485
|
+
if matches:
|
|
486
|
+
filtered_results.append(item)
|
|
487
|
+
|
|
488
|
+
if filtered_results:
|
|
489
|
+
result['value'] = filtered_results
|
|
490
|
+
result['@search.count'] = len(filtered_results)
|
|
491
|
+
else:
|
|
492
|
+
console.print(f"[yellow]No results found matching '{search_pattern}'[/yellow]")
|
|
493
|
+
if temp_filter_file:
|
|
494
|
+
os.unlink(temp_filter_file)
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# Display results
|
|
498
|
+
if id_only:
|
|
499
|
+
# Output only the ID(s) for scripting purposes
|
|
500
|
+
if result and 'value' in result and result['value']:
|
|
501
|
+
for item in result['value']:
|
|
502
|
+
print(item.get('id', ''))
|
|
503
|
+
else:
|
|
504
|
+
console.print("[yellow]No results found[/yellow]")
|
|
505
|
+
elif output_json:
|
|
506
|
+
_format_json_output(result)
|
|
507
|
+
elif detailed:
|
|
508
|
+
_format_detailed_output(result)
|
|
509
|
+
else:
|
|
510
|
+
_format_search_results(result, show_ids=show_ids)
|
|
511
|
+
|
|
512
|
+
# Clean up temp file
|
|
513
|
+
if temp_filter_file:
|
|
514
|
+
import os
|
|
515
|
+
os.unlink(temp_filter_file)
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
519
|
+
# Clean up temp file on error
|
|
520
|
+
if temp_filter_file:
|
|
521
|
+
import os
|
|
522
|
+
try:
|
|
523
|
+
os.unlink(temp_filter_file)
|
|
524
|
+
except:
|
|
525
|
+
pass
|
|
526
|
+
|
|
319
527
|
__all__ = ['search']
|
purviewcli/client/_search.py
CHANGED
|
@@ -93,13 +93,13 @@ class Search(Endpoint):
|
|
|
93
93
|
self.endpoint = ENDPOINTS["discovery"]["suggest"]
|
|
94
94
|
self.params = get_api_version_params("datamap")
|
|
95
95
|
|
|
96
|
+
# Suggest API expects keywords in search field, not keywords field
|
|
96
97
|
suggest_request = {
|
|
97
|
-
"keywords": args.get("--keywords", ""),
|
|
98
|
-
"limit": args.get("--limit", 5)
|
|
99
|
-
"filter": {}
|
|
98
|
+
"keywords": args.get("--keywords", "*"),
|
|
99
|
+
"limit": args.get("--limit", 5)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
#
|
|
102
|
+
# Only add filter if provided and not empty
|
|
103
103
|
if args.get("--filter"):
|
|
104
104
|
suggest_request["filter"] = self._parse_filter(args["--filter"])
|
|
105
105
|
|
|
@@ -107,18 +107,18 @@ class Search(Endpoint):
|
|
|
107
107
|
|
|
108
108
|
@decorator
|
|
109
109
|
def searchAutocomplete(self, args):
|
|
110
|
-
"""Get autocomplete suggestions (Official API:
|
|
110
|
+
"""Get search autocomplete suggestions (Official API: AutoComplete)"""
|
|
111
111
|
self.method = "POST"
|
|
112
112
|
self.endpoint = ENDPOINTS["discovery"]["autocomplete"]
|
|
113
113
|
self.params = get_api_version_params("datamap")
|
|
114
114
|
|
|
115
|
+
# Autocomplete API expects keywords (text to complete)
|
|
115
116
|
autocomplete_request = {
|
|
116
117
|
"keywords": args.get("--keywords", ""),
|
|
117
|
-
"limit": args.get("--limit",
|
|
118
|
-
"filter": {}
|
|
118
|
+
"limit": args.get("--limit", 5)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
#
|
|
121
|
+
# Only add filter if provided and not empty
|
|
122
122
|
if args.get("--filter"):
|
|
123
123
|
autocomplete_request["filter"] = self._parse_filter(args["--filter"])
|
|
124
124
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pvw-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Microsoft Purview CLI with comprehensive automation capabilities
|
|
5
5
|
Author-email: AYOUB KEBAILI <keayoub@msn.com>
|
|
6
6
|
Maintainer-email: AYOUB KEBAILI <keayoub@msn.com>
|
|
@@ -34,7 +34,7 @@ Requires-Dist: rich>=12.0.0
|
|
|
34
34
|
Requires-Dist: requests>=2.28.0
|
|
35
35
|
Requires-Dist: pandas>=1.5.0
|
|
36
36
|
Requires-Dist: aiohttp>=3.8.0
|
|
37
|
-
Requires-Dist: pydantic<
|
|
37
|
+
Requires-Dist: pydantic<2.12,>=1.10.0
|
|
38
38
|
Requires-Dist: PyYAML>=6.0
|
|
39
39
|
Requires-Dist: cryptography<46.0.0,>=41.0.5
|
|
40
40
|
Provides-Extra: dev
|
|
@@ -56,10 +56,13 @@ Requires-Dist: pytest-asyncio>=0.20.0; extra == "test"
|
|
|
56
56
|
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
57
57
|
Requires-Dist: requests-mock>=1.9.0; extra == "test"
|
|
58
58
|
|
|
59
|
-
# PURVIEW CLI v1.2.
|
|
59
|
+
# PURVIEW CLI v1.2.3 - Microsoft Purview Automation & Data Governance
|
|
60
60
|
|
|
61
|
-
> **LATEST UPDATE (October 2025):**
|
|
62
|
-
> -
|
|
61
|
+
> **LATEST UPDATE v1.2.3 (October 2025):**
|
|
62
|
+
> - **🔍 FIXED: Search API Integration** - Fixed `suggest` and `autocomplete` API payload format (HTTP 400 errors resolved)
|
|
63
|
+
> - **📊 ENHANCED: Collection Display** - Improved collection name detection in search results with proper fallback logic
|
|
64
|
+
> - **✅ VALIDATED: All Search Commands** - Comprehensive testing of query, browse, suggest, find-table operations
|
|
65
|
+
> - **📦 NEW: Bulk Term Import/Export** - Import multiple terms from CSV/JSON with dry-run support
|
|
63
66
|
> - **🗑️ NEW: Bulk Delete Scripts** - PowerShell and Python scripts for bulk term deletion
|
|
64
67
|
> - **📊 NEW: Multiple Output Formats** - `--output` flag supports table, json, and jsonc formats
|
|
65
68
|
> - **🔧 NEW: PowerShell Integration** - Plain JSON output works with `ConvertFrom-Json`
|
|
@@ -72,7 +75,7 @@ Requires-Dist: requests-mock>=1.9.0; extra == "test"
|
|
|
72
75
|
|
|
73
76
|
## What is PVW CLI?
|
|
74
77
|
|
|
75
|
-
**PVW CLI v1.2.
|
|
78
|
+
**PVW CLI v1.2.3** is a modern, full-featured command-line interface and Python library for Microsoft Purview. It enables automation and management of *all major Purview APIs* including:
|
|
76
79
|
|
|
77
80
|
- **Unified Catalog (UC) Management** - Complete governance domains, glossary terms, data products, OKRs, CDEs
|
|
78
81
|
- **Bulk Operations** - Import/export terms from CSV/JSON, bulk delete scripts with progress tracking
|
|
@@ -164,7 +167,7 @@ For more advanced usage, see the documentation in `doc/` or the project docs: ht
|
|
|
164
167
|
|
|
165
168
|
## Overview
|
|
166
169
|
|
|
167
|
-
**PVW CLI v1.2.
|
|
170
|
+
**PVW CLI v1.2.3** is a modern command-line interface and Python library for Microsoft Purview, enabling:
|
|
168
171
|
|
|
169
172
|
- Advanced data catalog search and discovery
|
|
170
173
|
- Bulk import/export of entities, glossary terms, and lineage
|
|
@@ -511,6 +514,11 @@ The PVW CLI provides advanced search using the latest Microsoft Purview Discover
|
|
|
511
514
|
- Use autocomplete and suggestion endpoints
|
|
512
515
|
- Perform faceted, time-based, and entity-type-specific queries
|
|
513
516
|
|
|
517
|
+
**✅ v1.2.3 Improvements:**
|
|
518
|
+
- Fixed `suggest` and `autocomplete` API payload format (removed empty filter causing HTTP 400 errors)
|
|
519
|
+
- Enhanced collection display with robust type checking and fallback logic
|
|
520
|
+
- All search commands validated and working correctly (query, browse, suggest, find-table)
|
|
521
|
+
|
|
514
522
|
### CLI Usage Examples
|
|
515
523
|
|
|
516
524
|
#### 🎯 **Multiple Output Formats**
|
|
@@ -1149,10 +1157,13 @@ PVW CLI includes comprehensive sample files and scripts for bulk operations:
|
|
|
1149
1157
|
- Success/failure tracking per term
|
|
1150
1158
|
- Rate limiting (200ms delay)
|
|
1151
1159
|
|
|
1152
|
-
### ✅ Critical Fixes
|
|
1160
|
+
### ✅ Critical Fixes (v1.2.3)
|
|
1161
|
+
- **Search API Suggest/Autocomplete:** Fixed HTTP 400 errors by removing empty filter objects from payload
|
|
1162
|
+
- **Collection Display:** Enhanced collection name detection with proper fallback logic (isinstance checks)
|
|
1153
1163
|
- **Owner ID Format:** Must use Entra ID Object IDs (GUIDs), not email addresses
|
|
1154
1164
|
- **Domain Status:** Terms cannot be "Published" in unpublished domains - use "Draft"
|
|
1155
1165
|
- **Error Validation:** Enhanced error handling shows actual API responses
|
|
1166
|
+
- **Windows Console Compatibility:** All emoji removed for CP-1252 encoding support
|
|
1156
1167
|
|
|
1157
1168
|
---
|
|
1158
1169
|
|
|
@@ -1203,6 +1214,12 @@ See [LICENSE](LICENSE) file for details.
|
|
|
1203
1214
|
|
|
1204
1215
|
---
|
|
1205
1216
|
|
|
1206
|
-
**PVW CLI v1.2.
|
|
1217
|
+
**PVW CLI v1.2.3 empowers data engineers, stewards, and architects to automate, scale, and enhance their Microsoft Purview experience with powerful command-line and programmatic capabilities.**
|
|
1207
1218
|
|
|
1208
|
-
**Latest
|
|
1219
|
+
**Latest in v1.2.3:**
|
|
1220
|
+
- Fixed Search API suggest/autocomplete (HTTP 400 errors resolved)
|
|
1221
|
+
- Enhanced collection display with robust fallback logic
|
|
1222
|
+
- Comprehensive search command validation
|
|
1223
|
+
- Bulk term import/export with dry-run support
|
|
1224
|
+
- PowerShell integration with plain JSON output
|
|
1225
|
+
- Multiple output formats and beautiful progress tracking
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
purviewcli/__init__.py,sha256=
|
|
1
|
+
purviewcli/__init__.py,sha256=v5JNPN85G2EM5adxgzN03nNd_5Nv5Q_BM2foitqgsUw,413
|
|
2
2
|
purviewcli/__main__.py,sha256=n_PFo1PjW8L1OKCNLsW0vlVSo8tzac_saEYYLTu93iQ,372
|
|
3
3
|
purviewcli/cli/__init__.py,sha256=UGMctZaXXsV2l2ycnmhTgyksH81_JBQjAPq3oRF2Dqk,56
|
|
4
4
|
purviewcli/cli/account.py,sha256=Z_bwhKriMQpoBicOORM64wpQ1MJ94QG7jGKiaG-D_r8,7092
|
|
@@ -14,7 +14,7 @@ purviewcli/cli/management.py,sha256=ArSN7ADWMKUJ-fVYjPiuab4g666GQUmxU2WZov3JxzI,
|
|
|
14
14
|
purviewcli/cli/policystore.py,sha256=Xj3Yx6kNt9W8v577x2iacqSlPatR6AuFfMBpjtHTj1E,4439
|
|
15
15
|
purviewcli/cli/relationship.py,sha256=Ky4klI-clKh6sRK7bsI7SwgtVrpo1ljegVyrbqjkeOY,2682
|
|
16
16
|
purviewcli/cli/scan.py,sha256=91iKDH8iVNJKndJAisrKx3J4HRoPH2qfmxguLZH3xHY,13807
|
|
17
|
-
purviewcli/cli/search.py,sha256=
|
|
17
|
+
purviewcli/cli/search.py,sha256=QNAKiFSi8oSEwzh-ksR4vhNAZ1z0dje1icdg36xRMvk,22667
|
|
18
18
|
purviewcli/cli/share.py,sha256=QRZhHM59RxdYqXOjSYLfVRZmjwMg4Y-bWxMSQVTQiIE,20197
|
|
19
19
|
purviewcli/cli/types.py,sha256=zo_8rAqDQ1vqi5y-dBh_sVY6i16UaJLLx_vBJBfZrrw,23729
|
|
20
20
|
purviewcli/cli/unified_catalog.py,sha256=ZQn5GsHsfXCrjzTPnfNA6gfGBOD5AH2m7uauGZt-oZs,74753
|
|
@@ -32,7 +32,7 @@ purviewcli/client/_management.py,sha256=2_ZXRSeEzGuHv1muUbn8mQb07enYYuHPI3NskIHI
|
|
|
32
32
|
purviewcli/client/_policystore.py,sha256=wx9Yw6wcvj236ZmmitUKxW5u4YAtfJqAFNuxMpa7HIU,18078
|
|
33
33
|
purviewcli/client/_relationship.py,sha256=KJZRltrzpTw7cfKjlZH2MuoTPS7eHxxp3cqU2etDSEA,9997
|
|
34
34
|
purviewcli/client/_scan.py,sha256=2atEBD-kKWtFuBSWh2P0cwp42gfg7qgwWq-072QZMs4,15154
|
|
35
|
-
purviewcli/client/_search.py,sha256=
|
|
35
|
+
purviewcli/client/_search.py,sha256=9QfRE7FUukhpMQ_Xc9NziG6aKFMlPt4_OVaI0QinGa8,12019
|
|
36
36
|
purviewcli/client/_share.py,sha256=vKENIhePuzi3WQazNfv5U9y-6yxRk222zrFA-SGh1pc,10494
|
|
37
37
|
purviewcli/client/_types.py,sha256=ONa3wh1F02QOVy51UGq54121TkqRcWczdXIvNqPIFU0,15454
|
|
38
38
|
purviewcli/client/_unified_catalog.py,sha256=WDpl68NJXfMJ5OIlING0wrfXwbOLwbnGRAFL9WPZnCE,40017
|
|
@@ -53,8 +53,8 @@ purviewcli/client/settings.py,sha256=nYdnYurTZsgv9vcgljnzVxLPtYVl9q6IplqOzi1aRvI
|
|
|
53
53
|
purviewcli/client/sync_client.py,sha256=gwCqesJTNaXn1Q-j57O95R9mn3fIOhdP4sc8jBaBcYw,9493
|
|
54
54
|
purviewcli/plugins/__init__.py,sha256=rpt3OhFt_wSE_o8Ga8AXvw1pqkdBxLmjrhYtE_-LuJo,29
|
|
55
55
|
purviewcli/plugins/plugin_system.py,sha256=C-_dL4FUj90o1JS7Saxkpov6fz0GIF5PFhZTYwqBkWE,26774
|
|
56
|
-
pvw_cli-1.2.
|
|
57
|
-
pvw_cli-1.2.
|
|
58
|
-
pvw_cli-1.2.
|
|
59
|
-
pvw_cli-1.2.
|
|
60
|
-
pvw_cli-1.2.
|
|
56
|
+
pvw_cli-1.2.3.dist-info/METADATA,sha256=rx7pJk-uMjIsZXSixs-R590RDEzxOYMeHmLYXVFUrtk,40276
|
|
57
|
+
pvw_cli-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
pvw_cli-1.2.3.dist-info/entry_points.txt,sha256=VI6AAbc6sWahOCX7sn_lhJIr9OiJM0pHF7rmw1YVGlE,82
|
|
59
|
+
pvw_cli-1.2.3.dist-info/top_level.txt,sha256=LrADzPoKwF1xY0pGKpWauyOVruHCIWKCkT7cwIl6IuI,11
|
|
60
|
+
pvw_cli-1.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|