From 855928a5a11d1e4739930bea5742cd93546b59b4 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 15:52:51 -0700 Subject: [PATCH 01/12] Update CHANGELOG.md and version for v1.0.0 release Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++------------------ pyproject.toml | 2 +- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae58c5e..1f11d566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,33 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.0] - 2026-05-25 ### Added -- `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels (#175) -- `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values (#175) -- `client.records.list_pages(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()`; same parameter set (#175) -- `client.query.fetchxml(xml)` — FetchXML support returning an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called (#175) -- `FetchXmlQuery` implements the correct Dataverse paging cookie algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) -- `QueryBuilder.execute_pages()` — lazy per-page streaming returning one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) -- `QueryBuilder.where()` — composable filter expressions using `col()` and Python operators (`==`, `>`, `&`, `|`, `~`); replaces deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) -- `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` (#175) -- `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually (#175) -- `col()`, `raw()`, `QueryResult`, and `DataverseModel` exported from the top-level `PowerPlatform.Dataverse` package (#175) -- v0→v1 migration tool: installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`); rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns; requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) (#175) -- Migration tool now auto-rewrites `QueryBuilder.to_dataframe()` → `.execute().to_dataframe()` (inserts `.execute()` when receiver is a recognised builder chain); output improved with `[NEEDS-MANUAL]` label for files that have no auto-rewrites but require manual attention, and a trailing note on `[MIGRATED]` lines when manual items remain (#175) +- Full async client: `AsyncDataverseClient` with complete feature parity to the sync SDK — all CRUD, query, batch, file, and DataFrame operations available as `async def`; install via `pip install PowerPlatform-Dataverse-Client[async]` and use `async with AsyncDataverseClient(...)` (#171) +- `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising (#175) +- `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID (#175) +- `client.records.list_pages(table, *, ...)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()` (#175) +- `client.query.fetchxml(xml)` — FetchXML query support returning an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called; implements the correct Dataverse paging cookie algorithm with a 10,000-page circuit breaker (#175) +- `QueryBuilder.execute_pages()` — lazy per-page streaming; replaces `execute(by_page=True)` (#175) +- `QueryBuilder.where(expr)` — composable filter expressions using `col()` and Python operators (`==`, `!=`, `>`, `>=`, `<`, `<=`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) +- `QueryResult[0]` index access returns a `Record`; `QueryResult[1:5]` slice access returns a new `QueryResult` (#175) +- `DataverseModel` structural `Protocol` — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually (#175) +- `col()`, `raw()`, `QueryResult`, and `DataverseModel` are now importable directly from `PowerPlatform.Dataverse` (#175) +- Shorter import paths: `Record`, `DataverseError`, `QueryBuilder`, and other public types are now importable directly from `PowerPlatform.Dataverse.models` and `PowerPlatform.Dataverse.core` without navigating to submodule paths (#165) +- v0→v1 migration tool: `dataverse-migrate` console script (also `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`) rewrites v0 call sites to the v1 API with `--dry-run` support; auto-rewrites `QueryBuilder.to_dataframe()` calls; marks files requiring manual attention with `[NEEDS-MANUAL]`; requires `pip install PowerPlatform-Dataverse-Client[migration]` (#175, #184) ### Changed - `QueryBuilder.execute()` now returns a flat `QueryResult` (all pages collected eagerly) instead of `Iterable[Record]` (#175) -- `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` (#175) +- `records.get()` deprecation extended: calling with a `record_id` directs to `retrieve()`; calling without one directs to `list()` (#175) +- `float` and `double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) +- Server-side error detail (`error.innererror.message`) is now included in `HttpError.message` and batch failure details, making the offending field or data type visible without inspecting raw wire payloads (#185) +- `OperationContext` now validates keys and values against a fixed allowlist (`app`, `skill`, `agent`); unknown keys or non-conforming values raise `ValidationError` at construction time, preventing PII from reaching the `User-Agent` header (#181) +- Table creation now uses the `CreateEntities` API, improving reliability and aligning with the current Dataverse API contract (#183) +- `pandas.DataFrame` with MultiIndex columns now raises a descriptive error with a flatten hint at the point of use, instead of producing broken tuple keys deep in the serialization path (#185) ### Deprecated - `QueryBuilder.execute(by_page=True)` and `execute(by_page=False)` emit `UserWarning`; use `execute_pages()` and `execute()` respectively (#175) -- `client.query.odata_select()`, `client.query.odata_expands()`, `client.query.odata_expand()`, `client.query.odata_bind()` emit `DeprecationWarning`; navigation property helpers are replaced by `QueryBuilder.expand()` (#175) +- `client.query.odata_select()`, `odata_expands()`, `odata_expand()`, `odata_bind()` emit `DeprecationWarning`; use `QueryBuilder.expand()` instead (#175) ### Removed -- All v0 flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) removed (~570 lines); use the `client.records`, `client.query`, and `client.batch` namespaces (#175) -- `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` removed (#175) +- All v0 flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.); use the `client.records`, `client.query`, and `client.batch` namespaces (#175) +- `client.query.sql_select()`, `sql_joins()`, `sql_join()` (#175) + +### Fixed +- `async for record in results:` now works correctly; `QueryResult` was missing `__aiter__`, causing `async for` to fail in async code (#187) +- Client creation raised an error on Python 3.10 and 3.11; all supported Python versions (3.10–3.14) now work correctly (#188) +- SQL guardrails now block write statements and statement stacking even when hidden inside comments, string literals, or zero-width prefixes (#185) ## [0.1.0b10] - 2026-05-12 @@ -156,7 +166,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Comprehensive error handling with specific exception types (`DataverseError`, `AuthenticationError`, etc.) (#22, #24) - HTTP retry logic with exponential backoff for resilient operations (#72) -[Unreleased]: https://github.com/microsoft/PowerPlatform-DataverseClient-Python/compare/v0.1.0b10...HEAD +[Unreleased]: https://github.com/microsoft/PowerPlatform-DataverseClient-Python/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/microsoft/PowerPlatform-DataverseClient-Python/compare/v0.1.0b10...v1.0.0 [0.1.0b10]: https://github.com/microsoft/PowerPlatform-DataverseClient-Python/compare/v0.1.0b9...v0.1.0b10 [0.1.0b9]: https://github.com/microsoft/PowerPlatform-DataverseClient-Python/compare/v0.1.0b8...v0.1.0b9 [0.1.0b8]: https://github.com/microsoft/PowerPlatform-DataverseClient-Python/compare/v0.1.0b7...v0.1.0b8 diff --git a/pyproject.toml b/pyproject.toml index d0d7ba95..9e9a2e4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "PowerPlatform-Dataverse-Client" -version = "0.1.0b11" +version = "1.0.0" description = "Python SDK for Microsoft Dataverse" readme = {file = "README.md", content-type = "text/markdown"} authors = [{name = "Microsoft Corporation"}] From cd90571d9cfed25a00e2f5de36f87b3fd6e8a726 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 16:05:36 -0700 Subject: [PATCH 02/12] Restore original [Unreleased] wording verbatim; add new entries grouped by PR Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f11d566..82569a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,43 +5,45 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-05-25 +## [1.0.0] - 2026-05-28 ### Added -- Full async client: `AsyncDataverseClient` with complete feature parity to the sync SDK — all CRUD, query, batch, file, and DataFrame operations available as `async def`; install via `pip install PowerPlatform-Dataverse-Client[async]` and use `async with AsyncDataverseClient(...)` (#171) -- `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising (#175) -- `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID (#175) -- `client.records.list_pages(table, *, ...)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()` (#175) -- `client.query.fetchxml(xml)` — FetchXML query support returning an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called; implements the correct Dataverse paging cookie algorithm with a 10,000-page circuit breaker (#175) -- `QueryBuilder.execute_pages()` — lazy per-page streaming; replaces `execute(by_page=True)` (#175) -- `QueryBuilder.where(expr)` — composable filter expressions using `col()` and Python operators (`==`, `!=`, `>`, `>=`, `<`, `<=`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) -- `QueryResult[0]` index access returns a `Record`; `QueryResult[1:5]` slice access returns a new `QueryResult` (#175) -- `DataverseModel` structural `Protocol` — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually (#175) -- `col()`, `raw()`, `QueryResult`, and `DataverseModel` are now importable directly from `PowerPlatform.Dataverse` (#175) -- Shorter import paths: `Record`, `DataverseError`, `QueryBuilder`, and other public types are now importable directly from `PowerPlatform.Dataverse.models` and `PowerPlatform.Dataverse.core` without navigating to submodule paths (#165) -- v0→v1 migration tool: `dataverse-migrate` console script (also `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`) rewrites v0 call sites to the v1 API with `--dry-run` support; auto-rewrites `QueryBuilder.to_dataframe()` calls; marks files requiring manual attention with `[NEEDS-MANUAL]`; requires `pip install PowerPlatform-Dataverse-Client[migration]` (#175, #184) +- `AsyncDataverseClient` — full async counterpart to `DataverseClient`; all operation namespaces (`records`, `query`, `tables`, `files`, `batch`, `dataframe`) exposed as `async def` methods with `async with` lifecycle management; requires `pip install PowerPlatform-Dataverse-Client[async]` (#171) +- `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels (#175) +- `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values (#175) +- `client.records.list_pages(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()`; same parameter set (#175) +- `client.query.fetchxml(xml)` — FetchXML support returning an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called (#175) +- `FetchXmlQuery` implements the correct Dataverse paging cookie algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) +- `QueryBuilder.execute_pages()` — lazy per-page streaming returning one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) +- `QueryBuilder.where()` — composable filter expressions using `col()` and Python operators (`==`, `>`, `&`, `|`, `~`); replaces deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) +- `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` (#175) +- `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually (#175) +- `col()`, `raw()`, `QueryResult`, and `DataverseModel` exported from the top-level `PowerPlatform.Dataverse` package (#175) +- v0→v1 migration tool: installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`); rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns; requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) (#175) +- Migration tool now auto-rewrites `QueryBuilder.to_dataframe()` → `.execute().to_dataframe()` (inserts `.execute()` when receiver is a recognised builder chain); output improved with `[NEEDS-MANUAL]` label for files that have no auto-rewrites but require manual attention, and a trailing note on `[MIGRATED]` lines when manual items remain (#175) +- Public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) are now importable directly from `PowerPlatform.Dataverse.models`, `PowerPlatform.Dataverse.core`, and `PowerPlatform.Dataverse.operations` without navigating into submodule paths (#165) ### Changed - `QueryBuilder.execute()` now returns a flat `QueryResult` (all pages collected eagerly) instead of `Iterable[Record]` (#175) -- `records.get()` deprecation extended: calling with a `record_id` directs to `retrieve()`; calling without one directs to `list()` (#175) +- `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` (#175) +- `OperationContext` now validates keys against an allowlist (`app`, `skill`, `agent`) and values against per-key format rules; unknown keys or non-conforming values raise `ValidationError` at construction time, preventing PII from reaching the `User-Agent` header (#181) +- `client.tables.create()` now uses the `CreateEntities` API instead of `EntityDefinitions`, improving reliability and aligning with the current Dataverse API contract (#183) - `float` and `double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) -- Server-side error detail (`error.innererror.message`) is now included in `HttpError.message` and batch failure details, making the offending field or data type visible without inspecting raw wire payloads (#185) -- `OperationContext` now validates keys and values against a fixed allowlist (`app`, `skill`, `agent`); unknown keys or non-conforming values raise `ValidationError` at construction time, preventing PII from reaching the `User-Agent` header (#181) -- Table creation now uses the `CreateEntities` API, improving reliability and aligning with the current Dataverse API contract (#183) -- `pandas.DataFrame` with MultiIndex columns now raises a descriptive error with a flatten hint at the point of use, instead of producing broken tuple keys deep in the serialization path (#185) +- Server-side error detail (`error.innererror.message`) is now appended to `HttpError.message` on both single-request and batch paths, surfacing the offending field or data type without inspecting raw wire payloads (#185) +- `pandas.DataFrame` with MultiIndex columns now raises a clear error with a flatten hint at the point of use, instead of producing broken tuple keys deep in the serialization path (#185) ### Deprecated - `QueryBuilder.execute(by_page=True)` and `execute(by_page=False)` emit `UserWarning`; use `execute_pages()` and `execute()` respectively (#175) -- `client.query.odata_select()`, `odata_expands()`, `odata_expand()`, `odata_bind()` emit `DeprecationWarning`; use `QueryBuilder.expand()` instead (#175) +- `client.query.odata_select()`, `client.query.odata_expands()`, `client.query.odata_expand()`, `client.query.odata_bind()` emit `DeprecationWarning`; navigation property helpers are replaced by `QueryBuilder.expand()` (#175) ### Removed -- All v0 flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.); use the `client.records`, `client.query`, and `client.batch` namespaces (#175) -- `client.query.sql_select()`, `sql_joins()`, `sql_join()` (#175) +- All v0 flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) removed (~570 lines); use the `client.records`, `client.query`, and `client.batch` namespaces (#175) +- `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` removed (#175) ### Fixed -- `async for record in results:` now works correctly; `QueryResult` was missing `__aiter__`, causing `async for` to fail in async code (#187) -- Client creation raised an error on Python 3.10 and 3.11; all supported Python versions (3.10–3.14) now work correctly (#188) - SQL guardrails now block write statements and statement stacking even when hidden inside comments, string literals, or zero-width prefixes (#185) +- `records.get()` deprecation warning now names both migration paths: `retrieve()` for single-by-ID lookups and `list(filter=...)` for filtered queries (#185) +- Fixed client creation error on Python 3.10 and 3.11; all supported Python versions (3.10–3.14) now work correctly (#188) ## [0.1.0b10] - 2026-05-12 From a4f31ee456c9f3bbd214a3544874b20ff5f2b64f Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 16:17:46 -0700 Subject: [PATCH 03/12] Group #175 records methods and migration tool entries in CHANGELOG Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82569a1a..47454c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2026-05-28 ### Added -- `AsyncDataverseClient` — full async counterpart to `DataverseClient`; all operation namespaces (`records`, `query`, `tables`, `files`, `batch`, `dataframe`) exposed as `async def` methods with `async with` lifecycle management; requires `pip install PowerPlatform-Dataverse-Client[async]` (#171) -- `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels (#175) -- `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values (#175) -- `client.records.list_pages(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()`; same parameter set (#175) +- `AsyncDataverseClient` — full async counterpart to `DataverseClient`; all operation namespaces (`records`, `query`, `tables`, `files`, `batch`, `dataframe`) supported; requires `pip install PowerPlatform-Dataverse-Client[async]` (#171) +- New record-fetch methods on `client.records` (#175): + - `retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising + - `list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID + - `list_pages(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()` - `client.query.fetchxml(xml)` — FetchXML support returning an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called (#175) - `FetchXmlQuery` implements the correct Dataverse paging cookie algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) - `QueryBuilder.execute_pages()` — lazy per-page streaming returning one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) @@ -19,15 +20,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` (#175) - `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually (#175) - `col()`, `raw()`, `QueryResult`, and `DataverseModel` exported from the top-level `PowerPlatform.Dataverse` package (#175) -- v0→v1 migration tool: installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`); rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns; requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) (#175) -- Migration tool now auto-rewrites `QueryBuilder.to_dataframe()` → `.execute().to_dataframe()` (inserts `.execute()` when receiver is a recognised builder chain); output improved with `[NEEDS-MANUAL]` label for files that have no auto-rewrites but require manual attention, and a trailing note on `[MIGRATED]` lines when manual items remain (#175) +- v0→v1 migration tool (#175): + - Installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`) + - Rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns + - Marks files requiring manual attention with `[NEEDS-MANUAL]` + - Requires `pip install PowerPlatform-Dataverse-Client[migration]` - Public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) are now importable directly from `PowerPlatform.Dataverse.models`, `PowerPlatform.Dataverse.core`, and `PowerPlatform.Dataverse.operations` without navigating into submodule paths (#165) ### Changed - `QueryBuilder.execute()` now returns a flat `QueryResult` (all pages collected eagerly) instead of `Iterable[Record]` (#175) - `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` (#175) - `OperationContext` now validates keys against an allowlist (`app`, `skill`, `agent`) and values against per-key format rules; unknown keys or non-conforming values raise `ValidationError` at construction time, preventing PII from reaching the `User-Agent` header (#181) -- `client.tables.create()` now uses the `CreateEntities` API instead of `EntityDefinitions`, improving reliability and aligning with the current Dataverse API contract (#183) +- `client.tables.create()` now uses the `CreateEntities` API instead of `EntityDefinitions`, improving performance of entity creation (#183) - `float` and `double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) - Server-side error detail (`error.innererror.message`) is now appended to `HttpError.message` on both single-request and batch paths, surfacing the offending field or data type without inspecting raw wire payloads (#185) - `pandas.DataFrame` with MultiIndex columns now raises a clear error with a flatten hint at the point of use, instead of producing broken tuple keys deep in the serialization path (#185) From 06461c35ff48e4d69524dffd027c73c5d654831e Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 16:23:43 -0700 Subject: [PATCH 04/12] Group remaining #175 entries across Added, Changed, Deprecated, Removed Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47454c64..162c51b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,13 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising - `list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID - `list_pages(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()` -- `client.query.fetchxml(xml)` — FetchXML support returning an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called (#175) -- `FetchXmlQuery` implements the correct Dataverse paging cookie algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) -- `QueryBuilder.execute_pages()` — lazy per-page streaming returning one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) -- `QueryBuilder.where()` — composable filter expressions using `col()` and Python operators (`==`, `>`, `&`, `|`, `~`); replaces deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) -- `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` (#175) -- `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually (#175) -- `col()`, `raw()`, `QueryResult`, and `DataverseModel` exported from the top-level `PowerPlatform.Dataverse` package (#175) +- FetchXML query support via `client.query.fetchxml(xml)` (#175): + - Returns an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called + - Implements the correct Dataverse paging cookie algorithm with double URL-decoded `pagingcookie`, server-supplied `pagenumber`, and `morerecords` detection + - Enforces the 32,768-character URL limit and a 10,000-page circuit breaker against runaway iteration +- New `QueryBuilder` methods (#175): + - `execute_pages()` — lazy per-page streaming returning one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` + - `where(expr)` — composable filter expressions using `col()` and Python operators (`==`, `>`, `&`, `|`, `~`); replaces deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers +- Model and API surface additions (#175): + - `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` + - `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually + - `col()`, `raw()`, `QueryResult`, and `DataverseModel` exported from the top-level `PowerPlatform.Dataverse` package - v0→v1 migration tool (#175): - Installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`) - Rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns @@ -28,8 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) are now importable directly from `PowerPlatform.Dataverse.models`, `PowerPlatform.Dataverse.core`, and `PowerPlatform.Dataverse.operations` without navigating into submodule paths (#165) ### Changed -- `QueryBuilder.execute()` now returns a flat `QueryResult` (all pages collected eagerly) instead of `Iterable[Record]` (#175) -- `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` (#175) +- Query API changes (#175): + - `QueryBuilder.execute()` now returns a flat `QueryResult` (all pages collected eagerly) instead of `Iterable[Record]` + - `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` - `OperationContext` now validates keys against an allowlist (`app`, `skill`, `agent`) and values against per-key format rules; unknown keys or non-conforming values raise `ValidationError` at construction time, preventing PII from reaching the `User-Agent` header (#181) - `client.tables.create()` now uses the `CreateEntities` API instead of `EntityDefinitions`, improving performance of entity creation (#183) - `float` and `double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) @@ -37,12 +42,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `pandas.DataFrame` with MultiIndex columns now raises a clear error with a flatten hint at the point of use, instead of producing broken tuple keys deep in the serialization path (#185) ### Deprecated -- `QueryBuilder.execute(by_page=True)` and `execute(by_page=False)` emit `UserWarning`; use `execute_pages()` and `execute()` respectively (#175) -- `client.query.odata_select()`, `client.query.odata_expands()`, `client.query.odata_expand()`, `client.query.odata_bind()` emit `DeprecationWarning`; navigation property helpers are replaced by `QueryBuilder.expand()` (#175) +- Deprecated query API patterns (#175): + - `QueryBuilder.execute(by_page=True)` and `execute(by_page=False)` emit `UserWarning`; use `execute_pages()` and `execute()` respectively + - `client.query.odata_select()`, `client.query.odata_expands()`, `client.query.odata_expand()`, `client.query.odata_bind()` emit `DeprecationWarning`; navigation property helpers are replaced by `QueryBuilder.expand()` ### Removed -- All v0 flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) removed (~570 lines); use the `client.records`, `client.query`, and `client.batch` namespaces (#175) -- `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` removed (#175) +- Removed v0 API surface (#175): + - All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) removed; use the `client.records`, `client.query`, and `client.batch` namespaces + - `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` removed ### Fixed - SQL guardrails now block write statements and statement stacking even when hidden inside comments, string literals, or zero-width prefixes (#185) From b2bf4adc901c44c078a73687313b686544a77c06 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 16:29:18 -0700 Subject: [PATCH 05/12] Update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 162c51b0..7a87a832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Model and API surface additions (#175): - `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` - `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually - - `col()`, `raw()`, `QueryResult`, and `DataverseModel` exported from the top-level `PowerPlatform.Dataverse` package - v0→v1 migration tool (#175): - Installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`) - Rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns From 29eabb535eef796882cc0b3f8c7a941d39fac149 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 16:40:58 -0700 Subject: [PATCH 06/12] Rewrite 1.0.0 changelog following standard release note conventions Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 76 ++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a87a832..a7e94f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,53 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2026-05-28 +This is the first stable release. It establishes the v1 public API and removes all v0 beta methods. + +### Breaking Changes + +- All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) are removed; use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) +- `client.query.sql_select()`, `sql_joins()`, `sql_join()` are removed (#175) +- `QueryBuilder.execute()` now returns a flat `QueryResult` instead of `Iterable[Record]`; use `execute_pages()` for lazy iteration (#175) + + Run `dataverse-migrate --dry-run .` to automatically rewrite v0 call sites (`pip install PowerPlatform-Dataverse-Client[migration]`). + ### Added -- `AsyncDataverseClient` — full async counterpart to `DataverseClient`; all operation namespaces (`records`, `query`, `tables`, `files`, `batch`, `dataframe`) supported; requires `pip install PowerPlatform-Dataverse-Client[async]` (#171) -- New record-fetch methods on `client.records` (#175): - - `retrieve(table, record_id, *, select, expand, include_annotations)` — fetch a single record by GUID; returns `None` on 404 instead of raising - - `list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — eager fetch returning a flat `QueryResult`; GA replacement for `records.get()` without a record ID - - `list_pages(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` — lazy iterator yielding one `QueryResult` per HTTP page; streaming counterpart to `list()` -- FetchXML query support via `client.query.fetchxml(xml)` (#175): - - Returns an inert `FetchXmlQuery`; no HTTP request is made until `.execute()` or `.execute_pages()` is called - - Implements the correct Dataverse paging cookie algorithm with double URL-decoded `pagingcookie`, server-supplied `pagenumber`, and `morerecords` detection - - Enforces the 32,768-character URL limit and a 10,000-page circuit breaker against runaway iteration -- New `QueryBuilder` methods (#175): - - `execute_pages()` — lazy per-page streaming returning one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` - - `where(expr)` — composable filter expressions using `col()` and Python operators (`==`, `>`, `&`, `|`, `~`); replaces deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers -- Model and API surface additions (#175): - - `QueryResult.__getitem__` — index access (`result[0]`) returns a `Record`; slice access (`result[1:5]`) returns a new `QueryResult` - - `DataverseModel` structural `Protocol` (`models/protocol.py`) — implement on any entity class to enable typed integration with CRUD operations without specifying table names or serializing manually -- v0→v1 migration tool (#175): - - Installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`) - - Rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query builder patterns - - Marks files requiring manual attention with `[NEEDS-MANUAL]` - - Requires `pip install PowerPlatform-Dataverse-Client[migration]` -- Public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) are now importable directly from `PowerPlatform.Dataverse.models`, `PowerPlatform.Dataverse.core`, and `PowerPlatform.Dataverse.operations` without navigating into submodule paths (#165) -### Changed -- Query API changes (#175): - - `QueryBuilder.execute()` now returns a flat `QueryResult` (all pages collected eagerly) instead of `Iterable[Record]` - - `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` -- `OperationContext` now validates keys against an allowlist (`app`, `skill`, `agent`) and values against per-key format rules; unknown keys or non-conforming values raise `ValidationError` at construction time, preventing PII from reaching the `User-Agent` header (#181) -- `client.tables.create()` now uses the `CreateEntities` API instead of `EntityDefinitions`, improving performance of entity creation (#183) -- `float` and `double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) -- Server-side error detail (`error.innererror.message`) is now appended to `HttpError.message` on both single-request and batch paths, surfacing the offending field or data type without inspecting raw wire payloads (#185) -- `pandas.DataFrame` with MultiIndex columns now raises a clear error with a flatten hint at the point of use, instead of producing broken tuple keys deep in the serialization path (#185) +- **Async client** — `AsyncDataverseClient` with full feature parity to the sync SDK; all operation namespaces available as `async def` with `async with` lifecycle management. Install with `pip install PowerPlatform-Dataverse-Client[async]` (#171) +- **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery` that executes lazily via `.execute()` or `.execute_pages()`; handles Dataverse paging, URL length limits, and a 10,000-page circuit breaker (#175) +- **Streaming queries** — `QueryBuilder.execute_pages()` and `client.records.list_pages()` yield one `QueryResult` per HTTP page for memory-efficient iteration over large result sets (#175) +- **Multi-record fetch** — `client.records.list()` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID. Supports `filter`, `select`, `orderby`, `expand`, `page_size`, `count`, and `include_annotations` (#175) +- **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) +- **Single-record fetch** — `client.records.retrieve(table, id)` returns `None` on 404 instead of raising; accepts `select`, `expand`, and `include_annotations` (#175) +- **`QueryResult` indexing** — `result[0]` returns a `Record`; `result[1:5]` returns a new `QueryResult` (#175) +- **`DataverseModel` protocol** — structural `Protocol` for typed entity classes; enables CRUD operations without manual table names or dict serialization (#175) +- **Migration tool** — `dataverse-migrate` console script rewrites v0 call sites to the v1 API with `--dry-run` support; marks files requiring manual attention with `[NEEDS-MANUAL]`. Requires `pip install PowerPlatform-Dataverse-Client[migration]` (#175) +- **Shorter imports** — public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) now importable directly from `PowerPlatform.Dataverse.models`, `.core`, and `.operations` (#165) -### Deprecated -- Deprecated query API patterns (#175): - - `QueryBuilder.execute(by_page=True)` and `execute(by_page=False)` emit `UserWarning`; use `execute_pages()` and `execute()` respectively - - `client.query.odata_select()`, `client.query.odata_expands()`, `client.query.odata_expand()`, `client.query.odata_bind()` emit `DeprecationWarning`; navigation property helpers are replaced by `QueryBuilder.expand()` +### Changed -### Removed -- Removed v0 API surface (#175): - - All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) removed; use the `client.records`, `client.query`, and `client.batch` namespaces - - `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` removed +- `OperationContext` now validates keys and values against an allowlist; unknown keys or non-conforming values raise `ValidationError`, preventing PII from reaching the `User-Agent` header (#181) +- Table creation now uses the `CreateEntities` API, improving reliability (#183) +- `float`/`double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) +- Server inner error messages now surface in `HttpError.message` on both single-request and batch paths (#185) +- `pandas.DataFrame` with MultiIndex columns now raises a descriptive error with a flatten hint instead of failing deep in serialization (#185) ### Fixed -- SQL guardrails now block write statements and statement stacking even when hidden inside comments, string literals, or zero-width prefixes (#185) -- `records.get()` deprecation warning now names both migration paths: `retrieve()` for single-by-ID lookups and `list(filter=...)` for filtered queries (#185) -- Fixed client creation error on Python 3.10 and 3.11; all supported Python versions (3.10–3.14) now work correctly (#188) + +- `async for record in results:` now works correctly; `QueryResult` was missing `__aiter__` (#187) +- Client creation no longer errors on Python 3.10 and 3.11 (#188) +- SQL guardrails now catch write statements hidden inside comments, string literals, or zero-width prefixes (#185) + +### Deprecated + +- `QueryBuilder.execute(by_page=True/False)` — use `execute_pages()` and `execute()` respectively (#175) +- `client.query.odata_select()`, `odata_expands()`, `odata_expand()`, `odata_bind()` — use `QueryBuilder.expand()` instead (#175) ## [0.1.0b10] - 2026-05-12 From 26adac1a2e995af6dc07fe4e4734d373589cff89 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 16:56:49 -0700 Subject: [PATCH 07/12] Update changelog --- CHANGELOG.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e94f11..ed433b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ This is the first stable release. It establishes the v1 public API and removes a ### Breaking Changes -- All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, etc.) are removed; use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) -- `client.query.sql_select()`, `sql_joins()`, `sql_join()` are removed (#175) +- All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, `upload_file`, etc.) are removed (~570 lines); use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) +- `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` are removed (#175) - `QueryBuilder.execute()` now returns a flat `QueryResult` instead of `Iterable[Record]`; use `execute_pages()` for lazy iteration (#175) Run `dataverse-migrate --dry-run .` to automatically rewrite v0 call sites (`pip install PowerPlatform-Dataverse-Client[migration]`). @@ -20,18 +20,21 @@ This is the first stable release. It establishes the v1 public API and removes a ### Added - **Async client** — `AsyncDataverseClient` with full feature parity to the sync SDK; all operation namespaces available as `async def` with `async with` lifecycle management. Install with `pip install PowerPlatform-Dataverse-Client[async]` (#171) -- **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery` that executes lazily via `.execute()` or `.execute_pages()`; handles Dataverse paging, URL length limits, and a 10,000-page circuit breaker (#175) -- **Streaming queries** — `QueryBuilder.execute_pages()` and `client.records.list_pages()` yield one `QueryResult` per HTTP page for memory-efficient iteration over large result sets (#175) -- **Multi-record fetch** — `client.records.list()` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID. Supports `filter`, `select`, `orderby`, `expand`, `page_size`, `count`, and `include_annotations` (#175) -- **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) -- **Single-record fetch** — `client.records.retrieve(table, id)` returns `None` on 404 instead of raising; accepts `select`, `expand`, and `include_annotations` (#175) +- **Single-record fetch** — `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation-property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels (#175) +- **Multi-record fetch** — `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values (#175) +- **Streaming multi-record fetch** — `client.records.list_pages(...)` yields one `QueryResult` per HTTP page; streaming counterpart to `list()` with the same parameter set (#175) +- **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery`; no HTTP call until `.execute()` or `.execute_pages()`. Paging implements the documented Dataverse algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for the next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple-paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) +- **Streaming QueryBuilder** — `QueryBuilder.execute_pages()` lazily yields one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) +- **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`==`, `>`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) - **`QueryResult` indexing** — `result[0]` returns a `Record`; `result[1:5]` returns a new `QueryResult` (#175) -- **`DataverseModel` protocol** — structural `Protocol` for typed entity classes; enables CRUD operations without manual table names or dict serialization (#175) -- **Migration tool** — `dataverse-migrate` console script rewrites v0 call sites to the v1 API with `--dry-run` support; marks files requiring manual attention with `[NEEDS-MANUAL]`. Requires `pip install PowerPlatform-Dataverse-Client[migration]` (#175) +- **`DataverseModel` protocol** — structural `Protocol` in `models/protocol.py` for typed entity classes; enables CRUD operations without manual table names or dict serialization (#175) +- **Top-level re-exports** — `col()`, `raw()`, `QueryResult`, and `DataverseModel` importable directly from the top-level `PowerPlatform.Dataverse` package (#175) - **Shorter imports** — public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) now importable directly from `PowerPlatform.Dataverse.models`, `.core`, and `.operations` (#165) +- **Migration tool** — installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`); rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query-builder patterns; auto-rewrites `QueryBuilder.to_dataframe()` → `.execute().to_dataframe()`; emits a `[NEEDS-MANUAL]` label for files with no auto-rewrites but manual attention needed, and appends a trailing note on `[MIGRATED]` lines when manual items remain. Requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) (#175) ### Changed +- `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` (#175) - `OperationContext` now validates keys and values against an allowlist; unknown keys or non-conforming values raise `ValidationError`, preventing PII from reaching the `User-Agent` header (#181) - Table creation now uses the `CreateEntities` API, improving reliability (#183) - `float`/`double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) @@ -46,8 +49,8 @@ This is the first stable release. It establishes the v1 public API and removes a ### Deprecated -- `QueryBuilder.execute(by_page=True/False)` — use `execute_pages()` and `execute()` respectively (#175) -- `client.query.odata_select()`, `odata_expands()`, `odata_expand()`, `odata_bind()` — use `QueryBuilder.expand()` instead (#175) +- `QueryBuilder.execute(by_page=True)` and `execute(by_page=False)` emit `UserWarning`; use `execute_pages()` and `execute()` respectively (#175) +- `client.query.odata_select()`, `client.query.odata_expands()`, `client.query.odata_expand()`, `client.query.odata_bind()` emit `DeprecationWarning`; navigation-property helpers are replaced by `QueryBuilder.expand()` (#175) ## [0.1.0b10] - 2026-05-12 From b964cb480655ccf0225c1446fd40630387e5b506 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 17:14:04 -0700 Subject: [PATCH 08/12] Update changelog --- CHANGELOG.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed433b3e..44f8409a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This is the first stable release. It establishes the v1 public API and removes a ### Breaking Changes -- All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, `upload_file`, etc.) are removed (~570 lines); use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) +- All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, `upload_file`, etc.) are removed; use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) - `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` are removed (#175) - `QueryBuilder.execute()` now returns a flat `QueryResult` instead of `Iterable[Record]`; use `execute_pages()` for lazy iteration (#175) @@ -19,18 +19,25 @@ This is the first stable release. It establishes the v1 public API and removes a ### Added -- **Async client** — `AsyncDataverseClient` with full feature parity to the sync SDK; all operation namespaces available as `async def` with `async with` lifecycle management. Install with `pip install PowerPlatform-Dataverse-Client[async]` (#171) -- **Single-record fetch** — `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation-property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels (#175) -- **Multi-record fetch** — `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values (#175) -- **Streaming multi-record fetch** — `client.records.list_pages(...)` yields one `QueryResult` per HTTP page; streaming counterpart to `list()` with the same parameter set (#175) -- **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery`; no HTTP call until `.execute()` or `.execute_pages()`. Paging implements the documented Dataverse algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for the next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple-paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) -- **Streaming QueryBuilder** — `QueryBuilder.execute_pages()` lazily yields one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) -- **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`==`, `>`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) -- **`QueryResult` indexing** — `result[0]` returns a `Record`; `result[1:5]` returns a new `QueryResult` (#175) -- **`DataverseModel` protocol** — structural `Protocol` in `models/protocol.py` for typed entity classes; enables CRUD operations without manual table names or dict serialization (#175) -- **Top-level re-exports** — `col()`, `raw()`, `QueryResult`, and `DataverseModel` importable directly from the top-level `PowerPlatform.Dataverse` package (#175) +- **Async client** — `AsyncDataverseClient` with full feature parity to the sync SDK; all operation namespaces available with `async with` lifecycle management. Install with `pip install PowerPlatform-Dataverse-Client[async]` (#171) +- **GA query and records API (#175)** — namespace operations, FetchXML, streaming, typed models, and the migration codemod: + - **Single-record fetch** — `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation-property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels + - **Multi-record fetch** — `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values + - **Streaming multi-record fetch** — `client.records.list_pages(...)` yields one `QueryResult` per HTTP page; streaming counterpart to `list()` with the same parameter set + - **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery`; no HTTP call until `.execute()` or `.execute_pages()`. Paging implements the documented Dataverse algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for the next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple-paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration + - **Streaming QueryBuilder** — `QueryBuilder.execute_pages()` lazily yields one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` + - **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`==`, `>`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers + - **`QueryResult` indexing** — `result[0]` returns a `Record`; `result[1:5]` returns a new `QueryResult` + - **`DataverseModel` protocol** — structural `Protocol` in `models/protocol.py` for typed entity classes; enables CRUD operations without manual table names or dict serialization + - **Top-level re-exports** — `col()`, `raw()`, `QueryResult`, and `DataverseModel` importable directly from the top-level `PowerPlatform.Dataverse` package + - **Migration tool** — `dataverse-migrate` codemod for rewriting v0 call sites to the v1 API: + - Installed as the `dataverse-migrate` console script; also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1` + - `--dry-run` previews changes without writing files + - Covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query-builder patterns + - Emits a `[NEEDS-MANUAL]` label for files with no auto-rewrites but manual attention needed + - Appends a trailing note on `[MIGRATED]` lines when manual items remain + - Requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) - **Shorter imports** — public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) now importable directly from `PowerPlatform.Dataverse.models`, `.core`, and `.operations` (#165) -- **Migration tool** — installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`); rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query-builder patterns; auto-rewrites `QueryBuilder.to_dataframe()` → `.execute().to_dataframe()`; emits a `[NEEDS-MANUAL]` label for files with no auto-rewrites but manual attention needed, and appends a trailing note on `[MIGRATED]` lines when manual items remain. Requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) (#175) ### Changed From 715ec83782ea0a1598fb82055d83a90d9ebff09b Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 17:19:05 -0700 Subject: [PATCH 09/12] Update changelog --- CHANGELOG.md | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f8409a..0bf01906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,37 +20,29 @@ This is the first stable release. It establishes the v1 public API and removes a ### Added - **Async client** — `AsyncDataverseClient` with full feature parity to the sync SDK; all operation namespaces available with `async with` lifecycle management. Install with `pip install PowerPlatform-Dataverse-Client[async]` (#171) -- **GA query and records API (#175)** — namespace operations, FetchXML, streaming, typed models, and the migration codemod: - - **Single-record fetch** — `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation-property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels - - **Multi-record fetch** — `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values - - **Streaming multi-record fetch** — `client.records.list_pages(...)` yields one `QueryResult` per HTTP page; streaming counterpart to `list()` with the same parameter set - - **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery`; no HTTP call until `.execute()` or `.execute_pages()`. Paging implements the documented Dataverse algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for the next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple-paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration - - **Streaming QueryBuilder** — `QueryBuilder.execute_pages()` lazily yields one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` - - **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`==`, `>`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers - - **`QueryResult` indexing** — `result[0]` returns a `Record`; `result[1:5]` returns a new `QueryResult` - - **`DataverseModel` protocol** — structural `Protocol` in `models/protocol.py` for typed entity classes; enables CRUD operations without manual table names or dict serialization - - **Top-level re-exports** — `col()`, `raw()`, `QueryResult`, and `DataverseModel` importable directly from the top-level `PowerPlatform.Dataverse` package - - **Migration tool** — `dataverse-migrate` codemod for rewriting v0 call sites to the v1 API: - - Installed as the `dataverse-migrate` console script; also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1` - - `--dry-run` previews changes without writing files - - Covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query-builder patterns - - Emits a `[NEEDS-MANUAL]` label for files with no auto-rewrites but manual attention needed - - Appends a trailing note on `[MIGRATED]` lines when manual items remain - - Requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) +- **Single-record fetch** — `client.records.retrieve(table, record_id, *, select, expand, include_annotations)` returns `None` on 404 instead of raising; `expand` adds `$expand` for navigation-property expansion on the single-record GET; `include_annotations` maps to the `Prefer: odata.include-annotations` header for formatted values and lookup labels (#175) +- **Multi-record fetch** — `client.records.list(table, *, filter, select, top, orderby, expand, page_size, count, include_annotations)` returns an eager flat `QueryResult`; GA replacement for `records.get()` without a record ID; `page_size` controls `Prefer: odata.maxpagesize`, `count=True` adds `$count=true`, `include_annotations` requests formatted values (#175) +- **Streaming multi-record fetch** — `client.records.list_pages(...)` yields one `QueryResult` per HTTP page; streaming counterpart to `list()` with the same parameter set (#175) +- **FetchXML** — `client.query.fetchxml(xml)` returns an inert `FetchXmlQuery`; no HTTP call until `.execute()` or `.execute_pages()`. Paging implements the documented Dataverse algorithm: annotation parsed as outer XML, `pagingcookie` attribute double URL-decoded, server-supplied `pagenumber` used for the next page, `morerecords` handled as both `bool` and `"true"` string, `UserWarning` emitted on simple-paging fallback, 32,768-character URL limit enforced (documented Dataverse GET cap), 10,000-page circuit breaker against runaway iteration (#175) +- **Streaming QueryBuilder** — `QueryBuilder.execute_pages()` lazily yields one `QueryResult` per HTTP page; replaces deprecated `execute(by_page=True)` (#175) +- **Composable filters** — `QueryBuilder.where(col("name") == "Contoso")` with Python operators (`==`, `>`, `&`, `|`, `~`); replaces the deprecated `filter_eq()`, `filter_contains()`, and other `filter_*` helpers (#175) +- **`QueryResult` indexing** — `result[0]` returns a `Record`; `result[1:5]` returns a new `QueryResult` (#175) +- **`DataverseModel` protocol** — structural `Protocol` in `models/protocol.py` for typed entity classes; enables CRUD operations without manual table names or dict serialization (#175) +- **Top-level re-exports** — `col()`, `raw()`, `QueryResult`, and `DataverseModel` importable directly from the top-level `PowerPlatform.Dataverse` package (#175) - **Shorter imports** — public types (`Record`, `DataverseError`, `QueryBuilder`, `BatchResult`, and others) now importable directly from `PowerPlatform.Dataverse.models`, `.core`, and `.operations` (#165) +- **Migration tool** — installed as the `dataverse-migrate` console script (also runnable via `python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1`); rewrites v0 call sites to the v1 API with `--dry-run` support; covers `create`, `update`, `delete`, `get`, `list`, `fetchxml`, and query-builder patterns; emits a `[NEEDS-MANUAL]` label for files with no auto-rewrites but manual attention needed, and appends a trailing note on `[MIGRATED]` lines when manual items remain. Requires the `[migration]` optional extra (`pip install PowerPlatform-Dataverse-Client[migration]`) (#175) ### Changed - `records.get()` deprecation extended: calling with a `record_id` emits `DeprecationWarning` directing callers to `retrieve()`; calling without a `record_id` directs callers to `list()` (#175) - `OperationContext` now validates keys and values against an allowlist; unknown keys or non-conforming values raise `ValidationError`, preventing PII from reaching the `User-Agent` header (#181) -- Table creation now uses the `CreateEntities` API, improving reliability (#183) +- Table creation now uses the `CreateEntities` API, improving performance (#183) - `float`/`double` column precision default raised from 2 to 5 decimal places, preventing silent truncation of values like `2.718` (#185) - Server inner error messages now surface in `HttpError.message` on both single-request and batch paths (#185) - `pandas.DataFrame` with MultiIndex columns now raises a descriptive error with a flatten hint instead of failing deep in serialization (#185) ### Fixed -- `async for record in results:` now works correctly; `QueryResult` was missing `__aiter__` (#187) - Client creation no longer errors on Python 3.10 and 3.11 (#188) - SQL guardrails now catch write statements hidden inside comments, string literals, or zero-width prefixes (#185) From ff711b0abb2863adfa17869833a8e2eb7e046b1d Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 17:25:38 -0700 Subject: [PATCH 10/12] Update trove classifier to Production/Stable for 1.0.0 release Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e9a2e4b..1d07f04d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ license-files = ["LICENSE"] requires-python = ">=3.10" keywords = ["dataverse", "powerapps", "powerplatform", "crm", "dynamics", "odata"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", From 7baab4196dc0ed14cacd7ccf78847f1104185f6c Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 17:30:50 -0700 Subject: [PATCH 11/12] Update changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bf01906..303382cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2026-05-28 -This is the first stable release. It establishes the v1 public API and removes all v0 beta methods. - ### Breaking Changes - All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, `upload_file`, etc.) are removed; use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) From ddf1c8c80befa41c3ef80fdf6d3f59934a87e1df Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Thu, 28 May 2026 17:53:34 -0700 Subject: [PATCH 12/12] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 303382cb..aeb35bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes -- All flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, `upload_file`, etc.) are removed; use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) +- Previously deprecated flat methods on `DataverseClient` (`create`, `update`, `delete`, `get`, `list`, `query_sql`, `upload_file`, etc.) are now removed; use the namespaced operations: `client.records`, `client.query`, `client.tables`, `client.files`, `client.batch` (#175) - `client.query.sql_select()`, `client.query.sql_joins()`, `client.query.sql_join()` are removed (#175) - `QueryBuilder.execute()` now returns a flat `QueryResult` instead of `Iterable[Record]`; use `execute_pages()` for lazy iteration (#175)