Skip to content

Commit cb20a97

Browse files
tpellissierclaude
andcommitted
Add client.files namespace for file upload operations
- Create FileOperations class with upload() method in operations/files.py - Deprecate client.upload_file() with DeprecationWarning pointing to client.files.upload() - Update file_upload example, README, and both SKILL files - Add namespace delegation tests and deprecation test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4dad4bd commit cb20a97

8 files changed

Lines changed: 252 additions & 67 deletions

File tree

.claude/skills/dataverse-sdk-use/SKILL.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Use the PowerPlatform Dataverse Client Python SDK to interact with Microsoft Dat
2121
- `client.records` -- CRUD and OData queries
2222
- `client.query` -- query and search operations
2323
- `client.tables` -- table metadata, columns, and relationships
24+
- `client.files` -- file upload operations
2425

2526
### Bulk Operations
2627
The SDK supports Dataverse's native bulk operations: Pass lists to `create()`, `update()` for automatic bulk processing, for `delete()`, set `use_bulk_delete` when passing lists to use bulk operation
@@ -267,11 +268,11 @@ client.tables.delete_relationship(result["relationship_id"])
267268

268269
```python
269270
# Upload file to a file column
270-
client.upload_file(
271-
table_schema_name="account",
271+
client.files.upload(
272+
table="account",
272273
record_id=account_id,
273-
file_name_attribute="new_Document", # If the file column doesn't exist, it will be created automatically
274-
path="/path/to/document.pdf"
274+
file_column="new_Document", # If the file column doesn't exist, it will be created automatically
275+
path="/path/to/document.pdf",
275276
)
276277
```
277278

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ The SDK provides a simple, pythonic interface for Dataverse operations:
112112
| Concept | Description |
113113
|---------|-------------|
114114
| **DataverseClient** | Main entry point; provides `records`, `query`, and `tables` namespaces |
115-
| **Namespaces** | Operations are organized into `client.records` (CRUD & OData queries), `client.query` (query & search), and `client.tables` (metadata) |
115+
| **Namespaces** | Operations are organized into `client.records` (CRUD & OData queries), `client.query` (query & search), `client.tables` (metadata), and `client.files` (file uploads) |
116116
| **Records** | Dataverse records represented as Python dictionaries with column schema names |
117117
| **Schema names** | Use table schema names (`"account"`, `"new_MyTestTable"`) and column schema names (`"name"`, `"new_MyTestColumn"`). See: [Table definitions in Microsoft Dataverse](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/entity-metadata) |
118118
| **Bulk Operations** | Efficient bulk processing for multiple records with automatic optimization |
@@ -326,11 +326,11 @@ result = client.tables.create_lookup_field(
326326

327327
```python
328328
# Upload a file to a record
329-
client.upload_file(
330-
table_schema_name="account",
331-
record_id=account_id,
332-
file_name_attribute="new_Document", # If the file column doesn't exist, it will be created automatically
333-
path="/path/to/document.pdf"
329+
client.files.upload(
330+
"account",
331+
account_id,
332+
"new_Document", # If the file column doesn't exist, it will be created automatically
333+
"/path/to/document.pdf",
334334
)
335335
```
336336

examples/advanced/file_upload.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def get_dataset_info(file_path: Path):
248248
try:
249249
DATASET_FILE, small_file_size, src_hash = get_dataset_info(_GENERATED_TEST_FILE)
250250
backoff(
251-
lambda: client.upload_file(
251+
lambda: client.files.upload(
252252
table_schema_name,
253253
record_id,
254254
small_file_attr_schema,
@@ -282,7 +282,7 @@ def get_dataset_info(file_path: Path):
282282
print("Small single-request upload demo - REPLACE with 8MB file:")
283283
replacement_file, replace_size_small, replace_hash_small = get_dataset_info(_GENERATED_TEST_FILE_8MB)
284284
backoff(
285-
lambda: client.upload_file(
285+
lambda: client.files.upload(
286286
table_schema_name,
287287
record_id,
288288
small_file_attr_schema,
@@ -320,7 +320,7 @@ def get_dataset_info(file_path: Path):
320320
try:
321321
DATASET_FILE, src_size_chunk, src_hash_chunk = get_dataset_info(_GENERATED_TEST_FILE)
322322
backoff(
323-
lambda: client.upload_file(
323+
lambda: client.files.upload(
324324
table_schema_name,
325325
record_id,
326326
chunk_file_attr_schema,
@@ -351,7 +351,7 @@ def get_dataset_info(file_path: Path):
351351
print("Streaming chunk upload demo - REPLACE with 8MB file:")
352352
replacement_file, replace_size_chunk, replace_hash_chunk = get_dataset_info(_GENERATED_TEST_FILE_8MB)
353353
backoff(
354-
lambda: client.upload_file(
354+
lambda: client.files.upload(
355355
table_schema_name,
356356
record_id,
357357
chunk_file_attr_schema,

src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Use the PowerPlatform Dataverse Client Python SDK to interact with Microsoft Dat
2121
- `client.records` -- CRUD and OData queries
2222
- `client.query` -- query and search operations
2323
- `client.tables` -- table metadata, columns, and relationships
24+
- `client.files` -- file upload operations
2425

2526
### Bulk Operations
2627
The SDK supports Dataverse's native bulk operations: Pass lists to `create()`, `update()` for automatic bulk processing, for `delete()`, set `use_bulk_delete` when passing lists to use bulk operation
@@ -267,11 +268,11 @@ client.tables.delete_relationship(result["relationship_id"])
267268

268269
```python
269270
# Upload file to a file column
270-
client.upload_file(
271-
table_schema_name="account",
271+
client.files.upload(
272+
table="account",
272273
record_id=account_id,
273-
file_name_attribute="new_Document", # If the file column doesn't exist, it will be created automatically
274-
path="/path/to/document.pdf"
274+
file_column="new_Document", # If the file column doesn't exist, it will be created automatically
275+
path="/path/to/document.pdf",
275276
)
276277
```
277278

src/PowerPlatform/Dataverse/client.py

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .data._odata import _ODataClient
1515
from .operations.records import RecordOperations
1616
from .operations.query import QueryOperations
17+
from .operations.files import FileOperations
1718
from .operations.tables import TableOperations
1819

1920

@@ -56,6 +57,7 @@ class DataverseClient:
5657
- ``client.records`` -- create, update, delete, and get records (single or paginated queries)
5758
- ``client.query`` -- query and search operations
5859
- ``client.tables`` -- table and column metadata management
60+
- ``client.files`` -- file upload operations
5961
6062
Example:
6163
Create a client and perform basic operations::
@@ -101,6 +103,7 @@ def __init__(
101103
self.records = RecordOperations(self)
102104
self.query = QueryOperations(self)
103105
self.tables = TableOperations(self)
106+
self.files = FileOperations(self)
104107

105108
def _get_odata(self) -> _ODataClient:
106109
"""
@@ -665,67 +668,41 @@ def upload_file(
665668
if_none_match: bool = True,
666669
) -> None:
667670
"""
671+
.. note::
672+
Deprecated. Use :meth:`~PowerPlatform.Dataverse.operations.files.FileOperations.upload` instead.
673+
668674
Upload a file to a Dataverse file column.
669675
670-
:param table_schema_name: Schema name of the table, e.g. ``"account"`` or ``"new_MyTestTable"``.
676+
:param table_schema_name: Schema name of the table.
671677
:type table_schema_name: :class:`str`
672678
:param record_id: GUID of the target record.
673679
:type record_id: :class:`str`
674-
:param file_name_attribute: Schema name of the file column attribute (e.g., ``"new_Document"``). If the column doesn't exist, it will be created automatically.
680+
:param file_name_attribute: Schema name of the file column attribute.
675681
:type file_name_attribute: :class:`str`
676-
:param path: Local filesystem path to the file. The stored filename will be
677-
the basename of this path.
682+
:param path: Local filesystem path to the file.
678683
:type path: :class:`str`
679684
:param mode: Upload strategy: ``"auto"`` (default), ``"small"``, or ``"chunk"``.
680-
Auto mode selects small or chunked upload based on file size.
681685
:type mode: :class:`str` or None
682-
:param mime_type: Explicit MIME type to store with the file (e.g. ``"application/pdf"``).
683-
If not provided, the MIME type may be inferred from the file extension.
686+
:param mime_type: Explicit MIME type to store with the file.
684687
:type mime_type: :class:`str` or None
685-
:param if_none_match: When True (default), sends ``If-None-Match: null`` header to only
686-
succeed if the column is currently empty. Set False to always overwrite using
687-
``If-Match: *``. Used for small and chunk modes only.
688+
:param if_none_match: When True (default), only succeed if the column is
689+
currently empty.
688690
:type if_none_match: :class:`bool`
689-
690-
:raises ~PowerPlatform.Dataverse.core.errors.HttpError: If the upload fails or the file column is not empty
691-
when ``if_none_match=True``.
692-
:raises FileNotFoundError: If the specified file path does not exist.
693-
694-
.. note::
695-
Large files are automatically chunked to avoid request size limits. The chunk mode performs multiple requests with resumable upload support.
696-
697-
Example:
698-
Upload a PDF file::
699-
700-
client.upload_file(
701-
table_schema_name="account",
702-
record_id=account_id,
703-
file_name_attribute="new_Contract",
704-
path="/path/to/contract.pdf",
705-
mime_type="application/pdf"
706-
)
707-
708-
Upload with auto mode selection::
709-
710-
client.upload_file(
711-
table_schema_name="email",
712-
record_id=email_id,
713-
file_name_attribute="new_Attachment",
714-
path="/path/to/large_file.zip",
715-
mode="auto"
716-
)
717691
"""
718-
with self._scoped_odata() as od:
719-
od._upload_file(
720-
table_schema_name,
721-
record_id,
722-
file_name_attribute,
723-
path,
724-
mode=mode,
725-
mime_type=mime_type,
726-
if_none_match=if_none_match,
727-
)
728-
return None
692+
warnings.warn(
693+
"client.upload_file() is deprecated. Use client.files.upload() instead.",
694+
DeprecationWarning,
695+
stacklevel=2,
696+
)
697+
self.files.upload(
698+
table_schema_name,
699+
record_id,
700+
file_name_attribute,
701+
path,
702+
mode=mode,
703+
mime_type=mime_type,
704+
if_none_match=if_none_match,
705+
)
729706

730707
# Cache utilities
731708
def flush_cache(self, kind) -> int:
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
"""File operations namespace for the Dataverse SDK."""
5+
6+
from __future__ import annotations
7+
8+
from typing import Optional, TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from ..client import DataverseClient
12+
13+
14+
__all__ = ["FileOperations"]
15+
16+
17+
class FileOperations:
18+
"""Namespace for file operations.
19+
20+
Accessed via ``client.files``. Provides file upload operations for
21+
Dataverse file columns.
22+
23+
:param client: The parent :class:`~PowerPlatform.Dataverse.client.DataverseClient` instance.
24+
:type client: ~PowerPlatform.Dataverse.client.DataverseClient
25+
26+
Example::
27+
28+
client = DataverseClient(base_url, credential)
29+
30+
client.files.upload(
31+
"account", account_id, "new_Document", "/path/to/file.pdf"
32+
)
33+
"""
34+
35+
def __init__(self, client: DataverseClient) -> None:
36+
self._client = client
37+
38+
# ----------------------------------------------------------------- upload
39+
40+
def upload(
41+
self,
42+
table: str,
43+
record_id: str,
44+
file_column: str,
45+
path: str,
46+
*,
47+
mode: Optional[str] = None,
48+
mime_type: Optional[str] = None,
49+
if_none_match: bool = True,
50+
) -> None:
51+
"""Upload a file to a Dataverse file column.
52+
53+
:param table: Schema name of the table (e.g. ``"account"`` or
54+
``"new_MyTestTable"``).
55+
:type table: :class:`str`
56+
:param record_id: GUID of the target record.
57+
:type record_id: :class:`str`
58+
:param file_column: Schema name of the file column attribute (e.g.,
59+
``"new_Document"``). If the column doesn't exist, it will be
60+
created automatically.
61+
:type file_column: :class:`str`
62+
:param path: Local filesystem path to the file. The stored filename
63+
will be the basename of this path.
64+
:type path: :class:`str`
65+
:param mode: Upload strategy: ``"auto"`` (default), ``"small"``, or
66+
``"chunk"``. Auto mode selects small or chunked upload based on
67+
file size.
68+
:type mode: :class:`str` or None
69+
:param mime_type: Explicit MIME type to store with the file (e.g.
70+
``"application/pdf"``). If not provided, the MIME type may be
71+
inferred from the file extension.
72+
:type mime_type: :class:`str` or None
73+
:param if_none_match: When True (default), sends
74+
``If-None-Match: null`` header to only succeed if the column is
75+
currently empty. Set False to always overwrite using
76+
``If-Match: *``.
77+
:type if_none_match: :class:`bool`
78+
79+
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
80+
If the upload fails or the file column is not empty when
81+
``if_none_match=True``.
82+
:raises FileNotFoundError: If the specified file path does not exist.
83+
84+
Example:
85+
Upload a PDF file::
86+
87+
client.files.upload(
88+
"account",
89+
account_id,
90+
"new_Contract",
91+
"/path/to/contract.pdf",
92+
mime_type="application/pdf",
93+
)
94+
95+
Upload with auto mode selection::
96+
97+
client.files.upload(
98+
"email",
99+
email_id,
100+
"new_Attachment",
101+
"/path/to/large_file.zip",
102+
)
103+
"""
104+
with self._client._scoped_odata() as od:
105+
od._upload_file(
106+
table,
107+
record_id,
108+
file_column,
109+
path,
110+
mode=mode,
111+
mime_type=mime_type,
112+
if_none_match=if_none_match,
113+
)
114+
return None

tests/unit/test_client_deprecations.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,25 @@ def test_delete_columns_warns(self):
264264
)
265265
self.assertEqual(result, ["new_Notes", "new_Active"])
266266

267+
# ----------------------------------------------------------- upload_file
268+
269+
def test_upload_file_warns(self):
270+
"""client.upload_file() emits a DeprecationWarning and delegates
271+
to files.upload.
272+
"""
273+
with self.assertWarns(DeprecationWarning):
274+
self.client.upload_file("account", "guid-1", "new_Document", "/path/to/file.pdf")
275+
276+
self.client._odata._upload_file.assert_called_once_with(
277+
"account",
278+
"guid-1",
279+
"new_Document",
280+
"/path/to/file.pdf",
281+
mode=None,
282+
mime_type=None,
283+
if_none_match=True,
284+
)
285+
267286

268287
if __name__ == "__main__":
269288
unittest.main()

0 commit comments

Comments
 (0)