feat: add find command for catalog search#174
feat: add find command for catalog search#174nadavs123 wants to merge 47 commits intomicrosoft:mainfrom
find command for catalog search#174Conversation
Features: - Search across all workspaces by displayName, workspaceName, or description - Filter by item type with --type flag - Limit results with --limit flag - Detailed output with --detailed flag (includes id, workspaceId) - Custom endpoint support with --endpoint flag or FAB_CATALOG_ENDPOINT env var Output columns (default): name, type, workspace, description Output columns (detailed): + workspaceId, id Required scope: Catalog.Read.All Unsupported types: Dashboard, Dataflow, Scorecard Includes unit tests (12 tests passing)
…dling Changes based on issue microsoft#172 feedback: - Changed --type from comma-separated to nargs='+' (space-separated) - Removed --endpoint flag (use internal mechanism instead) - Added FabricCLIError for invalid/unsupported item types - Added error handling for API failures - Updated tests to match new patterns (15 tests passing)
- Added complete_item_types() completer for searchable types - Tab completion excludes unsupported types (Dashboard, Dataflow, Scorecard) - Restored unsupported type validation with clear error message - Updated ALL_ITEM_TYPES list from official API spec - Added SEARCHABLE_ITEM_TYPES for valid filter types - 20 tests passing
- Keep tab-completion for --type flag - Custom FabricCLIError for unsupported types (Dashboard, Dataflow, Scorecard) - Custom FabricCLIError for unknown types - Cleaner error messages vs argparse choices listing all 40+ types - 22 tests passing
- Changed from data= to json= for request payload - Added raw_response=True to avoid auto-pagination hanging - Added fallback from displayName to name (API bug workaround) - Updated tests to use dict instead of JSON string - Successfully tested against dailyapi.fabric.microsoft.com
…, workspace_id, description)
…ination and -P params - Interactive mode: pages 50 items at a time with 'Press any key to continue...' - Command-line mode: fetches up to 1000 items in a single request - Replace --type with -P type=Report,Lakehouse (key=value pattern) - Remove --max-items and --next-token flags Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
The Catalog Search API returns an empty string continuationToken on the last page instead of null/omitting it. This caused the interactive pagination loop to send an empty token on the next request, which the API treats as a fresh empty search — returning unrelated results from the entire tenant. Co-authored-by: Copilot <[email protected]>
Long descriptions caused the table separator line to wrap, appearing as a double separator. Descriptions are now truncated with ellipsis to keep the table within the terminal width. Full descriptions are still available via -l/--long mode. Co-authored-by: Copilot <[email protected]>
Command-line mode now fetches all pages instead of stopping at one page of 1000. Uses the same continuation token pattern as interactive mode with 'or None' guard against empty string tokens. Co-authored-by: Copilot <[email protected]>
- Use contractions and active voice for friendlier tone - Suggest close matches for unknown types instead of dumping all 43 - Remove redundant type list from unsupported type error Co-authored-by: Copilot <[email protected]>
| required=False, | ||
| metavar="", | ||
| nargs="*", | ||
| help="Parameters in key=value or key!=value format. Use brackets for multiple values: type=[Lakehouse,Notebook]. Use != to exclude: type!=Dashboard", |
There was a problem hiding this comment.
If you parse it with get_dict_from_params, I think you will need to use quats: type=["Lakehouse","Notebook"]
There was a problem hiding this comment.
Since I'm keeping a dedicated parser, no quotes are needed.
There was a problem hiding this comment.
considering the option I suggest above, using get_dict_from_params, will required this change
There was a problem hiding this comment.
Since I adopted get_dict_from_params with the updated regex, bracket values like type=[Report,SemanticModel] work without quotes. The regex treats brackets as part of the value.
| assert payload["filter"] == "(Type ne 'Dashboard' and Type ne 'Datamart')" | ||
|
|
||
|
|
||
| class TestParseTypeParam: |
There was a problem hiding this comment.
you should create e2e tests that call find command and record the tests and validate all different scenarios instead of those unit tests.
There was a problem hiding this comment.
Done. Added 5 e2e tests with VCR cassettes covering basic search, type filter, long output, no results, and ne filter. I kept the unit tests for pure-function logic since those test edge cases and error paths that are hard to cover with e2e alone. Removed TestFindCommandline and TestFindInteractive.
There was a problem hiding this comment.
I think you should have only the e2e tests - and using that test all the different scenarios that will covered the unit tests.
@ayeshurun please keep me honest
There was a problem hiding this comment.
I converted the test suite to be e2e-first (16 e2e tests now) and removed every unit test that was redundant with an e2e equivalent. 13 remaining unit tests cover edge cases that can't practically be e2e.
Co-authored-by: Copilot <[email protected]>
docs/commands/find.md
Outdated
| fab find 'finance' -P type=[Warehouse,Lakehouse] -l | ||
|
|
||
| # filter results client-side with JMESPath | ||
| fab find 'sales' -q "[?type=='Report']" |
There was a problem hiding this comment.
Isn’t this effectively the same as -P type='Report', but with poorer performance due to fetching all items before filtering? We might want to skip this example since it’s not a best practice.
There was a problem hiding this comment.
I'm not even sure why we added -q in the first place, as all filters should be done server-side anyways imo... I added it as part of the initial review requests.
There was a problem hiding this comment.
-q is usefull for column filtering.
find command return the name, ws name. desc etc... if user want to see only the name, for example, user can use -q for that.
There was a problem hiding this comment.
Updated the docs example to focus on column filtering.
tests/test_commands/test_find.py
Outdated
| """Search for nonexistent term shows 'No items found'.""" | ||
| cli_executor.exec_command("find 'xyznonexistent98765zzz'") | ||
|
|
||
| # print_grey is used for "No items found." but it's not mocked here |
There was a problem hiding this comment.
you have mock_print_grey you can use, but I do believe also in mock_questionary_print you can see the output. if you dont see it that means something is wrong with the test
There was a problem hiding this comment.
I updated the no-results test to assert through mock_print_grey that No items found. is printed.
tests/test_commands/test_find.py
Outdated
| mock_questionary_print, | ||
| ): | ||
| """Search with type!=Dashboard excludes Dashboard items.""" | ||
| cli_executor.exec_command("find 'report' -P type!=Dashboard") |
There was a problem hiding this comment.
we should also test with multiple values like: type!=[Report,Notebook] and verify those types didn't return in the result
There was a problem hiding this comment.
I added a test for multi-value negation with bracket syntax: type!=[Report,Notebook].
There was a problem hiding this comment.
From the test I can see it is working without the quotes... did you verify it in command_line as well? I dontmran here in the tests, I mean manually from the console
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| class TestFindE2E: |
There was a problem hiding this comment.
missing failure scenarios where the API return an error, where the json parse failed, where the type is unknomn etc...
any error we have in the code should be tested.
There was a problem hiding this comment.
I added failure scenario tests for API error responses, malformed JSON responses, and unknown type errors.
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| class TestFindE2E: |
There was a problem hiding this comment.
Relevant for all tests - the name should have prefix of failure or success - depend on the test. you can see examples in any other test's file
There was a problem hiding this comment.
I renamed all test methods to use _success or _failure suffixes.
…st improvements - Replace _split_params with shared get_dict_from_params utility - Update get_dict_from_params regex to support != operator - Move unsupported_parameter/invalid_parameter_format to CommonErrors - Remove unsearchable_type from FindErrors (use CommonErrors.type_not_supported) - Use ERROR_UNEXPECTED_ERROR constant in _raise_on_error - Extract _print_search_summary helper function - Reorder has_more assignment in _find_interactive - Remove JMESPath type filter example and Notes section from docs - Remove TestSplitParams class and legacy comma-syntax tests - Add e2e tests: case-insensitive type, multi-value !=, unknown type, unsupported type - Rename all e2e tests with _success/_failure suffix - Add _assert_strings_in_mock_calls helper for cleaner assertions - Rename cassettes to match new test names, add 2 new cassettes Co-authored-by: Copilot <[email protected]>
- Add e2e tests: invalid param format, unsupported param, unsupported param !=, JMESPath query - Remove redundant unit tests now covered by e2e (payload single type, ne single type, invalid format, unknown param, unknown param !=) - Test split: 26 unit + 13 e2e = 39 tests Co-authored-by: Copilot <[email protected]>
- Add e2e: multi-type eq, JSON output, search summary with count - Remove 14 unit tests now fully covered by e2e (DisplayItems, parse/payload overlaps) - Remove unused imports and sample data constants - Final split: 13 unit + 16 e2e = 29 tests Co-authored-by: Copilot <[email protected]>
| uri: https://api.fabric.microsoft.com/v1/catalog/search | ||
| response: | ||
| body: | ||
| string: '{"value": [{"id": "bb0df270-bbc3-4713-a3f3-47010daa20d5", "type": "KQLQueryset", |
There was a problem hiding this comment.
When recording no private information should be visible in the recording.
Please use this guide: https://github.com/microsoft/fabric-cli/blob/main/tests/authoring_tests.md
There was a problem hiding this comment.
Sanitized all cassettes with mock values from static_test_data.py
tests/test_commands/test_find.py
Outdated
|
|
||
|
|
||
| # Sample API responses for testing | ||
| SAMPLE_RESPONSE_WITH_RESULTS = { |
There was a problem hiding this comment.
we don't want to have a sample response - we should trigger the API and confirm the actual response.
In each test we should add a setup phase where we created/set/update the needed items them call find and rely on the items we created to verify they are in the results. also it's better to use 'mock' data and not actual values of items
There was a problem hiding this comment.
See above responses on this issue.
- Pass max_depth=1 to get_dict_from_params (find uses flat keys only) - Sanitize all VCR cassettes: replace real IDs, workspace names, display names with mock values - Delete unused ErrorMessages.Config.invalid_parameter_format (now in Common) Co-authored-by: Copilot <[email protected]>
|
@aviatco , while testing locally I noticed that print_output_format breaks the table layout when item names exceed the terminal width. I did a small patch for "description" but the issue also reproduces with long item names. I think a more robust fix would be terminal-width-aware column sizing in the shared table renderer ( |
Support both current flat fields (workspaceName, workspaceId) and upcoming nested format (hierarchy.workspace.displayName, .id). Co-authored-by: Copilot <[email protected]>
- Replace truncate_descriptions with truncate_columns: priority-based truncation (description -> workspace -> name), with absolute max width cap (50 chars) and terminal-width fitting - Strip trailing spaces before ellipsis - Show running total in interactive pagination instead of per-page count Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds a new top-level fab find command that searches Fabric items across all accessible workspaces via the Catalog Search API (POST /v1/catalog/search), integrating with existing CLI output/error patterns and adding unit + VCR-backed E2E coverage.
Changes:
- Introduces
findcommand implementation, parser registration, and supported-type allow/deny lists. - Adds a dedicated Catalog API client wrapper for
catalog/search. - Extends shared utilities (
get_dict_from_paramsto support!=, plus a newtruncate_columnshelper) and adds tests + VCR recordings + docs.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/fabric_cli/commands/find/fab_find.py |
Implements find command logic, paging, filtering, JMESPath support, and output formatting. |
src/fabric_cli/commands/find/type_supported.yaml |
Declares searchable vs unsupported item types for catalog search. |
src/fabric_cli/commands/find/__init__.py |
Adds find command package. |
src/fabric_cli/client/fab_api_catalog.py |
Adds API client wrapper for POST /v1/catalog/search. |
src/fabric_cli/parsers/fab_find_parser.py |
Adds argparse parser, flags, help, and examples for find. |
src/fabric_cli/core/fab_parser_setup.py |
Registers find with the global parser setup. |
src/fabric_cli/errors/find.py |
Adds FindErrors message helpers. |
src/fabric_cli/errors/__init__.py |
Registers ErrorMessages.Find. |
src/fabric_cli/errors/common.py |
Adds common error helpers used by param parsing (unsupported_parameter, invalid_parameter_format). |
src/fabric_cli/errors/config.py |
Removes config-specific invalid_parameter_format helper (superseded by common). |
src/fabric_cli/utils/fab_util.py |
Updates param parsing to accept != and adds truncate_columns() for table-fit truncation. |
docs/commands/find.md |
Documents find usage, flags, and examples. |
tests/test_commands/commands_parser.py |
Registers find parser in test CLI harness and adjusts Windows PromptSession handling. |
tests/test_commands/test_find.py |
Adds unit tests + VCR-backed E2E tests for find behaviors. |
tests/test_commands/recordings/test_commands/test_find/test_find_basic_search_success.yaml |
VCR cassette for basic search. |
tests/test_commands/recordings/test_commands/test_find/test_find_with_type_filter_success.yaml |
VCR cassette for type= filtering. |
tests/test_commands/recordings/test_commands/test_find/test_find_type_case_insensitive_success.yaml |
VCR cassette for case-insensitive type=. |
tests/test_commands/recordings/test_commands/test_find/test_find_with_long_output_success.yaml |
VCR cassette for --long output. |
tests/test_commands/recordings/test_commands/test_find/test_find_no_results_success.yaml |
VCR cassette for empty results. |
tests/test_commands/recordings/test_commands/test_find/test_find_with_ne_filter_success.yaml |
VCR cassette for type!= filtering. |
tests/test_commands/recordings/test_commands/test_find/test_find_ne_multi_type_success.yaml |
VCR cassette for type!=[...]. |
tests/test_commands/recordings/test_commands/test_find/test_find_multi_type_eq_success.yaml |
VCR cassette for type=[...]. |
tests/test_commands/recordings/test_commands/test_find/test_find_with_jmespath_query_success.yaml |
VCR cassette for -q/--query behavior. |
tests/test_commands/recordings/test_commands/test_find/test_find_json_output_success.yaml |
VCR cassette for --output_format json. |
tests/test_commands/recordings/test_commands/test_find/test_find_search_summary_success.yaml |
VCR cassette for summary output. |
.changes/unreleased/added-20260209-171617.yaml |
Adds unreleased changelog entry for the new command. |
| params_dict: dict = {} | ||
| # Split the params using a regular expression that matches a comma that is not inside a pair of quotes, brackets or braces | ||
| # Example key1.key2=hello,key2={"hello":"testing","bye":2},key3=[1,2,3],key4={"key5":"value5"} | ||
| # Result ['key1.key2=hello', 'key2={"hello":"testing","bye":2}', 'key3=[1,2,3]', 'key4={"key5":"value5"}'] | ||
| # Example key1.key2=hello | ||
| # Result ['key1.key=hello'] | ||
| pattern = r"((?:[\w\.]+=.+?)(?=(?:,\s*[\w\.]+=)|$))" | ||
| pattern = r"((?:[\w\.]+!?=.+?)(?=(?:,\s*[\w\.]+!?=)|$))" | ||
|
|
||
| if params: | ||
| if isinstance(params, list): | ||
| norm_params = " ".join(params) | ||
| else: | ||
| norm_params = params | ||
|
|
||
| # Remove from multiline | ||
| norm_params = norm_params.replace("\\", "").strip() | ||
|
|
||
| matches = re.findall(pattern, norm_params) | ||
| if not matches: | ||
| raise FabricCLIError( | ||
| ErrorMessages.Config.invalid_parameter_format(norm_params), | ||
| ErrorMessages.Common.invalid_parameter_format(norm_params), | ||
| fab_constant.ERROR_INVALID_INPUT, | ||
| ) |
There was a problem hiding this comment.
The new parameter-regex allows key!=value by treating it as a key! key. Since this changes get_dict_from_params() behavior (and error text) across the CLI, it would help to document the != support directly here (docstring/comment), and consider whether downstream callers should be insulated from !-suffixed keys unless they explicitly opted into that behavior.
There was a problem hiding this comment.
Added a docstring note in get_dict_from_params documenting the != operator support and the !-suffixed key convention (already present at line 72-73 of fab_util.py).
tests/test_commands/test_find.py
Outdated
| assert match_found, f"Expected strings {strings} to {'all be present together' if require_all_in_same_args else 'be present'} in mock calls, but not found." | ||
| else: | ||
| assert not match_found, f"Expected strings {strings} to {'not all be present together' if require_all_in_same_args else 'not be present'} in mock calls, but found." |
There was a problem hiding this comment.
This test file contains a couple of very long f-strings (e.g., the assertion messages in _assert_strings_in_mock_calls) that don't appear to be Black-formatted and may fail black --check in CI. Reformat these assertions (let Black wrap them) to keep the file compliant with the repo formatter.
| assert match_found, f"Expected strings {strings} to {'all be present together' if require_all_in_same_args else 'be present'} in mock calls, but not found." | |
| else: | |
| assert not match_found, f"Expected strings {strings} to {'not all be present together' if require_all_in_same_args else 'not be present'} in mock calls, but found." | |
| assert match_found, ( | |
| f"Expected strings {strings} to " | |
| f"{'all be present together' if require_all_in_same_args else 'be present'} " | |
| "in mock calls, but not found." | |
| ) | |
| else: | |
| assert not match_found, ( | |
| f"Expected strings {strings} to " | |
| f"{'not all be present together' if require_all_in_same_args else 'not be present'} " | |
| "in mock calls, but found." | |
| ) |
There was a problem hiding this comment.
Fixed. Ran black on the test file and source file. Both pass black --check now.
|
|
||
| try: | ||
| utils_ui.print_grey("") | ||
| input("Press any key to continue... (Ctrl+C to stop)") |
There was a problem hiding this comment.
The prompt says "Press any key to continue..." but the implementation uses input(...), which requires pressing Enter (and will echo a newline). Either adjust the prompt text to reflect this, or use an input mechanism that truly reads a single keypress (or a consistent fab_ui prompt helper) in interactive mode.
| input("Press any key to continue... (Ctrl+C to stop)") | |
| input("Press Enter to continue... (Ctrl+C to stop)") |
There was a problem hiding this comment.
The code already says "Press Enter to continue..." (fixed in a prior commit). Updated the docs to match.
| if not show_details: | ||
| utils.truncate_columns(display_items, ["description", "workspace", "name"]) | ||
|
|
||
| if getattr(args, "query", None): | ||
| display_items = utils_jmespath.search(display_items, args.query) | ||
|
|
There was a problem hiding this comment.
_display_items() truncates columns unconditionally for non---long output, which will also mutate the data returned in --output_format json (and can affect -q/--query results). Truncation should be applied only for text/table output (and ideally after any JMESPath filtering/projection), leaving JSON output unmodified for scripting.
| if not show_details: | |
| utils.truncate_columns(display_items, ["description", "workspace", "name"]) | |
| if getattr(args, "query", None): | |
| display_items = utils_jmespath.search(display_items, args.query) | |
| if getattr(args, "query", None): | |
| display_items = utils_jmespath.search(display_items, args.query) | |
| # Apply truncation only for non-detailed, non-JSON outputs, after any JMESPath filtering. | |
| output_format = getattr(args, "output", None) or getattr(args, "output_format", None) | |
| is_json_output = output_format == "json" | |
| if not show_details and not is_json_output: | |
| utils.truncate_columns(display_items, ["description", "workspace", "name"]) |
There was a problem hiding this comment.
Fixed. JMESPath filtering now runs before truncation, and truncation is guarded with output_format == "text".
| all_items.extend(items) | ||
| has_more = continuation_token is not None | ||
| if has_more: | ||
| payload = {"continuationToken": continuation_token} |
There was a problem hiding this comment.
When paging, the code replaces the request payload with only {"continuationToken": ...}. This drops pageSize (and any filter), which can change paging behavior (e.g., command-line mode may fall back to the API default page size on subsequent requests) and increases request count. Keep pageSize/filter consistent across pages (or explicitly include them alongside continuationToken) rather than relying on continuation token semantics.
| payload = {"continuationToken": continuation_token} | |
| payload["continuationToken"] = continuation_token |
There was a problem hiding this comment.
Fixed. continuation payloads now preserve pageSize. The filter is embedded in the continuation token by the API, so re-sending it isn't needed (and the API ignores it on continuation requests).
tests/test_commands/test_find.py
Outdated
| mock_questionary_print, | ||
| ): | ||
| """Search with type!=Dashboard excludes Dashboard items.""" | ||
| cli_executor.exec_command("find 'report' -P type!=Dashboard") |
There was a problem hiding this comment.
From the test I can see it is working without the quotes... did you verify it in command_line as well? I dontmran here in the tests, I mean manually from the console
Source changes: - Restructure _raise_on_error -> _parse_response (check 200 first) - Only truncate columns for text output, not JSON - Change 'Press any key' to 'Press Enter' - Preserve pageSize in continuation payloads - Document != support in get_dict_from_params docstring Test changes: - Switch to item_factory pattern per reviewer request - Remove all unit tests (reviewer: verify via e2e only) - Use ErrorMessage constants in failure assertions - Check errors via mock_fab_ui_print_error (not concatenated mocks) - Merge search summary into basic_search test - Add CatalogSearchAPIProcessor for response filtering - Add dailyapi URI normalization in processors.py - Re-record all cassettes with create-then-search pattern - 15 e2e tests, all passing Co-authored-by: Copilot <[email protected]>
- Fix empty last page: show final count without '(more available)' - Use post-filter count in summary (matches JMESPath -q results) - Move summary before table with correct count via prepare/display split - Proper singular/plural: '1 item found' vs '50 items found' - Fix error hint: replace 'tab completion' with 'find --help' - Add empty line after table output Co-authored-by: Copilot <[email protected]>
| def _load_type_config() -> dict[str, list[str]]: | ||
| """Load item type definitions from type_supported.yaml.""" | ||
| yaml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "type_supported.yaml") | ||
| with open(yaml_path, "r") as f: | ||
| return yaml.safe_load(f) |
There was a problem hiding this comment.
type_supported.yaml is loaded via a direct filesystem path at import time. In the current packaging config, only fabric_cli.core.fab_config/command_support.yaml and fabric_cli.commands.fs/payloads/* are included as package data, so this YAML is likely missing in the installed distribution and will raise FileNotFoundError on import. Consider loading it via importlib.resources (or similar) and ensure the YAML is included in package data/MANIFEST so the command works when installed from PyPI.
There was a problem hiding this comment.
Fixed. Added type_supported.yaml to both pyproject.toml package-data and MANIFEST.in (69451ec).
| # Parse bracket syntax: [val1,val2] or plain: val1 | ||
| if type_value.startswith("[") and type_value.endswith("]"): | ||
| inner = type_value[1:-1] | ||
| types = [t.strip() for t in inner.split(",") if t.strip()] | ||
| else: | ||
| types = [type_value.strip()] | ||
|
|
There was a problem hiding this comment.
The PR description mentions supporting legacy comma syntax like type=Report,Lakehouse, but _parse_type_from_params only splits multiple values when the bracket form ([A,B]) is used. As written, type=Report,Lakehouse will be treated as a single type and fail validation. Either add support for comma-separated values here or update the documented behavior.
There was a problem hiding this comment.
By design — bracket syntax (type=[Report,Lakehouse]) is the documented way to specify multiple types, consistent with other CLI commands like mkdir -P paths=[Files,Tables]. The comma in get_dict_from_params is a key-value pair separator, not a value list separator, so type=Report,Lakehouse would be ambiguous.
| output_format = getattr(args, "output_format", "text") or "text" | ||
| if not show_details and output_format == "text": | ||
| utils.truncate_columns(display_items, ["description", "workspace", "name"]) | ||
|
|
||
| if getattr(args, "query", None): | ||
| display_items = utils_jmespath.search(display_items, args.query) | ||
|
|
||
| return display_items if isinstance(display_items, list) else [] |
There was a problem hiding this comment.
JMESPath querying is applied after truncate_columns, and non-list query results are discarded (return ... if isinstance(..., list) else []). This can break valid queries that return a dict/scalar and can also cause filtering/projection to operate on already-truncated strings. Apply the JMESPath query before truncation, and pass through dict/scalar results to print_output_format instead of forcing a list.
| "-P", | ||
| "--params", | ||
| metavar="", | ||
| nargs="?", |
There was a problem hiding this comment.
--params/-P is defined with nargs='?', so a user can pass -P with no value and the command will silently ignore the flag (no validation error, no filter applied). If -P is intended to always take a value, define it without an optional nargs and/or validate that args.params is not None when the flag is present.
| nargs="?", |
There was a problem hiding this comment.
This is consistent with how optional params work in the CLI. Passing -P alone is equivalent to not passing it. Adding validation for this edge case would break the argparse pattern used by other commands.
docs/commands/find.md
Outdated
|
|
||
| **Behavior:** | ||
|
|
||
| - In interactive mode (`fab` shell), results are paged 50 at a time with "Press any key to continue..." prompts. |
There was a problem hiding this comment.
Docs say interactive mode prompts "Press any key to continue...", but the implementation currently prompts with "Press Enter to continue...". Please align the documentation with the actual prompt (or update the prompt to match the docs) so users aren’t confused.
| - In interactive mode (`fab` shell), results are paged 50 at a time with "Press any key to continue..." prompts. | |
| - In interactive mode (`fab` shell), results are paged 50 at a time with "Press Enter to continue..." prompts. |
There was a problem hiding this comment.
Fixed — docs now say "Press Enter to continue..." (69451ec).
| class CatalogSearchAPIProcessor(BaseAPIProcessor): | ||
| CATALOG_SEARCH_URI = "https://api.fabric.microsoft.com/v1/catalog/search" | ||
|
|
||
| def __init__(self, generated_name_mapping): | ||
| self.generated_name_mapping = generated_name_mapping | ||
|
|
||
| def try_process_request(self, request) -> bool: | ||
| return False | ||
|
|
||
| def try_process_response(self, request, response) -> bool: | ||
| uri = request.uri | ||
|
|
||
| if uri.lower() == self.CATALOG_SEARCH_URI.lower(): | ||
| method = request.method | ||
| if method == "POST": | ||
| self._handle_search_response(response) | ||
| return True | ||
|
|
There was a problem hiding this comment.
This API processor isn’t currently wired into APIProcessorHandler (which only registers capacities/workspaces/gateways/etc.), so it will never run and the catalog/search responses won’t be scrubbed/trimmed during recording. Register this processor in the handler so the find VCR cassettes only include deterministic, test-generated catalog entries (and avoid leaking unrelated tenant data).
There was a problem hiding this comment.
Fixed — CatalogSearchAPIProcessor is now registered in APIProcessorHandler (69451ec). It will scrub catalog search responses during future recordings.
|
|
||
| self._interactiveCLI = InteractiveCLI(customArgumentParser, self._parser) | ||
|
|
There was a problem hiding this comment.
The Windows workaround patches fabric_cli.core.fab_interactive.PromptSession at module scope and never restores it. This can leak into other tests in the same process and make failures harder to diagnose. Consider restoring _interactive_mod.PromptSession to _orig_ps after InteractiveCLI(...) is constructed (or patch via a fixture/monkeypatch context) to keep the change scoped to this executor.
| self._interactiveCLI = InteractiveCLI(customArgumentParser, self._parser) | |
| try: | |
| self._interactiveCLI = InteractiveCLI( | |
| customArgumentParser, self._parser | |
| ) | |
| finally: | |
| _interactive_mod.PromptSession = _orig_ps | |
| else: | |
| self._interactiveCLI = InteractiveCLI(customArgumentParser, self._parser) |
There was a problem hiding this comment.
Fixed — wrapped in try/finally so the original PromptSession is restored after the singleton is created (69451ec).
- Validate empty bracket syntax (-P type=[]) with clear error - Apply JMESPath filtering before column truncation - Include header name length in truncate_columns width calc - Add type_supported.yaml to package-data and MANIFEST.in - Register CatalogSearchAPIProcessor in handler - Restore PromptSession patch in test executor (try/finally) - Fix docs: 'Press any key' -> 'Press Enter' - Black formatting pass Co-authored-by: Copilot <[email protected]>
Remove query text echo from status message. The user already sees what they typed in the terminal — echoing the search text without filters was inconsistent, and echoing everything would be noisy. Co-authored-by: Copilot <[email protected]>
…ead code - Collapse 4-way eq/ne filter branch into uniform clause builder - Extract _next_page_payload helper to deduplicate continuation logic - Simplify pagination loops to while True + break (no has_more flag) - Remove unused SEARCHABLE_ITEM_TYPES constant - Delete orphaned test_find_search_summary_success.yaml cassette Co-authored-by: Copilot <[email protected]>
| has_more = True | ||
|
|
||
| while has_more: | ||
| while True: |
There was a problem hiding this comment.
why did you change it back to while Ture? better to use has_more since this is the exit condition
| has_more = True | ||
|
|
||
| while has_more: | ||
| while True: |
There was a problem hiding this comment.
same here -
while continuation_token:
| error_code = fab_constant.ERROR_UNEXPECTED_ERROR | ||
| error_message = response.text | ||
| raise FabricCLIError( | ||
| ErrorMessages.Find.invalid_response(), |
There was a problem hiding this comment.
redundent - you have invalid_json_format error in ErrorMessages.Common
| error_data = json.loads(response.text) | ||
| error_code = error_data.get("errorCode", fab_constant.ERROR_UNEXPECTED_ERROR) | ||
| error_message = error_data.get("message", response.text) | ||
| except json.JSONDecodeError: |
There was a problem hiding this comment.
what can be the values of response.text? json/string?
cause if we got to this point this is not ERROR_UNEXPECTED_ERROR it is ERROR_INVALID_JSON
| error_message = response.text | ||
|
|
||
| raise FabricCLIError( | ||
| ErrorMessages.Find.search_failed(error_message), |
There was a problem hiding this comment.
consider print the error message from the API with logger.debug and for the user use something simple and clear
| def _raise_on_error(response) -> None: | ||
| """Raise FabricCLIError if the API response indicates failure.""" | ||
| if response.status_code != 200: | ||
| def _parse_response(response) -> dict: |
There was a problem hiding this comment.
nit - consider rewrite the function (considering response.text is always a json object and getting json.JSONDecodeError is always thrown cause the response.text is an invalid json format ):
"""Parse a successful API response or raise FabricCLIError on failure."""
try:
data = json.loads(response.text)
if response.status_code == 200:
return data
raise FabricCLIError(
ErrorMessages.Find.search_failed(data.get("message", response.text)),
data.get("errorCode", fab_constant.ERROR_UNEXPECTED_ERROR),
)
except json.JSONDecodeError:
if response.status_code == 200:
raise FabricCLIError(
ErrorMessages.Find.invalid_response(),
fab_constant.ERROR_INVALID_JSON,
)
| return [] | ||
|
|
||
| output_format = getattr(args, "output_format", "text") or "text" | ||
| if not show_details and output_format == "text": |
There was a problem hiding this comment.
you shouldn't use output_format in any command logic.
fab_ui is the only component that should be aware of this field.
|
|
||
| output_format = getattr(args, "output_format", "text") or "text" | ||
| if not show_details and output_format == "text": | ||
| utils.truncate_columns(display_items, ["description", "workspace", "name"]) |
There was a problem hiding this comment.
paraphs move this logic to fab_ui where output_format is text and show_details is false.
but need to verify this will not cause regression in other commands
Or any other solution, but you should not use output_format value here
Pull Request
Closes #172
Summary
Adds a new
findcommand that searches for Fabric items across all accessible workspaces using the Catalog Search API (POST /v1/catalog/search).Usage
Flags
-P/--paramskey=valueorkey!=valueformat. Brackets for multiple values:type=[Lakehouse,Notebook]-l/--long-q/--queryImplementation details
@handle_exceptionsdecorator,print_output_format(),FabricCLIErrortype_supported.yaml(included in package-data, matchingcommand_supported.yamlpattern)ErrorMessages.Findclass (matches repo pattern)_parse_type_from_paramsusesget_dict_from_paramsfor parameter parsing with!=regex support_fetch_resultsshared helper for response parsing and paginationtruncate_columnsinfab_util.pyfor terminal-width-aware column sizing (reusable by other commands)Files added
src/fabric_cli/client/fab_api_catalog.py— API client (search()function)src/fabric_cli/commands/find/__init__.py— Package initsrc/fabric_cli/commands/find/fab_find.py— Command logicsrc/fabric_cli/commands/find/type_supported.yaml— Supported/unsupported item typessrc/fabric_cli/errors/find.py—FindErrorsclasssrc/fabric_cli/parsers/fab_find_parser.py— Argument parserdocs/commands/find.md— Command documentationtests/test_commands/test_find.py— 15 e2e VCR teststests/test_commands/recordings/test_commands/test_find/*.yaml— VCR cassettestests/test_commands/api_processors/catalog_search_api_processor.py— Cassette processor for catalog search responsesFiles modified
src/fabric_cli/core/fab_parser_setup.py— Register find parsersrc/fabric_cli/errors/__init__.py— RegisterFinderror classsrc/fabric_cli/utils/fab_util.py— Addtruncate_columns(),!=regex support inget_dict_from_paramstests/test_commands/commands_parser.py— Register find parser + Windows PromptSession fix (scoped with try/finally)tests/test_commands/api_processors/api_processor_handler.py— RegisterCatalogSearchAPIProcessorpyproject.toml— Addtype_supported.yamlto package-dataMANIFEST.in— Addtype_supported.yamlto source distributionNotes
Dataflow)ReportCatalog.Read.Allscope