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.
- purviewcli/__init__.py +27 -0
- purviewcli/__main__.py +15 -0
- purviewcli/cli/__init__.py +5 -0
- purviewcli/cli/account.py +199 -0
- purviewcli/cli/cli.py +170 -0
- purviewcli/cli/collections.py +502 -0
- purviewcli/cli/domain.py +361 -0
- purviewcli/cli/entity.py +2436 -0
- purviewcli/cli/glossary.py +533 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/insight.py +113 -0
- purviewcli/cli/lineage.py +1103 -0
- purviewcli/cli/management.py +141 -0
- purviewcli/cli/policystore.py +103 -0
- purviewcli/cli/relationship.py +75 -0
- purviewcli/cli/scan.py +357 -0
- purviewcli/cli/search.py +527 -0
- purviewcli/cli/share.py +478 -0
- purviewcli/cli/types.py +831 -0
- purviewcli/cli/unified_catalog.py +3540 -0
- purviewcli/cli/workflow.py +402 -0
- purviewcli/client/__init__.py +21 -0
- purviewcli/client/_account.py +1877 -0
- purviewcli/client/_collections.py +1761 -0
- purviewcli/client/_domain.py +414 -0
- purviewcli/client/_entity.py +3545 -0
- purviewcli/client/_glossary.py +3233 -0
- purviewcli/client/_health.py +501 -0
- purviewcli/client/_insight.py +2873 -0
- purviewcli/client/_lineage.py +2138 -0
- purviewcli/client/_management.py +2202 -0
- purviewcli/client/_policystore.py +2915 -0
- purviewcli/client/_relationship.py +1351 -0
- purviewcli/client/_scan.py +2607 -0
- purviewcli/client/_search.py +1472 -0
- purviewcli/client/_share.py +272 -0
- purviewcli/client/_types.py +2708 -0
- purviewcli/client/_unified_catalog.py +5112 -0
- purviewcli/client/_workflow.py +2734 -0
- purviewcli/client/api_client.py +1295 -0
- purviewcli/client/business_rules.py +675 -0
- purviewcli/client/config.py +231 -0
- purviewcli/client/data_quality.py +433 -0
- purviewcli/client/endpoint.py +123 -0
- purviewcli/client/endpoints.py +554 -0
- purviewcli/client/exceptions.py +38 -0
- purviewcli/client/lineage_visualization.py +797 -0
- purviewcli/client/monitoring_dashboard.py +712 -0
- purviewcli/client/rate_limiter.py +30 -0
- purviewcli/client/retry_handler.py +125 -0
- purviewcli/client/scanning_operations.py +523 -0
- purviewcli/client/settings.py +1 -0
- purviewcli/client/sync_client.py +250 -0
- purviewcli/plugins/__init__.py +1 -0
- purviewcli/plugins/plugin_system.py +709 -0
- pvw_cli-1.2.8.dist-info/METADATA +1618 -0
- pvw_cli-1.2.8.dist-info/RECORD +60 -0
- pvw_cli-1.2.8.dist-info/WHEEL +5 -0
- pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
- pvw_cli-1.2.8.dist-info/top_level.txt +1 -0
purviewcli/cli/search.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"""
|
|
2
|
+
usage:
|
|
3
|
+
pvw search autoComplete [--keywords=<val> --limit=<val> --filterFile=<val>]
|
|
4
|
+
pvw search browse (--entityType=<val> | --path=<val>) [--limit=<val> --offset=<val>]
|
|
5
|
+
pvw search query [--keywords=<val> --limit=<val> --offset=<val> --filterFile=<val> --facets-file=<val>]
|
|
6
|
+
pvw search suggest [--keywords=<val> --limit=<val> --filterFile=<val>]
|
|
7
|
+
|
|
8
|
+
options:
|
|
9
|
+
--purviewName=<val> [string] Microsoft Purview account name.
|
|
10
|
+
--keywords=<val> [string] The keywords applied to all searchable fields.
|
|
11
|
+
--entityType=<val> [string] The entity type to browse as the root level entry point.
|
|
12
|
+
--path=<val> [string] The path to browse the next level child entities.
|
|
13
|
+
--limit=<val> [integer] By default there is no paging [default: 25].
|
|
14
|
+
--offset=<val> [integer] Offset for pagination purpose [default: 0].
|
|
15
|
+
--filterFile=<val> [string] File path to a filter json file.
|
|
16
|
+
--facets-file=<val> [string] File path to a facets json file.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
# Search CLI for Purview Data Map API (Atlas v2)
|
|
20
|
+
"""
|
|
21
|
+
CLI for advanced search and discovery
|
|
22
|
+
"""
|
|
23
|
+
import click
|
|
24
|
+
import json
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from purviewcli.client._search import Search
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def search():
|
|
33
|
+
"""Search and discover assets"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def _format_json_output(data):
|
|
37
|
+
"""Format JSON output with syntax highlighting using Rich"""
|
|
38
|
+
from rich.console import Console
|
|
39
|
+
from rich.syntax import Syntax
|
|
40
|
+
import json
|
|
41
|
+
|
|
42
|
+
console = Console()
|
|
43
|
+
|
|
44
|
+
# Pretty print JSON with syntax highlighting
|
|
45
|
+
json_str = json.dumps(data, indent=2)
|
|
46
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
|
|
47
|
+
console.print(syntax)
|
|
48
|
+
|
|
49
|
+
def _format_detailed_output(data):
|
|
50
|
+
"""Format search results with detailed information in readable format"""
|
|
51
|
+
from rich.console import Console
|
|
52
|
+
from rich.panel import Panel
|
|
53
|
+
from rich.text import Text
|
|
54
|
+
|
|
55
|
+
console = Console()
|
|
56
|
+
|
|
57
|
+
# Extract results data
|
|
58
|
+
count = data.get('@search.count', 0)
|
|
59
|
+
items = data.get('value', [])
|
|
60
|
+
|
|
61
|
+
if not items:
|
|
62
|
+
console.print("[yellow]No results found[/yellow]")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
console.print(f"\n[bold cyan]Search Results: {len(items)} of {count} total[/bold cyan]\n")
|
|
66
|
+
|
|
67
|
+
for i, item in enumerate(items, 1):
|
|
68
|
+
# Create a panel for each result
|
|
69
|
+
details = []
|
|
70
|
+
|
|
71
|
+
# Basic information
|
|
72
|
+
details.append(f"[bold cyan]Name:[/bold cyan] {item.get('name', 'N/A')}")
|
|
73
|
+
details.append(f"[bold green]Type:[/bold green] {item.get('entityType', 'N/A')}")
|
|
74
|
+
details.append(f"[bold yellow]ID:[/bold yellow] {item.get('id', 'N/A')}")
|
|
75
|
+
|
|
76
|
+
# Collection
|
|
77
|
+
if 'collection' in item and item['collection']:
|
|
78
|
+
collection_name = item['collection'].get('name', 'N/A')
|
|
79
|
+
else:
|
|
80
|
+
collection_name = item.get('collectionId', 'N/A')
|
|
81
|
+
details.append(f"[bold blue]Collection:[/bold blue] {collection_name}")
|
|
82
|
+
|
|
83
|
+
# Qualified Name
|
|
84
|
+
details.append(f"[bold white]Qualified Name:[/bold white] {item.get('qualifiedName', 'N/A')}")
|
|
85
|
+
|
|
86
|
+
# Classifications
|
|
87
|
+
if 'classification' in item and item['classification']:
|
|
88
|
+
classifications = []
|
|
89
|
+
for cls in item['classification']:
|
|
90
|
+
if isinstance(cls, dict):
|
|
91
|
+
classifications.append(cls.get('typeName', str(cls)))
|
|
92
|
+
else:
|
|
93
|
+
classifications.append(str(cls))
|
|
94
|
+
details.append(f"[bold magenta]Classifications:[/bold magenta] {', '.join(classifications)}")
|
|
95
|
+
|
|
96
|
+
# Additional metadata
|
|
97
|
+
if 'updateTime' in item:
|
|
98
|
+
details.append(f"[bold dim]Last Updated:[/bold dim] {item.get('updateTime')}")
|
|
99
|
+
if 'createTime' in item:
|
|
100
|
+
details.append(f"[bold dim]Created:[/bold dim] {item.get('createTime')}")
|
|
101
|
+
if 'updateBy' in item:
|
|
102
|
+
details.append(f"[bold dim]Updated By:[/bold dim] {item.get('updateBy')}")
|
|
103
|
+
|
|
104
|
+
# Search score
|
|
105
|
+
if '@search.score' in item:
|
|
106
|
+
details.append(f"[bold dim]Search Score:[/bold dim] {item.get('@search.score'):.2f}")
|
|
107
|
+
|
|
108
|
+
# Create panel
|
|
109
|
+
panel_content = "\n".join(details)
|
|
110
|
+
panel = Panel(
|
|
111
|
+
panel_content,
|
|
112
|
+
title=f"[bold]{i}. {item.get('name', 'Unknown')}[/bold]",
|
|
113
|
+
border_style="blue"
|
|
114
|
+
)
|
|
115
|
+
console.print(panel)
|
|
116
|
+
|
|
117
|
+
# Add pagination hint if there are more results
|
|
118
|
+
if len(items) < count:
|
|
119
|
+
console.print(f"\n💡 [dim]More results available. Use --offset to paginate.[/dim]")
|
|
120
|
+
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
def _format_search_results(data, show_ids=False):
|
|
124
|
+
"""Format search results as a nice table using Rich"""
|
|
125
|
+
from rich.console import Console
|
|
126
|
+
from rich.table import Table
|
|
127
|
+
|
|
128
|
+
console = Console()
|
|
129
|
+
|
|
130
|
+
# Extract results data
|
|
131
|
+
count = data.get('@search.count', 0)
|
|
132
|
+
items = data.get('value', [])
|
|
133
|
+
|
|
134
|
+
if not items:
|
|
135
|
+
console.print("[yellow]No results found[/yellow]")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Create table
|
|
139
|
+
table = Table(title=f"Search Results ({len(items)} of {count} total)")
|
|
140
|
+
table.add_column("Name", style="cyan", min_width=15, max_width=25)
|
|
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)
|
|
143
|
+
table.add_column("Collection", style="blue", min_width=12, max_width=20)
|
|
144
|
+
table.add_column("Qualified Name", style="white", min_width=30)
|
|
145
|
+
|
|
146
|
+
for item in items:
|
|
147
|
+
# Extract entity information
|
|
148
|
+
name = item.get('name', 'N/A')
|
|
149
|
+
entity_type = item.get('entityType', 'N/A')
|
|
150
|
+
entity_id = item.get('id', 'N/A')
|
|
151
|
+
qualified_name = item.get('qualifiedName', 'N/A')
|
|
152
|
+
|
|
153
|
+
# Truncate long qualified names for better display
|
|
154
|
+
if len(qualified_name) > 60:
|
|
155
|
+
qualified_name = qualified_name[:57] + "..."
|
|
156
|
+
|
|
157
|
+
# Handle collection - try multiple sources
|
|
158
|
+
collection = 'N/A'
|
|
159
|
+
if 'collection' in item and item['collection']:
|
|
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
|
|
171
|
+
|
|
172
|
+
# Build row data with ID always shown
|
|
173
|
+
row_data = [name, entity_type, entity_id, collection, qualified_name]
|
|
174
|
+
|
|
175
|
+
# Add row to table
|
|
176
|
+
table.add_row(*row_data)
|
|
177
|
+
|
|
178
|
+
# Print the table
|
|
179
|
+
console.print(table)
|
|
180
|
+
|
|
181
|
+
# Add pagination hint if there are more results
|
|
182
|
+
if len(items) < count:
|
|
183
|
+
console.print(f"\n💡 More results available. Use --offset to paginate.")
|
|
184
|
+
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
def _invoke_search_method(method_name, **kwargs):
|
|
188
|
+
search_client = Search()
|
|
189
|
+
method = getattr(search_client, method_name)
|
|
190
|
+
|
|
191
|
+
# Extract formatting options, don't pass to API
|
|
192
|
+
show_ids = kwargs.pop('show_ids', False)
|
|
193
|
+
output_json = kwargs.pop('output_json', False)
|
|
194
|
+
detailed = kwargs.pop('detailed', False)
|
|
195
|
+
|
|
196
|
+
args = {f'--{k}': v for k, v in kwargs.items() if v is not None}
|
|
197
|
+
try:
|
|
198
|
+
result = method(args)
|
|
199
|
+
# Choose output format
|
|
200
|
+
if output_json:
|
|
201
|
+
_format_json_output(result)
|
|
202
|
+
elif detailed and method_name in ['searchQuery', 'searchBrowse', 'searchSuggest', 'searchAutocomplete', 'searchFaceted']:
|
|
203
|
+
_format_detailed_output(result)
|
|
204
|
+
elif method_name in ['searchQuery', 'searchBrowse', 'searchSuggest', 'searchAutocomplete', 'searchFaceted']:
|
|
205
|
+
_format_search_results(result, show_ids=show_ids)
|
|
206
|
+
else:
|
|
207
|
+
_format_json_output(result)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
console.print(f"[red]ERROR:[/red] {str(e)}")
|
|
210
|
+
|
|
211
|
+
@search.command()
|
|
212
|
+
@click.option('--keywords', required=False)
|
|
213
|
+
@click.option('--limit', required=False, type=int, default=25)
|
|
214
|
+
@click.option('--filterFile', required=False, type=click.Path(exists=True))
|
|
215
|
+
@click.option('--json', 'output_json', is_flag=True, help='Show full JSON details instead of table')
|
|
216
|
+
def autocomplete(keywords, limit, filterfile, output_json):
|
|
217
|
+
"""Autocomplete search suggestions"""
|
|
218
|
+
_invoke_search_method('searchAutocomplete', keywords=keywords, limit=limit, filterFile=filterfile, output_json=output_json)
|
|
219
|
+
|
|
220
|
+
@search.command()
|
|
221
|
+
@click.option('--entityType', required=False)
|
|
222
|
+
@click.option('--path', required=False)
|
|
223
|
+
@click.option('--limit', required=False, type=int, default=25)
|
|
224
|
+
@click.option('--offset', required=False, type=int, default=0)
|
|
225
|
+
@click.option('--json', 'output_json', is_flag=True, help='Show full JSON details instead of table')
|
|
226
|
+
def browse(entitytype, path, limit, offset, output_json):
|
|
227
|
+
"""Browse entities by type or path"""
|
|
228
|
+
_invoke_search_method('searchBrowse', entityType=entitytype, path=path, limit=limit, offset=offset, output_json=output_json)
|
|
229
|
+
|
|
230
|
+
@search.command()
|
|
231
|
+
@click.option('--keywords', required=False)
|
|
232
|
+
@click.option('--limit', required=False, type=int, default=25)
|
|
233
|
+
@click.option('--offset', required=False, type=int, default=0)
|
|
234
|
+
@click.option('--filterFile', required=False, type=click.Path(exists=True))
|
|
235
|
+
@click.option('--facets-file', required=False, type=click.Path(exists=True))
|
|
236
|
+
@click.option('--show-ids', is_flag=True, help='Show entity IDs in the results')
|
|
237
|
+
@click.option('--json', 'output_json', is_flag=True, help='Show full JSON details instead of table')
|
|
238
|
+
@click.option('--detailed', is_flag=True, help='Show detailed information in readable format')
|
|
239
|
+
def query(keywords, limit, offset, filterfile, facets_file, show_ids, output_json, detailed):
|
|
240
|
+
"""Run a search query"""
|
|
241
|
+
_invoke_search_method('searchQuery', keywords=keywords, limit=limit, offset=offset, filterFile=filterfile, facets_file=facets_file, show_ids=show_ids, output_json=output_json, detailed=detailed)
|
|
242
|
+
|
|
243
|
+
@search.command()
|
|
244
|
+
@click.option('--keywords', required=False)
|
|
245
|
+
@click.option('--limit', required=False, type=int, default=25)
|
|
246
|
+
@click.option('--filterFile', required=False, type=click.Path(exists=True))
|
|
247
|
+
@click.option('--json', 'output_json', is_flag=True, help='Show full JSON details instead of table')
|
|
248
|
+
def suggest(keywords, limit, filterfile, output_json):
|
|
249
|
+
"""Get search suggestions"""
|
|
250
|
+
_invoke_search_method('searchSuggest', keywords=keywords, limit=limit, filterFile=filterfile, output_json=output_json)
|
|
251
|
+
|
|
252
|
+
@search.command()
|
|
253
|
+
@click.option('--keywords', required=False)
|
|
254
|
+
@click.option('--limit', required=False, type=int, default=25)
|
|
255
|
+
@click.option('--offset', required=False, type=int, default=0)
|
|
256
|
+
@click.option('--filterFile', required=False, type=click.Path(exists=True))
|
|
257
|
+
@click.option('--facets-file', required=False, type=click.Path(exists=True))
|
|
258
|
+
@click.option('--facetFields', required=False, help='Comma-separated facet fields (e.g., objectType,classification)')
|
|
259
|
+
@click.option('--facetCount', required=False, type=int, help='Facet count per field')
|
|
260
|
+
@click.option('--facetSort', required=False, type=str, help='Facet sort order (e.g., count, value)')
|
|
261
|
+
def faceted(keywords, limit, offset, filterfile, facets_file, facetfields, facetcount, facetsort):
|
|
262
|
+
"""Run a faceted search"""
|
|
263
|
+
_invoke_search_method(
|
|
264
|
+
'searchFaceted',
|
|
265
|
+
keywords=keywords,
|
|
266
|
+
limit=limit,
|
|
267
|
+
offset=offset,
|
|
268
|
+
filterFile=filterfile,
|
|
269
|
+
facets_file=facets_file,
|
|
270
|
+
facetFields=facetfields,
|
|
271
|
+
facetCount=facetcount,
|
|
272
|
+
facetSort=facetsort
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
@search.command()
|
|
276
|
+
@click.option('--keywords', required=False)
|
|
277
|
+
@click.option('--limit', required=False, type=int, default=25)
|
|
278
|
+
@click.option('--offset', required=False, type=int, default=0)
|
|
279
|
+
@click.option('--filterFile', required=False, type=click.Path(exists=True))
|
|
280
|
+
@click.option('--facets-file', required=False, type=click.Path(exists=True))
|
|
281
|
+
@click.option('--businessMetadata', required=False, type=click.Path(exists=True), help='Path to business metadata JSON file')
|
|
282
|
+
@click.option('--classifications', required=False, help='Comma-separated classifications')
|
|
283
|
+
@click.option('--termAssignments', required=False, help='Comma-separated term assignments')
|
|
284
|
+
def advanced(keywords, limit, offset, filterfile, facets_file, businessmetadata, classifications, termassignments):
|
|
285
|
+
"""Run an advanced search query"""
|
|
286
|
+
# Load business metadata JSON if provided
|
|
287
|
+
business_metadata_content = None
|
|
288
|
+
if businessmetadata:
|
|
289
|
+
import json
|
|
290
|
+
with open(businessmetadata, 'r', encoding='utf-8') as f:
|
|
291
|
+
business_metadata_content = json.load(f)
|
|
292
|
+
_invoke_search_method(
|
|
293
|
+
'searchAdvanced',
|
|
294
|
+
keywords=keywords,
|
|
295
|
+
limit=limit,
|
|
296
|
+
offset=offset,
|
|
297
|
+
filterFile=filterfile,
|
|
298
|
+
facets_file=facets_file,
|
|
299
|
+
businessMetadata=business_metadata_content,
|
|
300
|
+
classifications=classifications,
|
|
301
|
+
termAssignments=termassignments
|
|
302
|
+
)
|
|
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
|
+
|
|
527
|
+
__all__ = ['search']
|