diff --git a/src/PowerPlatform/Dataverse/client.py b/src/PowerPlatform/Dataverse/client.py index 2fb11e8..5b85064 100644 --- a/src/PowerPlatform/Dataverse/client.py +++ b/src/PowerPlatform/Dataverse/client.py @@ -653,6 +653,79 @@ def upload_file( ) return None + # File download + def download_file( + self, + table_schema_name: str, + record_id: str, + file_name_attribute: str, + ) -> tuple[str, bytes]: + """ + Download a file from a Dataverse file column. + :param table_schema_name: Schema name of the table, e.g. ``"account"`` or ``"new_MyTestTable"``. + :type table_schema_name: str + :param record_id: GUID of the record + :type record_id: str + :param file_name_attribute: Logical name of the file column attribute. + :type file_name_attribute: str + + :return: Tuple with file name and file content. + + :raises ~PowerPlatform.Dataverse.core.errors.HttpError: If the download fails or the file column is empty + + Example: + Download a PDF file:: + + client.download_file( + table_schema_name="account", + record_id=account_id, + file_name_attribute="new_contract" + ) + + """ + od = self._get_odata() + entity_set = od._entity_set_from_schema_name(table_schema_name) + return od._download_file( + entity_set, + record_id, + file_name_attribute, + ) + + # File delete + def delete_file( + self, + table_schema_name: str, + record_id: str, + file_name_attribute: str, + ) -> None: + """ + Delete a file from a Dataverse file column. + :param table_schema_name: Schema name of the table, e.g. ``"account"`` or ``"new_MyTestTable"``. + :param record_id: GUID of the record + :param file_name_attribute: Logical name of the file column attribute. + + + :return: None + :raises ~PowerPlatform.Dataverse.core.errors.HttpError: If the delete fails + + Example: + Delete a file:: + + client.delete_file( + table_schema_name="account", + record_id=account_id, + file_name_attribute="new_contract" + ) + + """ + od = self._get_odata() + entity_set = od._entity_set_from_schema_name(table_schema_name) + od._delete_file( + entity_set, + record_id, + file_name_attribute, + ) + # Cache utilities def flush_cache(self, kind) -> int: """ diff --git a/src/PowerPlatform/Dataverse/data/_upload.py b/src/PowerPlatform/Dataverse/data/_file_operations.py similarity index 79% rename from src/PowerPlatform/Dataverse/data/_upload.py rename to src/PowerPlatform/Dataverse/data/_file_operations.py index d82efb5..ab58c28 100644 --- a/src/PowerPlatform/Dataverse/data/_upload.py +++ b/src/PowerPlatform/Dataverse/data/_file_operations.py @@ -1,15 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -"""File upload helpers.""" +"""File operations helpers.""" from __future__ import annotations from typing import Optional -class _ODataFileUpload: - """File upload capabilities (small + chunk) with auto selection.""" +class _ODataFileOperations: + """Provides file management capabilities including upload, download, and delete operations.""" def _upload_file( self, @@ -127,7 +127,8 @@ def _upload_file_chunk( None Returns nothing on success. Any failure raises an exception. """ - import os, math + import os + import math from urllib.parse import quote if not record_id: @@ -176,3 +177,49 @@ def _upload_file_chunk( self._request("patch", location, headers=c_headers, data=chunk, expected=(206, 204)) uploaded_bytes += len(chunk) return None + + def _download_file( + self, + entity_set: str, + record_id: str, + file_name_attribute: str, + ) -> tuple[str, bytes]: + """ + Download a file from a Dataverse file column. + :param entity_set: Source entity set (plural logical name), e.g. "accounts". + :param record_id: GUID of the record + :param file_name_attribute: Logical name of the file column attribute. + + :return: Tuple with file name and file content. + """ + + key = self._format_key(record_id) + url = f"{self.api}/{entity_set}{key}/{file_name_attribute}/$value" + response = self._request("get", url, expected=(200,)) + file_name = response.headers.get("x-ms-file-name") + if file_name is None: + raise ValueError( + "Response is missing the 'x-ms-file-name' header. The file column may be empty or the server did not return the expected header." + ) + return file_name, response.content + + def _delete_file( + self, + entity_set: str, + record_id: str, + file_name_attribute: str, + ) -> None: + """ + Delete a file from a Dataverse file column. + :param entity_set: Target entity set (plural logical name), e.g. "accounts". + :param record_id: GUID of the record + :param file_name_attribute: Logical name of the file column attribute. + + :return: None + """ + + key = self._format_key(record_id) + url = f"{self.api}/{entity_set}{key}/{file_name_attribute}" + self._request("delete", url, expected=(204,)) + + return None diff --git a/src/PowerPlatform/Dataverse/data/_odata.py b/src/PowerPlatform/Dataverse/data/_odata.py index 8eda7ad..881fad7 100644 --- a/src/PowerPlatform/Dataverse/data/_odata.py +++ b/src/PowerPlatform/Dataverse/data/_odata.py @@ -15,7 +15,7 @@ import importlib.resources as ir from ..core._http import _HttpClient -from ._upload import _ODataFileUpload +from ._file_operations import _ODataFileOperations from ..core.errors import * from ..core._error_codes import ( _http_subcode, @@ -37,7 +37,7 @@ _GUID_RE = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") -class _ODataClient(_ODataFileUpload): +class _ODataClient(_ODataFileOperations): """Dataverse Web API client: CRUD, SQL-over-API, and table metadata helpers.""" @staticmethod