diff --git a/src/fabric_cli/client/fab_api_item.py b/src/fabric_cli/client/fab_api_item.py index 3d3837dd4..117661bd6 100644 --- a/src/fabric_cli/client/fab_api_item.py +++ b/src/fabric_cli/client/fab_api_item.py @@ -100,7 +100,9 @@ def get_item( def get_item_definition(args: Namespace) -> ApiResponse: """https://learn.microsoft.com/en-us/rest/api/fabric/core/items/get-item-definition""" - args.uri = f"workspaces/{args.ws_id}/items/{args.id}/getDefinition{args.format}" + args.uri = f"workspaces/{args.ws_id}/items/{args.id}/getDefinition" + if args.format: + args.uri += f"?format={args.format}" args.method = "post" args.wait = True diff --git a/src/fabric_cli/commands/fs/export/fab_fs_export_item.py b/src/fabric_cli/commands/fs/export/fab_fs_export_item.py index db11cf5d7..56e1d5d14 100644 --- a/src/fabric_cli/commands/fs/export/fab_fs_export_item.py +++ b/src/fabric_cli/commands/fs/export/fab_fs_export_item.py @@ -10,10 +10,9 @@ from fabric_cli.core import fab_constant from fabric_cli.core.fab_commands import Command from fabric_cli.core.fab_exceptions import FabricCLIError -from fabric_cli.core.fab_types import ItemType, definition_format_mapping +from fabric_cli.core.fab_types import ItemType from fabric_cli.core.hiearchy.fab_folder import Folder from fabric_cli.core.hiearchy.fab_hiearchy import Item, Workspace -from fabric_cli.errors import ErrorMessages from fabric_cli.utils import fab_cmd_export_utils as utils_export from fabric_cli.utils import fab_item_util, fab_mem_store, fab_storage, fab_ui @@ -103,27 +102,8 @@ def export_single_item( args.from_path = item.path.strip("/") args.ws_id, args.id, args.item_type = workspace_id, item_id, str(item_type) - # Get definition_format_mapping for item without default fallback - valid_export_formats = definition_format_mapping.get(item_type, {}) - # Get export_format_param from args without default export_format_param = getattr(args, "format", None) - - if export_format_param not in valid_export_formats: - # Export format not in definition_format_mapping - if not export_format_param: - # Empty format param - use default formats if exists - args.format = valid_export_formats.get("default", "") - else: - # Non-empty format param but not supported - available_formats = [k for k in valid_export_formats.keys() if k != "default"] - raise FabricCLIError( - ErrorMessages.Export.invalid_export_format(available_formats), - fab_constant.ERROR_INVALID_INPUT, - ) - else: - # Export format is explicitly supported - args.format = valid_export_formats[export_format_param] - + args.format = fab_item_util.resolve_definition_format(item_type, export_format_param) item_def = item_api.get_item_withdefinition(args, item_uri) diff --git a/src/fabric_cli/commands/fs/impor/fab_fs_import_item.py b/src/fabric_cli/commands/fs/impor/fab_fs_import_item.py index a7765b6bb..b3883699b 100644 --- a/src/fabric_cli/commands/fs/impor/fab_fs_import_item.py +++ b/src/fabric_cli/commands/fs/impor/fab_fs_import_item.py @@ -8,33 +8,19 @@ from fabric_cli.client.fab_api_types import ApiResponse from fabric_cli.core import fab_constant, fab_logger from fabric_cli.core.fab_exceptions import FabricCLIError -from fabric_cli.core.fab_types import ItemType, definition_format_mapping +from fabric_cli.core.fab_types import ItemType from fabric_cli.core.hiearchy.fab_hiearchy import Item from fabric_cli.utils import fab_cmd_import_utils as utils_import +from fabric_cli.utils import fab_item_util from fabric_cli.utils import fab_mem_store as utils_mem_store from fabric_cli.utils import fab_storage as utils_storage from fabric_cli.utils import fab_ui as utils_ui def import_single_item(item: Item, args: Namespace) -> None: - _input_format = None - if args.format: - _input_format = args.format - if item.item_type in definition_format_mapping: - valid_formats = list( - definition_format_mapping[item.item_type].keys()) - if _input_format not in valid_formats: - available_formats = [ - k for k in valid_formats if k != "default"] - raise FabricCLIError( - f"Invalid format. Only the following formats are supported: {', '.join(available_formats)}", - fab_constant.ERROR_INVALID_INPUT, - ) - else: - raise FabricCLIError( - f"Invalid format. No formats are supported", - fab_constant.ERROR_INVALID_INPUT, - ) + _input_format = fab_item_util.resolve_definition_format( + item_type=item.item_type, format_param=getattr(args, "format", None) + ) args.ws_id = item.workspace.id input_path = utils_storage.get_import_path(args.input) @@ -54,7 +40,7 @@ def import_single_item(item: Item, args: Namespace) -> None: # Get the payload payload = utils_import.get_payload_for_item_type( - _input_path, item, _input_format + _input_path, item, input_format=_input_format ) if item_exists: diff --git a/src/fabric_cli/commands/fs/set/fab_fs_set_item.py b/src/fabric_cli/commands/fs/set/fab_fs_set_item.py index 7fea4ef8c..9d1c61008 100644 --- a/src/fabric_cli/commands/fs/set/fab_fs_set_item.py +++ b/src/fabric_cli/commands/fs/set/fab_fs_set_item.py @@ -33,19 +33,24 @@ def exec(item: Item, args: Namespace) -> None: args.item_uri = format_mapping.get(item.item_type, "items") if query_value.startswith(fab_constant.ITEM_QUERY_DEFINITION): - formats = definition_format_mapping.get(item.item_type, {"default": ""}) + formats = definition_format_mapping.get( + item.item_type, {"default": ""}) + # plain value; query param built in get_item_definition() args.format = formats["default"] def_response = item_api.get_item_definition(args) definition = json.loads(def_response.text) - updated_def = _update_item_definition(definition, query_value, args.input) + updated_def = _update_item_definition( + definition, query_value, args.input) update_item_definition_payload = json.dumps(updated_def) utils_ui.print_grey(f"Setting new property for '{item.name}'...") - item_api.update_item_definition(args, update_item_definition_payload) + item_api.update_item_definition( + args, update_item_definition_payload) else: - item_metadata = json.loads(item_api.get_item(args, item_uri=True).text) + item_metadata = json.loads( + item_api.get_item(args, item_uri=True).text) update_payload_dict = _update_item_metadata( item_metadata, query_value, args.input diff --git a/src/fabric_cli/core/fab_types.py b/src/fabric_cli/core/fab_types.py index e200296ec..99860d0ff 100644 --- a/src/fabric_cli/core/fab_types.py +++ b/src/fabric_cli/core/fab_types.py @@ -583,19 +583,19 @@ class MirroredDatabaseFolders(Enum): definition_format_mapping = { ItemType.SPARK_JOB_DEFINITION: { - "default": "?format=SparkJobDefinitionV1", - "SparkJobDefinitionV1": "?format=SparkJobDefinitionV1", - "SparkJobDefinitionV2": "?format=SparkJobDefinitionV2", + "default": "SparkJobDefinitionV1", + "SparkJobDefinitionV1": "SparkJobDefinitionV1", + "SparkJobDefinitionV2": "SparkJobDefinitionV2", }, ItemType.NOTEBOOK: { - "default": "?format=ipynb", - ".py": "?format=fabricGitSource", - ".ipynb": "?format=ipynb", + "default": "ipynb", + ".py": "fabricGitSource", + ".ipynb": "ipynb", }, ItemType.SEMANTIC_MODEL: { "default": "", - "TMDL": "?format=TMDL", - "TMSL": "?format=TMSL", + "TMDL": "TMDL", + "TMSL": "TMSL", }, ItemType.COSMOS_DB_DATABASE: {"default": ""}, ItemType.USER_DATA_FUNCTION: {"default": ""}, diff --git a/src/fabric_cli/core/hiearchy/fab_item.py b/src/fabric_cli/core/hiearchy/fab_item.py index 0707cb187..bf86e56e4 100644 --- a/src/fabric_cli/core/hiearchy/fab_item.py +++ b/src/fabric_cli/core/hiearchy/fab_item.py @@ -74,89 +74,5 @@ def workspace(self) -> Workspace: assert isinstance(self.parent, Folder) return self.parent.workspace - def get_payload(self, definition, input_format=None) -> dict: - match self.item_type: - - case ItemType.SPARK_JOB_DEFINITION: - return { - "type": str(self.item_type), - "description": "Imported from fab", - "folderId": self.folder_id, - "displayName": self.short_name, - "definition": { - "format": ( - "SparkJobDefinitionV1" - if input_format is None - else input_format - ), - "parts": definition["parts"], - }, - } - case ItemType.NOTEBOOK: - return { - "type": str(self.item_type), - "description": "Imported from fab", - "folderId": self.folder_id, - "displayName": self.short_name, - "definition": { - **( - {"parts": definition["parts"]} - if input_format == ".py" - else {"format": "ipynb", "parts": definition["parts"]} - ) - }, - } - case ItemType.SEMANTIC_MODEL: - return { - "type": str(self.item_type), - "description": "Imported from fab", - "folderId": self.folder_id, - "displayName": self.short_name, - "definition": ( - definition - if input_format is None - else { - "format": input_format, - "parts": definition["parts"], - } - ), - } - case ( - ItemType.REPORT - | ItemType.KQL_DASHBOARD - | ItemType.DATA_PIPELINE - | ItemType.KQL_QUERYSET - | ItemType.EVENTHOUSE - | ItemType.KQL_DATABASE - | ItemType.MIRRORED_DATABASE - | ItemType.DIGITAL_TWIN_BUILDER - | ItemType.REFLEX - | ItemType.EVENTSTREAM - | ItemType.MOUNTED_DATA_FACTORY - | ItemType.COPYJOB - | ItemType.VARIABLE_LIBRARY - | ItemType.GRAPHQLAPI - | ItemType.DATAFLOW - | ItemType.SQL_DATABASE - | ItemType.COSMOS_DB_DATABASE - | ItemType.GRAPH_QUERY_SET - | ItemType.USER_DATA_FUNCTION - | ItemType.MAP - ): - return { - "type": str(self.item_type), - "description": "Imported from fab", - "folderId": self.folder_id, - "displayName": self.short_name, - "definition": definition, - } - case _: - raise FabricCLIError( - ErrorMessages.Hierarchy.item_type_doesnt_support_definition_payload( - str(self.item_type) - ), - fab_constant.ERROR_UNSUPPORTED_COMMAND, - ) - def get_folders(self) -> List[str]: return ItemFoldersMap.get(self.item_type, []) diff --git a/src/fabric_cli/errors/__init__.py b/src/fabric_cli/errors/__init__.py index d4c61be5c..161022c8b 100644 --- a/src/fabric_cli/errors/__init__.py +++ b/src/fabric_cli/errors/__init__.py @@ -7,7 +7,6 @@ from .config import ConfigErrors from .context import ContextErrors from .cp import CpErrors -from .export import ExportErrors from .hierarchy import HierarchyErrors from .labels import LabelsErrors from .mkdir import MkdirErrors @@ -23,7 +22,6 @@ class ErrorMessages: Config = ConfigErrors Context = ContextErrors Cp = CpErrors - Export = ExportErrors Hierarchy = HierarchyErrors Labels = LabelsErrors Mkdir = MkdirErrors diff --git a/src/fabric_cli/errors/common.py b/src/fabric_cli/errors/common.py index 801fafc03..0896f169c 100644 --- a/src/fabric_cli/errors/common.py +++ b/src/fabric_cli/errors/common.py @@ -248,3 +248,12 @@ def gateway_property_not_supported_for_type( @staticmethod def query_not_supported_for_set(query: str) -> str: return f"Query '{query}' is not supported for set command" + + @staticmethod + def invalid_definition_format(valid_formats: list[str]) -> str: + if valid_formats: + message = f"Only the following formats are supported: {', '.join(valid_formats)}" + else: + message = "No formats are supported" + return f"Invalid format. {message}" + diff --git a/src/fabric_cli/errors/export.py b/src/fabric_cli/errors/export.py deleted file mode 100644 index 47470e0d4..000000000 --- a/src/fabric_cli/errors/export.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - - -class ExportErrors: - @staticmethod - def invalid_export_format(valid_formats: list[str]) -> str: - message = "Only the following formats are supported: " + ", ".join(valid_formats) if len(valid_formats) else "No formats are supported" - return f"Invalid format. {message}" diff --git a/src/fabric_cli/utils/fab_cmd_import_utils.py b/src/fabric_cli/utils/fab_cmd_import_utils.py index 1b128e781..ef73525f3 100644 --- a/src/fabric_cli/utils/fab_cmd_import_utils.py +++ b/src/fabric_cli/utils/fab_cmd_import_utils.py @@ -25,11 +25,17 @@ def get_payload_for_item_type( if item.item_type == ItemType.ENVIRONMENT: return _build_environment_payload(path) else: - base64_definition = _build_payload(path) - return item.get_payload(base64_definition, input_format) + definition = _build_definition(path, input_format) + return { + "type": str(item.item_type), + "description": "Imported from fab", + "folderId": item.folder_id, + "displayName": item.short_name, + "definition": definition, + } -def _build_payload(input_path: Any) -> dict: +def _build_definition(input_path: Any, input_format: Optional[str] = None) -> dict: directory = input_path parts = [] @@ -67,9 +73,10 @@ def _build_payload(input_path: Any) -> dict: } ) - # Create the final JSON structure - payload_structure = {"parts": parts} - return payload_structure + definition: dict = {"parts": parts} + if input_format: + definition["format"] = input_format + return definition def _encode_file_to_base64(file_path: str) -> str: diff --git a/src/fabric_cli/utils/fab_item_util.py b/src/fabric_cli/utils/fab_item_util.py index d7ce545d8..ac8975edd 100644 --- a/src/fabric_cli/utils/fab_item_util.py +++ b/src/fabric_cli/utils/fab_item_util.py @@ -26,9 +26,14 @@ from fabric_cli.core import fab_constant, fab_state_config from fabric_cli.core.fab_commands import Command from fabric_cli.core.fab_exceptions import FabricCLIError -from fabric_cli.core.fab_types import ItemType, format_mapping +from fabric_cli.core.fab_types import ( + ItemType, + definition_format_mapping, + format_mapping, +) from fabric_cli.core.hiearchy.fab_folder import Folder from fabric_cli.core.hiearchy.fab_hiearchy import FabricElement, Item, OneLakeItem +from fabric_cli.errors import ErrorMessages from fabric_cli.utils import fab_ui @@ -125,4 +130,27 @@ def get_confirm_copy_move_message(is_move_command: bool) -> str: confirm_message = ( f"Item definition is {action} without its sensitivity label. Are you sure?" ) - return confirm_message \ No newline at end of file + return confirm_message + + +def resolve_definition_format(item_type: ItemType, format_param: Optional[str]) -> str: + """Validate and resolve a user-supplied format against definition_format_mapping. + + Returns the resolved API format value (may be empty string for no-format items). + Raises FabricCLIError if the format is invalid. + """ + valid_formats = definition_format_mapping.get(item_type, {}) + + if format_param and format_param in valid_formats: + return valid_formats[format_param] + + if format_param: + # Non-empty format param but not in the mapping + available = [k for k in valid_formats if k != "default"] + raise FabricCLIError( + ErrorMessages.Common.invalid_definition_format(available), + fab_constant.ERROR_INVALID_INPUT, + ) + + # No format supplied — use the default + return valid_formats.get("default", "") \ No newline at end of file diff --git a/tests/test_commands/conftest.py b/tests/test_commands/conftest.py index b389acfd2..b6b982d00 100644 --- a/tests/test_commands/conftest.py +++ b/tests/test_commands/conftest.py @@ -803,6 +803,7 @@ def mkdir(element_full_path, params=None): command_path="mkdir", path=element_full_path, params=params if params else ["run=true"], + output_format="text", ) context = handle_context.get_command_context(args.path, False) @@ -816,6 +817,7 @@ def rm(element_full_path): command_path="rm", path=element_full_path, force=True, + output_format="text", ) context = handle_context.get_command_context(args.path) diff --git a/tests/test_commands/recordings/test_commands/test_import/test_import_create_new_notebook_py_item_success.yaml b/tests/test_commands/recordings/test_commands/test_import/test_import_create_new_notebook_py_item_success.yaml index f5194a4a5..a4a063a25 100644 --- a/tests/test_commands/recordings/test_commands/test_import/test_import_create_new_notebook_py_item_success.yaml +++ b/tests/test_commands/recordings/test_commands/test_import/test_import_create_new_notebook_py_item_success.yaml @@ -827,8 +827,8 @@ interactions: message: OK - request: body: '{"type": "Notebook", "description": "Imported from fab", "folderId": null, - "displayName": "fabcli000001_new_5", "definition": {"parts": [{"path": "notebook-content.py", - "payload": "IyBGYWJyaWMgbm90ZWJvb2sgc291cmNlCgojIE1FVEFEQVRBICoqKioqKioqKioqKioqKioqKioqCgojIE1FVEEgewojIE1FVEEgICAia2VybmVsX2luZm8iOiB7CiMgTUVUQSAgICAgIm5hbWUiOiAic3luYXBzZV9weXNwYXJrIgojIE1FVEEgICB9LAojIE1FVEEgICAiZGVwZW5kZW5jaWVzIjoge30KIyBNRVRBIH0KCiMgQ0VMTCAqKioqKioqKioqKioqKioqKioqKgoKIyBXZWxjb21lIHRvIHlvdXIgbmV3IG5vdGVib29rCiMgVHlwZSBoZXJlIGluIHRoZSBjZWxsIGVkaXRvciB0byBhZGQgY29kZSEKCgojIE1FVEFEQVRBICoqKioqKioqKioqKioqKioqKioqCgojIE1FVEEgewojIE1FVEEgICAibGFuZ3VhZ2UiOiAicHl0aG9uIiwKIyBNRVRBICAgImxhbmd1YWdlX2dyb3VwIjogInN5bmFwc2VfcHlzcGFyayIKIyBNRVRBIH0K", + "displayName": "fabcli000001_new_5", "definition": {"format": "fabricGitSource", + "parts": [{"path": "notebook-content.py", "payload": "IyBGYWJyaWMgbm90ZWJvb2sgc291cmNlCgojIE1FVEFEQVRBICoqKioqKioqKioqKioqKioqKioqCgojIE1FVEEgewojIE1FVEEgICAia2VybmVsX2luZm8iOiB7CiMgTUVUQSAgICAgIm5hbWUiOiAic3luYXBzZV9weXNwYXJrIgojIE1FVEEgICB9LAojIE1FVEEgICAiZGVwZW5kZW5jaWVzIjoge30KIyBNRVRBIH0KCiMgQ0VMTCAqKioqKioqKioqKioqKioqKioqKgoKIyBXZWxjb21lIHRvIHlvdXIgbmV3IG5vdGVib29rCiMgVHlwZSBoZXJlIGluIHRoZSBjZWxsIGVkaXRvciB0byBhZGQgY29kZSEKCgojIE1FVEFEQVRBICoqKioqKioqKioqKioqKioqKioqCgojIE1FVEEgewojIE1FVEEgICAibGFuZ3VhZ2UiOiAicHl0aG9uIiwKIyBNRVRBICAgImxhbmd1YWdlX2dyb3VwIjogInN5bmFwc2VfcHlzcGFyayIKIyBNRVRBIH0K", "payloadType": "InlineBase64"}, {"path": ".platform", "payload": "ewogICAgIiRzY2hlbWEiOiAiaHR0cHM6Ly9kZXZlbG9wZXIubWljcm9zb2Z0LmNvbS9qc29uLXNjaGVtYXMvZmFicmljL2dpdEludGVncmF0aW9uL3BsYXRmb3JtUHJvcGVydGllcy8yLjAuMC9zY2hlbWEuanNvbiIsCiAgICAibWV0YWRhdGEiOiB7CiAgICAgICAgInR5cGUiOiAiTm90ZWJvb2siLAogICAgICAgICJkaXNwbGF5TmFtZSI6ICJmYWJjbGkwMDAwMDEiLAogICAgICAgICJkZXNjcmlwdGlvbiI6ICJDcmVhdGVkIGJ5IGZhYiIKICAgIH0sCiAgICAiY29uZmlnIjogewogICAgICAgICJ2ZXJzaW9uIjogIjIuMCIsCiAgICAgICAgImxvZ2ljYWxJZCI6ICIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiCiAgICB9Cn0=", "payloadType": "InlineBase64"}]}}' headers: diff --git a/tests/test_commands/test_import.py b/tests/test_commands/test_import.py index 7b2f70879..0507d2a1a 100644 --- a/tests/test_commands/test_import.py +++ b/tests/test_commands/test_import.py @@ -1165,6 +1165,7 @@ def _build_export_args(path, output, force=True): path=path, output=output, force=force, + output_format="text", ) @@ -1183,6 +1184,7 @@ def _build_export_format_args(path, output, force=True, format=None): output=output, force=force, format=format, + output_format="text", ) diff --git a/tests/test_core/test_fab_hiearchy.py b/tests/test_core/test_fab_hiearchy.py index e130917e5..a16e6ac33 100644 --- a/tests/test_core/test_fab_hiearchy.py +++ b/tests/test_core/test_fab_hiearchy.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +from unittest.mock import patch + import pytest import fabric_cli.core.fab_constant as fab_constant @@ -8,6 +10,10 @@ from fabric_cli.core.fab_exceptions import FabricCLIError from fabric_cli.core.fab_types import * from fabric_cli.core.hiearchy.fab_hiearchy import * +from fabric_cli.utils.fab_cmd_import_utils import ( + _build_definition, + get_payload_for_item_type, +) def test_create_tenant(): @@ -386,6 +392,12 @@ def test_get_item_payloads(): } } + def _mock_build(path, resolved_format=""): + result = {"parts": _base_payload["parts"]} + if resolved_format: + result["format"] = resolved_format + return result + # Test Notebook notebook = Item( name="item_name", @@ -403,7 +415,9 @@ def test_get_item_payloads(): } # Check that the payload is correct - assert notebook.get_payload(_base_payload) == _expected_payload + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert get_payload_for_item_type( + "dummy", notebook, "ipynb") == _expected_payload # Test Spark Job Definition spark_job_def = Item( @@ -425,8 +439,9 @@ def test_get_item_payloads(): } # Check that the payload is correct - assert spark_job_def.get_payload( - _base_payload, "SparkJobDefinitionV2") == _expected_payload + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert get_payload_for_item_type("dummy", + spark_job_def, "SparkJobDefinitionV2") == _expected_payload _expected_payload = { "type": "SparkJobDefinition", @@ -440,7 +455,9 @@ def test_get_item_payloads(): } # Check that the payload is correct - assert spark_job_def.get_payload(_base_payload) == _expected_payload + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert get_payload_for_item_type( + "dummy", spark_job_def, "SparkJobDefinitionV1") == _expected_payload # Test EventHouse event_house = Item( @@ -455,11 +472,13 @@ def test_get_item_payloads(): "description": "Imported from fab", "displayName": "item_name", "folderId": None, - "definition": _base_payload, + "definition": {"parts": _base_payload["parts"]}, } # Check that the payload is correct - assert event_house.get_payload(_base_payload) == _expected_payload + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert get_payload_for_item_type( + "dummy", event_house) == _expected_payload # Test Report report = Item( @@ -474,7 +493,7 @@ def test_get_item_payloads(): "description": "Imported from fab", "displayName": "item_name", "folderId": None, - "definition": _base_payload, + "definition": {"parts": _base_payload["parts"]}, } # Check that the payload is correct for SemanticModel which can have different formatting @@ -490,11 +509,12 @@ def test_get_item_payloads(): "description": "Imported from fab", "displayName": "item_name", "folderId": None, - "definition": _base_payload, + "definition": {"parts": _base_payload["parts"]}, } - assert smenticModel.get_payload( - _base_payload) == _expected_payload_without_format + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert get_payload_for_item_type("dummy", + smenticModel) == _expected_payload_without_format _expected_payload_with_format = { "type": "SemanticModel", @@ -507,24 +527,112 @@ def test_get_item_payloads(): }, } - assert ( - smenticModel.get_payload(_base_payload, input_format="TMDL") - == _expected_payload_with_format - ) + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert ( + get_payload_for_item_type("dummy", smenticModel, "TMDL") + == _expected_payload_with_format + ) # Check that the payload is correct - assert report.get_payload(_base_payload) == _expected_payload + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + assert get_payload_for_item_type("dummy", report) == _expected_payload - # Unsuported item - with pytest.raises(FabricCLIError) as e: - unsupported_item = Item( - name="item_name", - id="item_id", - parent=workspace, - item_type="Lakehouse", - ) - unsupported_item.get_payload(_base_payload) - assert e.value.status_code == fab_constant.ERROR_UNSUPPORTED_COMMAND + +# ------------------------------------------------------------------- +# Tests for _build_definition format handling +# ------------------------------------------------------------------- + + +def _make_item(item_type: str, parent=None) -> Item: + """Helper to create an Item with minimal boilerplate.""" + if parent is None: + tenant = Tenant(name="t", id="tid") + parent = Workspace(name="ws", id="wsid", + parent=tenant, type="Workspace") + return Item(name="item", id="iid", parent=parent, item_type=item_type) + + +class TestBuildPayload: + """Validate _build_definition includes format when provided.""" + + def test_with_format__includes_format_key(self, tmp_path): + (tmp_path / "notebook.ipynb").write_text("{}") + result = _build_definition(str(tmp_path), "ipynb") + assert result["format"] == "ipynb" + assert len(result["parts"]) == 1 + assert result["parts"][0]["path"] == "notebook.ipynb" + + def test_without_format__no_format_key(self, tmp_path): + (tmp_path / "notebook.ipynb").write_text("{}") + result = _build_definition(str(tmp_path)) + assert "format" not in result + assert len(result["parts"]) == 1 + + def test_empty_format__no_format_key(self, tmp_path): + (tmp_path / "file.json").write_text("{}") + result = _build_definition(str(tmp_path), "") + assert "format" not in result + + # -- Payload construction tests ------------------------------------------- + + def test_payload__lakehouse(self): + """Any item type can have a payload constructed.""" + item = _make_item("Lakehouse") + + def _mock_build(path, resolved_format=""): + return {"parts": {"key": "value"}} + + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + payload = get_payload_for_item_type("dummy", item) + assert payload["type"] == "Lakehouse" + assert payload["displayName"] == "item" + assert payload["definition"] == {"parts": {"key": "value"}} + + def test_payload__kql_dashboard(self): + """KQLDashboard (was in ImportDefinitionTypes) still works.""" + item = _make_item("KQLDashboard") + + def _mock_build(path, resolved_format=""): + return {"parts": {"key": "value"}} + + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + payload = get_payload_for_item_type("dummy", item) + assert payload["definition"] == {"parts": {"key": "value"}} + + # -- Folder-based items include folderId ---------------------------------- + + def test_payload__item_in_folder__includes_folder_id(self): + """Items inside a folder should have folderId set.""" + tenant = Tenant(name="t", id="tid") + ws = Workspace(name="ws", id="wsid", parent=tenant, type="Workspace") + folder = Folder(name="myfolder", id="folder123", parent=ws) + item = Item(name="nb", id="nbid", parent=folder, item_type="Notebook") + + def _mock_build(path, resolved_format=""): + result = {"parts": {"key": "value"}} + if resolved_format: + result["format"] = resolved_format + return result + + with patch("fabric_cli.utils.fab_cmd_import_utils._build_definition", side_effect=_mock_build): + payload = get_payload_for_item_type("dummy", item, "ipynb") + assert payload["folderId"] == "folder123" + assert payload["definition"]["format"] == "ipynb" + + def test_payload__item_in_workspace__folder_id_none(self): + """Items directly under workspace should have folderId=None.""" + item = _make_item("Notebook") + assert item.folder_id is None + + # -- Unknown format is now validated upstream by resolve_definition_format -- + + def test_unknown_format__raises_error(self): + """Unknown format raises FabricCLIError during resolution.""" + from fabric_cli.utils.fab_item_util import resolve_definition_format + + item = _make_item("SemanticModel") + with pytest.raises(FabricCLIError): + resolve_definition_format(item.item_type, "UnknownFormat") def test_create_folder():