Skip to content

feat: support bulk-export command#261

Open
ohadedry wants to merge 4 commits into
microsoft:mainfrom
ohadedry:support-bulk-export-command
Open

feat: support bulk-export command#261
ohadedry wants to merge 4 commits into
microsoft:mainfrom
ohadedry:support-bulk-export-command

Conversation

@ohadedry

@ohadedry ohadedry commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

✨ Description of new changes

This pull request introduces a new bulk-export command to the CLI, enabling bulk export of all supported items from a workspace or folder while preserving folder structure and item bindings. The implementation includes API integration, argument validation, error handling, and comprehensive documentation and examples. The changes also update command support configuration and the command index.

New Functionality:

  • Added a new bulk-export command that allows users to export all supported items from a workspace or folder in a single operation, preserving folder structure and item bindings. This includes both CLI logic and integration with the backend API. [1] [2] [3] [4] [5]

Documentation Updates:

  • Added a new documentation page bulk_export.md detailing usage, parameters, supported item types, output structure, and examples for the bulk-export command.
  • Updated the command index and navigation to include bulk-export. [1] [2]
  • Added example usage sections for bulk-exporting workspaces and folders in the relevant examples files. [1] [2]

Command Support and Configuration:

  • Registered the new bulkexport command in the CLI command enumeration and in the command support YAML, specifying supported elements (workspace, folder) and item types. [1] [2]

Internal Integration and Validation:

  • Integrated the bulk-export command into the main command handler and ensured argument validation, context checking, and user confirmation logic are in place. [1] [2] [3]
  • Improved error handling and user prompts for export preconditions, including checks for recursive export and non-empty output folders.

Minor Improvements:

  • Updated logging in the API client to include command context for improved traceability.
  • Added copyright headers to new files.

These changes collectively introduce and document the new bulk-export feature, making it easier for users to export entire workspaces or folders with structure and bindings preserved.# 📥 Pull Request

Copilot AI review requested due to automatic review settings July 6, 2026 10:51
@ohadedry ohadedry requested a review from a team as a code owner July 6, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new bulk-export command to Fabric CLI (fab) to export all supported items from a workspace or folder in one operation, preserving folder structure and item bindings. It includes end-to-end command wiring (parser → command handler → API client), export utilities for writing bulk-export definition parts to storage, debug-log redaction improvements, and corresponding documentation + tests (including VCR recordings).

Changes:

  • Implemented bulk-export command flow (parsing, command dispatch, workspace/folder execution paths, API call, and export-to-storage utilities).
  • Extended export utilities and HTTP debug logging to better support bulk export payload shapes and reduce noisy/large debug logs.
  • Added docs pages/examples and comprehensive tests + recordings for the new command.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/test_utils/test_fab_cmd_bulk_export_utils.py Unit tests for bulk-export utility helpers (payload creation, summary output, path stripping/export).
tests/test_core/test_fab_logger.py Updates logger tests for new command-context-aware response logging/redaction.
tests/test_commands/test_bulk_export.py New command-level tests for bulk-export behavior and output structure.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_workspace_without_recursive_fail.yaml VCR recording for workspace bulk-export missing --recursive.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_workspace_folder_without_recursive_fail.yaml VCR recording for folder bulk-export missing --recursive.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_output_path_not_empty_warning.yaml VCR recording for warning behavior when output directory is non-empty.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_no_exportable_items_fail.yaml VCR recording for “no exportable items” failure case.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_item_fail.yaml VCR recording for invalid target type (item path).
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_empty_workspace_fail.yaml VCR recording for empty workspace failure case.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_empty_folder_fail.yaml VCR recording for empty folder failure case.
tests/test_commands/recordings/test_commands/test_bulk_export/class_setup.yaml VCR class setup recording for bulk-export command tests.
tests/test_commands/commands_parser.py Registers the new bulk-export parser into the command test harness.
src/fabric_cli/utils/fab_commands.py Adds bulk-export to the CLI command index text.
src/fabric_cli/utils/fab_cmd_export_utils.py Extends decode/export helpers to support definitionParts payload shape.
src/fabric_cli/utils/fab_cmd_bulk_export_utils.py New bulk-export utility module (payload, export writing, summary printing, path validation).
src/fabric_cli/parsers/fab_fs_parser.py Adds argparse registration for the bulk-export command.
src/fabric_cli/errors/bulk_export.py Defines bulk-export-specific error messages.
src/fabric_cli/core/fab_parser_setup.py Wires bulk-export parser into the global CLI parser setup.
src/fabric_cli/core/fab_logger.py Adds command context to response logging + JSON response redaction logic.
src/fabric_cli/core/fab_constant.py Adds COMMAND_FS_BULKEXPORT_DESCRIPTION.
src/fabric_cli/core/fab_config/command_support.yaml Registers bulk-export support matrix (workspace/folder + supported item types).
src/fabric_cli/core/fab_commands.py Adds FS_BULKEXPORT command enum value.
src/fabric_cli/commands/fs/fab_fs.py Hooks bulk_export_command into the fs command module.
src/fabric_cli/commands/fs/fab_fs_bulk_export.py New bulk-export command implementation and precondition checks.
src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_workspace.py Workspace bulk-export execution and response filtering.
src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_folder.py Folder bulk-export execution and recursive item collection.
src/fabric_cli/commands/fs/bulk_export/init.py Package init for bulk-export submodule.
src/fabric_cli/client/fab_api_item.py Adds bulk_export_definitions() API wrapper.
src/fabric_cli/client/fab_api_client.py Passes command context into HTTP response debug logging.
mkdocs.yml Adds bulk-export page to MkDocs navigation.
docs/examples/workspace_examples.md Adds workspace bulk-export example section.
docs/examples/folder_examples.md Adds folder bulk-export example section.
docs/commands/index.md Adds bulk-export to the command index page.
docs/commands/fs/bulk_export.md New bulk-export command documentation page.

Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py
Comment thread src/fabric_cli/parsers/fab_fs_parser.py Outdated
Comment thread src/fabric_cli/parsers/fab_fs_parser.py Outdated
Comment thread src/fabric_cli/utils/fab_commands.py
Comment thread src/fabric_cli/core/fab_constant.py
Comment thread docs/commands/fs/bulk_export.md
Comment thread tests/test_commands/test_bulk_export.py
Comment thread docs/commands/fs/bulk_export.md
!!! warning "When exporting items, the item definitions are exported without their sensitivity labels"

!!! note "Permissions"
- **Workspace export**: Only items that the caller has both read and write permissions for are exported.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read & write -> if i have write permissions, i probably have read :) we could say - the caller has write permissions


!!! note "Permissions"
- **Workspace export**: Only items that the caller has both read and write permissions for are exported.
- **Folder export**: The caller must have read and write permissions for all folder items.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here - writer permissions


**Supported Item Types:**

`CopyJob`, `CosmosDBDatabase`, `Dataflow`, `DataPipeline`, `DigitalTwinBuilder`, `Environment`, `Eventhouse`, `Eventstream`, `GraphQLApi`, `GraphQuerySet`, `KQLDashboard`, `KQLDatabase`, `KQLQueryset`, `Lakehouse`, `Map`, `MirroredDatabase`, `MLExperiment`, `MLModel`, `MountedDataFactory`, `Notebook`, `Reflex`, `Report`, `SemanticModel`, `SparkJobDefinition`, `SQLDatabase`, `UserDataFunction`, `VariableLibrary`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this list different from the supported items in export ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does. It has 2 more items which are not supported in export and export has 1 item which is not supported in bulk-export (you can compare the lists in command_support.yaml)
I'm not sure we should have this list in documentation file as it requires maintenance. Also, we print the supported and unsupported items as part of the command output, WDYT?

Comment on lines +86 to +103
```
<output_path>/
├── Folder1/
│ ├── notebook1.Notebook/
│ │ ├── .platform
│ │ └── notebook-content.ipynb
│ └── report1.Report/
│ ├── .platform
│ ├── definition.pbir
│ └── StaticResources/
│ └── ...
└── Folder2/
└── model1.SemanticModel/
├── .platform
└── definition/
├── model.tmdl
└── ...
```

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
```
<output_path>/
├── Folder1/
│ ├── notebook1.Notebook/
│ │ ├── .platform
│ │ └── notebook-content.ipynb
│ └── report1.Report/
│ ├── .platform
│ ├── definition.pbir
│ └── StaticResources/
│ └── ...
└── Folder2/
└── model1.SemanticModel/
├── .platform
└── definition/
├── model.tmdl
└── ...
```

<output_path>/
|----- rti1.KQLDashboard/
|-----....
|-----...
├── Folder1/
│ ├── notebook1.Notebook/
│ │ ├── .platform
│ │ └── notebook-content.ipynb
│ └── report1.Report/
│ ├── .platform
│ ├── definition.pbir
│ └── StaticResources/
│ └── ...
└── Folder2/
└── model1.SemanticModel/
├── .platform
└── definition/
├── model.tmdl
└── ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you suggest changing here?

Comment thread docs/commands/index.md
| Command | Description |
|---------|-------------|
| [`assign`](./fs/assign.md) | Assign a resource to a workspace |
| [`bulk-export`](./fs/bulk_export.md) | Bulk export of Folder or Workspace |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: export Folder and Workspace items in bulk


### Bulk-Export Folder Items

!!! info "When you bulk-export item definitions, the sensitivity labels are not part of the definitions"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
!!! info "When you bulk-export item definitions, the sensitivity labels are not part of the definitions"
!!! info "When you execute the `bulk-export` command, the sensitivity labels are not part of the exported definitions"

Comment on lines +118 to +119
response = fabric_api.do_request(args, data=payload)
return json.loads(response.text)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most of the methods here do return fabric_api.do_request(). why we want to parse the response before?

)
bulk_export_parser.add_argument(
"--recursive",
required=False,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not True?

Comment on lines +16 to +17
elif "definitionParts" in item_def:
parts = item_def["definitionParts"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come we support both in export command?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getItemDefinition and bulk export APIs returns similar payload structure but under different response structure so I reused it

Comment thread src/fabric_cli/utils/fab_commands.py
Comment on lines +55 to +59
if not item_definitions:
raise FabricCLIError(
BulkExportErrors.no_definition_returned(artifact_name),
fab_constant.ERROR_INVALID_DEFINITION_PAYLOAD,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it's an error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it's seems like an API error but I thought it will be good to indicate about it to the user, WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it's seems like an API error but I thought it will be good to indicate about it to the user, WDYT?

_validate_definition_parts_paths_are_under_export_path(
item_def, export_path["path"]
)
os.makedirs(export_path["path"], exist_ok=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i expect this line will be as part of the export utility

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to have it for export command as well?

Copilot AI review requested due to automatic review settings July 8, 2026 10:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 7 comments.

Comment on lines +60 to +74
if export_path["type"] == "local" and os.path.isdir(export_path["path"]):
is_export_path_empty = True
with os.scandir(export_path["path"]) as entries:
is_export_path_empty = not any(entries)
if not is_export_path_empty:
if not args.force:
if not fab_ui.prompt_confirm(
f"Output folder '{export_path['path']}' is not empty. Do you want to proceed?"
):
return False
else:
export_path_warning = True
elif export_path["type"] == "local":
os.makedirs(export_path["path"], exist_ok=True)
else:
Comment on lines +74 to +78
"skipped": 1,
"skipped_types": {
"Dashboard": 1,
},
"output": "/tmp"
Comment on lines +150 to +151
mock_print_warning.assert_called_with(
"Exporting to a non-empty output folder")
Comment on lines +219 to +228
export_path1 = (
tmp_path / folder1.display_name /
f"{notebook1.display_name}.Notebook"
)
export_path2 = (
tmp_path
/ folder1.display_name
/ folder2.display_name
/ f"{notebook2.display_name}.Notebook"
)
Comment on lines +335 to +336
data = call_kwargs.kwargs.get(
"data", call_kwargs[1].get("data", []))
Comment on lines +341 to +344
export_path = (
tmp_path / folder.display_name /
f"{notebook.display_name}.Notebook"
)
Comment on lines +184 to +187
export_path = (
tmp_path / folder2.display_name /
f"{notebook2.display_name}.Notebook"
)
Copilot AI review requested due to automatic review settings July 8, 2026 12:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 6 comments.

Comment on lines +72 to +73
elif export_path["type"] == "local":
os.makedirs(export_path["path"], exist_ok=True)
Comment on lines +141 to +151
prefix = "/" + "/".join(segments)

for part in item_def.get("definitionParts", []):
path = part.get("path", "")
if path.startswith(prefix):
part["path"] = path[len(prefix) :]
else:
raise FabricCLIError(
BulkExportErrors.path_mismatch(),
fab_constant.ERROR_INVALID_DEFINITION_PAYLOAD,
)
f"{item_type} ({count})"
for item_type, count in unsupported_types_count.items()
]
output_format_message += f". Skipped {unsupported_count} items due to unsupported item types: {', '.join(unsupported_types)}"
Comment on lines +87 to +96
# find unsupported items that were not in the response index and add them to the unsupported_items_list
supported_item_ids: set[str] = {item.id for item in supported_items_list}
unsupported_item_ids: set[str] = {item.id for item in unsupported_items_list}
for ws_item in ws_items:
if (
ws_item.id not in supported_item_ids
and ws_item.id not in unsupported_item_ids
):
unsupported_items_list.append(ws_item)

Comment on lines 130 to +134
monkeypatch.setattr(fab_state_config, "get_config", lambda x: "1")
logger.log_debug_http_response(200, {}, "Response text", time.time())
logger.log_debug_http_response(200, {}, "Response text", time.time(), "cmd")

monkeypatch.setattr(fab_state_config, "get_config", lambda x: "0")
logger.log_debug_http_response(200, {}, "Response text", time.time())
logger.log_debug_http_response(200, {}, "Response text", time.time(), "cmd")
Comment on lines +75 to +78
"skipped_types": {
"Dashboard": 1,
},
"output": "/tmp"
Copilot AI review requested due to automatic review settings July 8, 2026 12:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/fabric_cli/utils/fab_cmd_export_utils.py:63

  • The docstring still says this function exports from the 'parts' array, but the implementation now supports an arbitrary key via definition_parts (e.g., definitionParts for bulk-export). Updating the docstring avoids misleading future callers.
    """
    Export each 'payload' in the 'parts' array to a file named based on 'path' in 'parts'.
    The 'payload' content will be saved as the file's content.
    """

Comment on lines 66 to 70
register_rm_parser,
register_mkdir_parser,
register_jobs_parser,
register_bulk_export_parser,
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where twice? Please explain

Comment on lines +60 to +78
if export_path["type"] == "local" and os.path.isdir(export_path["path"]):
is_export_path_empty = True
with os.scandir(export_path["path"]) as entries:
is_export_path_empty = not any(entries)
if not is_export_path_empty:
if not args.force:
if not fab_ui.prompt_confirm(
f"Output folder '{export_path['path']}' is not empty. Do you want to proceed?"
):
return False
else:
export_path_warning = True
elif export_path["type"] == "local":
os.makedirs(export_path["path"], exist_ok=True)
else:
raise FabricCLIError(
BulkExportErrors.invalid_export_path(export_path["path"]),
fab_constant.ERROR_INVALID_OPERATION,
)
Comment on lines +87 to +96
# find unsupported items that were not in the response index and add them to the unsupported_items_list
supported_item_ids: set[str] = {item.id for item in supported_items_list}
unsupported_item_ids: set[str] = {item.id for item in unsupported_items_list}
for ws_item in ws_items:
if (
ws_item.id not in supported_item_ids
and ws_item.id not in unsupported_item_ids
):
unsupported_items_list.append(ws_item)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants