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,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']