Skip to content

Commit 87b4faf

Browse files
committed
Merge branch 'main' into user/tpellissier/prefix-with-schemaname
2 parents d861410 + 1d469ed commit 87b4faf

13 files changed

Lines changed: 101 additions & 147 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
run: |
2929
python -m pip install --upgrade pip
3030
python -m pip install flake8 black build
31-
if [ -f dev_dependencies.txt ]; then pip install -r dev_dependencies.txt; fi
31+
python -m pip install -e .[dev]
3232
3333
- name: Check format with black
3434
continue-on-error: true # TODO: fix detected formatting errors and remove this line.

CHANGELOG.md

Lines changed: 34 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,42 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7-
8-
## [Unreleased]
5+
## [0.1.0b1] - 2025-11-14
96

107
### Added
11-
- Initial SDK implementation with CRUD operations
12-
- Service principal authentication support
13-
- Interactive browser authentication support
14-
- SQL query execution via `query_sql()`
15-
- File upload capabilities
16-
- Pandas integration for query results
17-
- Structured error handling with specific exception types
18-
- GitHub Actions CI pipeline for automated testing
8+
**Initial beta release** of Microsoft Dataverse SDK for Python
9+
10+
**Core Client & Authentication:**
11+
- Core `DataverseClient` with Azure Identity authentication support
12+
- Secure authentication using Azure Identity credentials (Service Principal, Managed Identity, Interactive Browser)
13+
- TLS 1.2+ encryption for all API communications
14+
- Proper credential handling without exposing secrets in logs
15+
16+
**Data Operations:**
17+
- Complete CRUD operations (create, read, update, delete) for Dataverse records
18+
- Advanced OData query support with filtering, sorting, and expansion
19+
- SQL query execution via `query_sql()` method with result pagination
20+
- Support for batch operations and transaction handling
21+
- File upload capabilities for file and image columns
22+
23+
**Table Management:**
24+
- Table metadata operations (create, inspect, delete custom tables)
25+
26+
**Integration & Analysis:**
27+
- Pandas DataFrame integration for seamless data analysis workflows
28+
29+
**Reliability & Error Handling:**
30+
- Comprehensive error handling with specific exception types (`DataverseError`, `AuthenticationError`, etc.)
31+
- HTTP retry logic with exponential backoff for resilient operations
32+
33+
**Developer Experience:**
34+
- Example scripts demonstrating common integration patterns
35+
- Complete documentation with quickstart guides and API reference
36+
- Modern Python packaging using `pyproject.toml` configuration
37+
38+
**Quality Assurance:**
39+
- Comprehensive test suite with unit and integration tests
40+
- GitHub Actions CI/CD pipeline for automated testing and validation
1941
- Azure DevOps PR validation pipeline
2042

2143
### Changed
@@ -32,42 +54,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3254

3355
### Security
3456
- N/A
35-
36-
## [0.1.0] - TBD
37-
38-
### Added
39-
- First alpha release
40-
- Core Dataverse client with authentication
41-
- Basic CRUD operations (create, get, update, delete)
42-
- OData query support
43-
- SQL query support
44-
- Error handling framework
45-
- Example scripts for common scenarios
46-
47-
---
48-
49-
## Release Notes Template
50-
51-
When creating a new release, copy this template:
52-
53-
```markdown
54-
## [X.Y.Z] - YYYY-MM-DD
55-
56-
### Added
57-
- New features
58-
59-
### Changed
60-
- Changes in existing functionality
61-
62-
### Deprecated
63-
- Soon-to-be removed features
64-
65-
### Removed
66-
- Removed features
67-
68-
### Fixed
69-
- Bug fixes
70-
71-
### Security
72-
- Security improvements or fixes
73-
```

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
A Python client library for Microsoft Dataverse that provides a unified interface for CRUD operations, SQL queries, table metadata management, and file uploads through the Dataverse Web API.
88

9-
**[Source code](https://github.com/microsoft/PowerPlatform-DataverseClient-Python)** | **[Package (PyPI)](https://pypi.org/project/PowerPlatform-Dataverse-Client/)** | **[API reference documentation](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/tree/main/examples)** | **[Product documentation](https://learn.microsoft.com/power-apps/developer/data-platform/)** | **[Samples](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/tree/main/examples)**
9+
**[Source code](https://github.com/microsoft/PowerPlatform-DataverseClient-Python)** | **[Package (PyPI)](https://pypi.org/project/PowerPlatform-Dataverse-Client/)** | **[API reference documentation](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/sdk-python/)** | **[Product documentation](https://learn.microsoft.com/en-us/python/api/dataverse-sdk-docs-python/dataverse-overview?view=dataverse-sdk-python-latest/)** | **[Samples](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/tree/main/examples)**
1010

1111
> [!IMPORTANT]
1212
> This library is currently in **preview**. Preview versions are provided for early access to new features and may contain breaking changes.

SUPPORT.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1-
# TODO: The maintainer of this repo has not yet edited this file
1+
# Support
22

3-
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
3+
## How to file issues and get help
44

5-
- **No CSS support:** Fill out this template with information about how to file issues and get help.
6-
- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.
7-
- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.
5+
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
6+
issues before filing new issues to avoid duplicates. For new issues, file your bug or
7+
feature request as a new Issue.
88

9-
*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
9+
### Getting Help
1010

11-
# Support
11+
For help and questions about using the Microsoft Dataverse SDK for Python:
1212

13-
## How to file issues and get help
13+
- **Documentation**: Check the [README](README.md) for quickstart guides and examples
14+
- **GitHub Issues**: [File an issue](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/issues) for bugs or feature requests
1415

15-
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
16-
issues before filing new issues to avoid duplicates. For new issues, file your bug or
17-
feature request as a new Issue.
16+
### Reporting Security Issues
17+
18+
Security issues should be reported privately via the [Microsoft Security Response Center (MSRC)](https://aka.ms/opensource/security/msrc) or by emailing [secure@microsoft.com](mailto:secure@microsoft.com). Please do not report security vulnerabilities through public GitHub issues.
19+
20+
## Microsoft Support Policy
1821

19-
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
20-
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
21-
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
22+
This is a community-supported project. Support for the Microsoft Dataverse SDK for Python is provided on a best-effort basis through:
2223

23-
## Microsoft Support Policy
24+
- Community contributions via GitHub Issues and Pull Requests
25+
- Documentation and examples in this repository
2426

25-
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
27+
This project is not covered by Microsoft's standard product support services. For issues with Microsoft Dataverse itself (not this SDK), please use the official Microsoft support channels.

dev_dependencies.txt

Lines changed: 0 additions & 10 deletions
This file was deleted.

requirements.txt

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/PowerPlatform/Dataverse/core/__init__.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,4 @@
88
configuration, HTTP client, and error handling.
99
"""
1010

11-
from .auth import AuthManager, TokenPair
12-
from .config import DataverseConfig
13-
from .errors import (
14-
DataverseError,
15-
HttpError,
16-
ValidationError,
17-
MetadataError,
18-
SQLParseError,
19-
)
20-
from .http import HttpClient
21-
22-
__all__ = [
23-
"AuthManager",
24-
"TokenPair",
25-
"DataverseConfig",
26-
"DataverseError",
27-
"HttpError",
28-
"ValidationError",
29-
"MetadataError",
30-
"SQLParseError",
31-
"HttpClient",
32-
]
11+
__all__ = []

src/PowerPlatform/Dataverse/core/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(
5353
self.details = details or {}
5454
self.source = source or "client"
5555
self.is_transient = is_transient
56-
self.timestamp = _dt.datetime.utcnow().isoformat() + "Z"
56+
self.timestamp = _dt.datetime.now(_dt.timezone.utc).isoformat().replace('+00:00', 'Z')
5757

5858
def to_dict(self) -> Dict[str, Any]:
5959
"""

src/PowerPlatform/Dataverse/data/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,4 @@
88
SQL query functionality, and file upload capabilities.
99
"""
1010

11-
from .odata import ODataClient
12-
from .upload import ODataFileUpload
13-
14-
__all__ = ["ODataClient", "ODataFileUpload"]
11+
__all__ = []

src/PowerPlatform/Dataverse/data/odata.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,18 @@
1717
from ..core.http import HttpClient
1818
from .upload import ODataFileUpload
1919
from ..core.errors import *
20-
from ..core import error_codes as ec
20+
from ..core.error_codes import (
21+
http_subcode,
22+
is_transient_status,
23+
VALIDATION_SQL_NOT_STRING,
24+
VALIDATION_SQL_EMPTY,
25+
METADATA_ENTITYSET_NOT_FOUND,
26+
METADATA_ENTITYSET_NAME_MISSING,
27+
METADATA_TABLE_NOT_FOUND,
28+
METADATA_TABLE_ALREADY_EXISTS,
29+
METADATA_COLUMN_NOT_FOUND,
30+
VALIDATION_UNSUPPORTED_CACHE_KIND,
31+
)
2132

2233
from ..__version__ import __version__ as _SDK_VERSION
2334

@@ -147,7 +158,7 @@ def _request(self, method: str, url: str, *, expected: tuple[int, ...] = (200, 2
147158
except Exception:
148159
pass
149160
sc = r.status_code
150-
subcode = ec.http_subcode(sc)
161+
subcode = http_subcode(sc)
151162
correlation_id = headers.get("x-ms-correlation-request-id") or headers.get("x-ms-correlation-id")
152163
request_id = headers.get("x-ms-client-request-id") or headers.get("request-id") or headers.get("x-ms-request-id")
153164
traceparent = headers.get("traceparent")
@@ -158,7 +169,7 @@ def _request(self, method: str, url: str, *, expected: tuple[int, ...] = (200, 2
158169
retry_after = int(ra)
159170
except Exception:
160171
retry_after = None
161-
is_transient = ec.is_transient_status(sc)
172+
is_transient = is_transient_status(sc)
162173
raise HttpError(
163174
msg,
164175
status_code=sc,
@@ -487,23 +498,23 @@ def _delete(self, table_schema_name: str, key: str) -> None:
487498
url = f"{self.api}/{entity_set}{self._format_key(key)}"
488499
self._request("delete", url, headers={"If-Match": "*"})
489500

490-
def _get(self, table_schema_name: str, key: str, select: Optional[str] = None) -> Dict[str, Any]:
501+
def _get(self, table_schema_name: str, key: str, select: Optional[List[str]] = None) -> Dict[str, Any]:
491502
"""Retrieve a single record.
492503
493504
:param table_schema_name: Schema name of the table.
494505
:type table_schema_name: ``str``
495506
:param key: Record GUID (with or without parentheses).
496507
:type key: ``str``
497-
:param select: Comma separated columns for ``$select`` (optional).
498-
:type select: ``str`` | ``None``
508+
:param select: Columns to select; joined with commas into $select.
509+
:type select: ``list[str]`` | ``None``
499510
500511
:return: Retrieved record dictionary (may be empty if no selected attributes).
501512
:rtype: ``dict[str, Any]``
502513
"""
503514
params = {}
504515
if select:
505516
# Lowercase column names for case-insensitive matching
506-
params["$select"] = select.lower()
517+
params["$select"] = ",".join(select)
507518
entity_set = self._entity_set_from_schema_name(table_schema_name)
508519
url = f"{self.api}/{entity_set}{self._format_key(key)}"
509520
r = self._request("get", url, params=params)
@@ -605,9 +616,9 @@ def _query_sql(self, sql: str) -> list[dict[str, Any]]:
605616
Endpoint form: ``GET /{entity_set}?sql=<encoded select>``. The client extracts the logical table name, resolves the entity set (metadata cached), then issues the request. Only a constrained SELECT subset is supported by the platform.
606617
"""
607618
if not isinstance(sql, str):
608-
raise ValidationError("sql must be a string", subcode=ec.VALIDATION_SQL_NOT_STRING)
619+
raise ValidationError("sql must be a string", subcode=VALIDATION_SQL_NOT_STRING)
609620
if not sql.strip():
610-
raise ValidationError("sql must be a non-empty string", subcode=ec.VALIDATION_SQL_EMPTY)
621+
raise ValidationError("sql must be a non-empty string", subcode=VALIDATION_SQL_EMPTY)
611622
sql = sql.strip()
612623

613624
# Extract logical table name via helper (robust to identifiers ending with 'from')
@@ -683,14 +694,14 @@ def _entity_set_from_schema_name(self, table_schema_name: str) -> str:
683694
plural_hint = " (did you pass a plural entity set name instead of the singular table schema name?)" if table_schema_name.endswith("s") and not table_schema_name.endswith("ss") else ""
684695
raise MetadataError(
685696
f"Unable to resolve entity set for table schema name '{table_schema_name}'. Provide the singular table schema name.{plural_hint}",
686-
subcode=ec.METADATA_ENTITYSET_NOT_FOUND,
697+
subcode=METADATA_ENTITYSET_NOT_FOUND,
687698
)
688699
md = items[0]
689700
es = md.get("EntitySetName")
690701
if not es:
691702
raise MetadataError(
692703
f"Metadata response missing EntitySetName for table schema name '{table_schema_name}'.",
693-
subcode=ec.METADATA_ENTITYSET_NAME_MISSING,
704+
subcode=METADATA_ENTITYSET_NAME_MISSING,
694705
)
695706
self._logical_to_entityset_cache[cache_key] = es
696707
primary_id_attr = md.get("PrimaryIdAttribute")
@@ -1195,7 +1206,7 @@ def _delete_table(self, table_schema_name: str) -> None:
11951206
if not ent or not ent.get("MetadataId"):
11961207
raise MetadataError(
11971208
f"Table '{table_schema_name}' not found.",
1198-
subcode=ec.METADATA_TABLE_NOT_FOUND,
1209+
subcode=METADATA_TABLE_NOT_FOUND,
11991210
)
12001211
metadata_id = ent["MetadataId"]
12011212
url = f"{self.api}/EntityDefinitions({metadata_id})"
@@ -1232,7 +1243,7 @@ def _create_table(
12321243
if ent:
12331244
raise MetadataError(
12341245
f"Table '{table_schema_name}' already exists.",
1235-
subcode=ec.METADATA_TABLE_ALREADY_EXISTS,
1246+
subcode=METADATA_TABLE_ALREADY_EXISTS,
12361247
)
12371248

12381249
created_cols: List[str] = []
@@ -1301,7 +1312,7 @@ def _create_columns(
13011312
if not ent or not ent.get("MetadataId"):
13021313
raise MetadataError(
13031314
f"Table '{table_schema_name}' not found.",
1304-
subcode=ec.METADATA_TABLE_NOT_FOUND,
1315+
subcode=METADATA_TABLE_NOT_FOUND,
13051316
)
13061317

13071318
metadata_id = ent.get("MetadataId")
@@ -1362,7 +1373,7 @@ def _delete_columns(
13621373
if not ent or not ent.get("MetadataId"):
13631374
raise MetadataError(
13641375
f"Table '{table_schema_name}' not found.",
1365-
subcode=ec.METADATA_TABLE_NOT_FOUND,
1376+
subcode=METADATA_TABLE_NOT_FOUND,
13661377
)
13671378

13681379
# Use the actual SchemaName from the entity metadata
@@ -1376,7 +1387,7 @@ def _delete_columns(
13761387
if not attr_meta:
13771388
raise MetadataError(
13781389
f"Column '{column_name}' not found on table '{entity_schema}'.",
1379-
subcode=ec.METADATA_COLUMN_NOT_FOUND,
1390+
subcode=METADATA_COLUMN_NOT_FOUND,
13801391
)
13811392

13821393
attr_metadata_id = attr_meta.get("MetadataId")
@@ -1418,7 +1429,7 @@ def _flush_cache(
14181429
if k != "picklist":
14191430
raise ValidationError(
14201431
f"Unsupported cache kind '{kind}' (only 'picklist' is implemented)",
1421-
subcode=ec.VALIDATION_UNSUPPORTED_CACHE_KIND,
1432+
subcode=VALIDATION_UNSUPPORTED_CACHE_KIND,
14221433
)
14231434

14241435
removed = len(self._picklist_label_cache)

0 commit comments

Comments
 (0)