diff --git a/README.md b/README.md index 21abb591..32140be1 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ a PATCH request; multiple items use the `UpsertMultiple` bulk action. > upsert requests will be rejected by Dataverse with a 400 error. ```python -from PowerPlatform.Dataverse.models.upsert import UpsertItem +from PowerPlatform.Dataverse.models import UpsertItem # Upsert a single record client.records.upsert("account", [ @@ -346,7 +346,7 @@ query = (client.query.builder("contact") For complex logic (OR, NOT, grouping), compose expressions with `&`, `|`, `~`: ```python -from PowerPlatform.Dataverse.models.filters import col +from PowerPlatform.Dataverse.models import col # OR conditions: (statecode = 0 OR statecode = 1) AND revenue > 100k for record in (client.query.builder("account") @@ -397,7 +397,7 @@ if record: **Nested expand with options** -- expand navigation properties with `$select`, `$filter`, `$orderby`, and `$top`: ```python -from PowerPlatform.Dataverse.models.query_builder import ExpandOption +from PowerPlatform.Dataverse.models import ExpandOption # Expand related tasks with filtering and sorting for record in (client.query.builder("account") @@ -614,12 +614,14 @@ client.tables.delete("new_Product") Create relationships between tables using the relationship API. For a complete working example, see [examples/advanced/relationships.py](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/blob/main/examples/advanced/relationships.py). ```python -from PowerPlatform.Dataverse.models.relationship import ( +from PowerPlatform.Dataverse.models import ( + CascadeConfiguration, + Label, + LocalizedLabel, LookupAttributeMetadata, - OneToManyRelationshipMetadata, ManyToManyRelationshipMetadata, + OneToManyRelationshipMetadata, ) -from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel # Create a one-to-many relationship: Department (1) -> Employee (N) # This adds a "Department" lookup field to the Employee table @@ -821,7 +823,7 @@ The client raises structured exceptions for different error scenarios: ```python from PowerPlatform.Dataverse.client import DataverseClient -from PowerPlatform.Dataverse.core.errors import HttpError, ValidationError +from PowerPlatform.Dataverse.core import HttpError, ValidationError try: client.records.retrieve("account", "invalid-id") @@ -862,8 +864,7 @@ Enable file-based HTTP logging to capture all requests and responses for debuggi ```python from PowerPlatform.Dataverse.client import DataverseClient -from PowerPlatform.Dataverse.core.config import DataverseConfig -from PowerPlatform.Dataverse.core.log_config import LogConfig +from PowerPlatform.Dataverse.core import DataverseConfig, LogConfig log_cfg = LogConfig( log_folder="./my_logs", # Directory for log files (created if missing) diff --git a/docs/spec-module-level-exports.md b/docs/spec-module-level-exports.md new file mode 100644 index 00000000..9c1482c6 --- /dev/null +++ b/docs/spec-module-level-exports.md @@ -0,0 +1,101 @@ +# Spec: Support Module-Level Exports via `__all__` + +## Goal + +Populate the `__all__` lists in each package-level `__init__.py` so that public symbols +are re-exported at the package level. Users will be able to import from the package +namespace directly rather than reaching into submodules. + +**Before:** +```python +from PowerPlatform.Dataverse.models.record import Record +from PowerPlatform.Dataverse.core.errors import DataverseError +``` + +**After:** +```python +from PowerPlatform.Dataverse.models import Record +from PowerPlatform.Dataverse.core import DataverseError +``` + +--- + +## Current Status + +`__all__` is already defined in every individual module (e.g. `models/filters.py`, +`core/errors.py`, `operations/records.py`), but all package-level `__init__.py` files +have empty exports: + +| Package `__init__.py` | Current `__all__` | +|---|---| +| `PowerPlatform.Dataverse.models` | `[]` | +| `PowerPlatform.Dataverse.operations` | `[]` | +| `PowerPlatform.Dataverse.core` | `[]` | +| `PowerPlatform.Dataverse.data` | `[]` | + +--- + +## The Challenge: Documentation Duplication Risk + +The public API docs on Microsoft Learn are auto-generated from the installed package. +The concern is that re-exporting a class in `__init__.py` could cause it to appear +twice in the docs — once at its definition location (e.g. `operations.records.RecordOperations`) +and again at the package level (e.g. `operations.RecordOperations`). + +**What we need to verify before merging:** +- [ ] Confirm with the team how the doc pipeline works and run a test build to check + for duplicate entries. + +--- + +## What Needs to Change + +### `models/__init__.py` +Re-export from: +- `models.query_builder` → `QueryBuilder`, `QueryParams`, `ExpandOption` +- `models.filters` → `eq`, `ne`, `gt`, `lt`, `ge`, `le`, `contains`, `startswith`, `endswith`, `filter_in`, `between`, `and_`, `or_`, `not_` +- `models.batch` → `BatchItemResponse`, `BatchResult` +- `models.record` → `Record` +- `models.table_info` → `TableInfo`, `ColumnInfo`, `AlternateKeyInfo` +- `models.relationship` → `OneToManyRelationship`, `ManyToManyRelationship`, `RelationshipInfo` (etc.) +- `models.upsert` → `UpsertItem` +- `models.labels` → `LocalizedLabel`, `Label` + +### `core/__init__.py` +Re-export from: +- `core.errors` → `DataverseError`, `HttpError`, `ValidationError`, `MetadataError`, `SQLParseError` +- `core.log_config` → `LogConfig` + +### `operations/__init__.py` +Re-export from: +- `operations.records` → `RecordOperations` +- `operations.tables` → `TableOperations` +- `operations.query` → `QueryOperations` +- `operations.batch` → `BatchOperations`, `BatchRecordOperations`, `BatchTableOperations` +- `operations.dataframe` → `DataFrameOperations` +- `operations.files` → `FileOperations` + +### `data/__init__.py` +No change — all submodules are internal (`_`-prefixed); `__all__` stays empty. + +--- + +## Benefits + +1. **Cleaner import paths** — users write `from PowerPlatform.Dataverse.models import Record` + instead of navigating submodule paths. + +2. **IDE discoverability** — autocompletion on `PowerPlatform.Dataverse.models.` surfaces + all public types immediately; users do not need to know submodule names. + +3. **No broken imports during refactoring** — if we ever rename or reorganise an internal + submodule, users' import paths stay the same as long as the `__init__.py` re-exports + are kept. Without this, any internal restructuring is a breaking change for users. + +4. **Wildcard imports work correctly** — currently `from PowerPlatform.Dataverse.models import *` + imports nothing, because `__all__ = []`. Once populated, wildcard imports pick up all + intended public symbols as defined by Python's module documentation. + +5. **Follows industry convention** — NumPy, pandas, and requests all expose their public + API at the package level via `__all__` in `__init__.py`. Aligning with this pattern + makes the SDK feel familiar to experienced Python users. diff --git a/examples/advanced/alternate_keys_upsert.py b/examples/advanced/alternate_keys_upsert.py index 3248282a..ca574fa5 100644 --- a/examples/advanced/alternate_keys_upsert.py +++ b/examples/advanced/alternate_keys_upsert.py @@ -23,7 +23,7 @@ import time from PowerPlatform.Dataverse.client import DataverseClient -from PowerPlatform.Dataverse.models.upsert import UpsertItem +from PowerPlatform.Dataverse.models import UpsertItem from azure.identity import InteractiveBrowserCredential # type: ignore # --- Config --- diff --git a/examples/advanced/relationships.py b/examples/advanced/relationships.py index c0a8baa1..eae76225 100644 --- a/examples/advanced/relationships.py +++ b/examples/advanced/relationships.py @@ -20,13 +20,14 @@ import time from azure.identity import InteractiveBrowserCredential from PowerPlatform.Dataverse.client import DataverseClient -from PowerPlatform.Dataverse.models.relationship import ( +from PowerPlatform.Dataverse.models import ( + CascadeConfiguration, + Label, + LocalizedLabel, LookupAttributeMetadata, - OneToManyRelationshipMetadata, ManyToManyRelationshipMetadata, - CascadeConfiguration, + OneToManyRelationshipMetadata, ) -from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel from PowerPlatform.Dataverse.common.constants import ( CASCADE_BEHAVIOR_NO_CASCADE, CASCADE_BEHAVIOR_REMOVE_LINK, diff --git a/examples/advanced/walkthrough.py b/examples/advanced/walkthrough.py index d2cc4ff9..5e6533e4 100644 --- a/examples/advanced/walkthrough.py +++ b/examples/advanced/walkthrough.py @@ -25,9 +25,8 @@ from enum import IntEnum from azure.identity import InteractiveBrowserCredential from PowerPlatform.Dataverse.client import DataverseClient -from PowerPlatform.Dataverse.core.errors import MetadataError -from PowerPlatform.Dataverse.models.filters import col -from PowerPlatform.Dataverse.models.query_builder import ExpandOption +from PowerPlatform.Dataverse.core import MetadataError +from PowerPlatform.Dataverse.models import ExpandOption, col import requests diff --git a/examples/basic/functional_testing.py b/examples/basic/functional_testing.py index ddebd362..10f240d2 100644 --- a/examples/basic/functional_testing.py +++ b/examples/basic/functional_testing.py @@ -32,19 +32,20 @@ # Import SDK components (assumes installation is already validated) from PowerPlatform.Dataverse.client import DataverseClient -from PowerPlatform.Dataverse.core.errors import HttpError, MetadataError -from PowerPlatform.Dataverse.models.relationship import ( +from PowerPlatform.Dataverse.core import HttpError, MetadataError +from PowerPlatform.Dataverse.models import ( + CascadeConfiguration, + Label, + LocalizedLabel, LookupAttributeMetadata, - OneToManyRelationshipMetadata, ManyToManyRelationshipMetadata, - CascadeConfiguration, + OneToManyRelationshipMetadata, + UpsertItem, ) -from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel from PowerPlatform.Dataverse.common.constants import ( CASCADE_BEHAVIOR_NO_CASCADE, CASCADE_BEHAVIOR_REMOVE_LINK, ) -from PowerPlatform.Dataverse.models.upsert import UpsertItem from azure.identity import InteractiveBrowserCredential diff --git a/examples/basic/installation_example.py b/examples/basic/installation_example.py index 61da149b..255b6ace 100644 --- a/examples/basic/installation_example.py +++ b/examples/basic/installation_example.py @@ -60,10 +60,7 @@ from typing import Optional from datetime import datetime -from PowerPlatform.Dataverse.operations.records import RecordOperations -from PowerPlatform.Dataverse.operations.query import QueryOperations -from PowerPlatform.Dataverse.operations.tables import TableOperations -from PowerPlatform.Dataverse.operations.files import FileOperations +from PowerPlatform.Dataverse.operations import FileOperations, QueryOperations, RecordOperations, TableOperations def validate_imports(): @@ -81,11 +78,11 @@ def validate_imports(): print(f" [OK] Client class: PowerPlatform.Dataverse.client.DataverseClient") # Test submodule imports - from PowerPlatform.Dataverse.core.errors import HttpError, MetadataError + from PowerPlatform.Dataverse.core import HttpError, MetadataError print(f" [OK] Core errors: HttpError, MetadataError") - from PowerPlatform.Dataverse.core.config import DataverseConfig + from PowerPlatform.Dataverse.core import DataverseConfig print(f" [OK] Core config: DataverseConfig") diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..c46ba05d --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +from os import environ, path +from setuptools import setup + +# Try to read from VERSION.txt file first, fall back to environment variable +version_file = path.join(path.dirname(__file__), "VERSION.txt") +if path.exists(version_file): + with open(version_file, "r", encoding="utf-8") as f: + package_version = f.read().strip() +else: + package_version = environ.get("PackageVersion", "0.0.0") + +setup( + version=package_version, +) diff --git a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md index d25815d7..a0dbb307 100644 --- a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md +++ b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md @@ -212,7 +212,7 @@ client.records.update("account", [id1, id2, id3], {"industry": "Technology"}) Creates or updates records identified by alternate keys. Single item -> PATCH; multiple items -> `UpsertMultiple` bulk action. > **Prerequisite**: The table must have an alternate key configured in Dataverse for the columns used in `alternate_key`. Without it, Dataverse will reject the request with a 400 error. ```python -from PowerPlatform.Dataverse.models.upsert import UpsertItem +from PowerPlatform.Dataverse.models import UpsertItem # Single upsert client.records.upsert("account", [ @@ -403,12 +403,12 @@ client.tables.delete("new_Product") #### Create One-to-Many Relationship ```python -from PowerPlatform.Dataverse.models.relationship import ( - LookupAttributeMetadata, - OneToManyRelationshipMetadata, +from PowerPlatform.Dataverse.models import ( + CascadeConfiguration, Label, LocalizedLabel, - CascadeConfiguration, + LookupAttributeMetadata, + OneToManyRelationshipMetadata, ) from PowerPlatform.Dataverse.common.constants import CASCADE_BEHAVIOR_REMOVE_LINK @@ -435,7 +435,7 @@ print(f"Created lookup field: {result['lookup_schema_name']}") #### Create Many-to-Many Relationship ```python -from PowerPlatform.Dataverse.models.relationship import ManyToManyRelationshipMetadata +from PowerPlatform.Dataverse.models import ManyToManyRelationshipMetadata relationship = ManyToManyRelationshipMetadata( schema_name="new_employee_project", @@ -532,12 +532,12 @@ print(f"Succeeded: {len(result.succeeded)}, Failed: {len(result.failed)}") The SDK provides structured exceptions with detailed error information: ```python -from PowerPlatform.Dataverse.core.errors import ( +from PowerPlatform.Dataverse.core import ( DataverseError, HttpError, - ValidationError, MetadataError, - SQLParseError + SQLParseError, + ValidationError, ) from PowerPlatform.Dataverse.client import DataverseClient diff --git a/src/PowerPlatform/Dataverse/core/__init__.py b/src/PowerPlatform/Dataverse/core/__init__.py index 79454f5b..b3e61864 100644 --- a/src/PowerPlatform/Dataverse/core/__init__.py +++ b/src/PowerPlatform/Dataverse/core/__init__.py @@ -8,4 +8,17 @@ configuration, HTTP client, and error handling. """ -__all__ = [] +from .config import DataverseConfig, OperationContext +from .errors import DataverseError, HttpError, MetadataError, SQLParseError, ValidationError +from .log_config import LogConfig + +__all__ = [ + "DataverseConfig", + "DataverseError", + "HttpError", + "LogConfig", + "MetadataError", + "OperationContext", + "SQLParseError", + "ValidationError", +] diff --git a/src/PowerPlatform/Dataverse/core/config.py b/src/PowerPlatform/Dataverse/core/config.py index ed161048..ef71db52 100644 --- a/src/PowerPlatform/Dataverse/core/config.py +++ b/src/PowerPlatform/Dataverse/core/config.py @@ -18,6 +18,8 @@ if TYPE_CHECKING: from .log_config import LogConfig +__all__ = ["DataverseConfig", "OperationContext"] + # key=value pairs separated by semicolons. # Keys: alphanumeric, hyphens, underscores. # Values: alphanumeric, hyphens, underscores, dots, slashes. diff --git a/src/PowerPlatform/Dataverse/models/__init__.py b/src/PowerPlatform/Dataverse/models/__init__.py index 50bd7326..e418ffb4 100644 --- a/src/PowerPlatform/Dataverse/models/__init__.py +++ b/src/PowerPlatform/Dataverse/models/__init__.py @@ -7,19 +7,56 @@ Provides dataclasses and helpers for Dataverse entities: - :class:`~PowerPlatform.Dataverse.models.query_builder.QueryBuilder`: Fluent query builder. -- :mod:`~PowerPlatform.Dataverse.models.filters`: Composable OData filter expressions. +- :mod:`~PowerPlatform.Dataverse.models.filters`: Composable OData filter expressions + via :func:`~PowerPlatform.Dataverse.models.filters.col` and + :func:`~PowerPlatform.Dataverse.models.filters.raw`. - :class:`~PowerPlatform.Dataverse.models.record.QueryResult`: Iterable result wrapper. +- :class:`~PowerPlatform.Dataverse.models.record.Record`: Dataverse entity record. - :class:`~PowerPlatform.Dataverse.models.upsert.UpsertItem`: Upsert operation item. - -Import directly from the specific module, e.g.:: - - from PowerPlatform.Dataverse.models.query_builder import QueryBuilder - from PowerPlatform.Dataverse.models.filters import col, raw - from PowerPlatform.Dataverse.models.record import QueryResult +- :class:`~PowerPlatform.Dataverse.models.fetchxml_query.FetchXmlQuery`: FetchXML query object. +- :class:`~PowerPlatform.Dataverse.models.protocol.DataverseModel`: Typed-model protocol. """ -from .filters import col, raw +from .batch import BatchItemResponse, BatchResult +from .fetchxml_query import FetchXmlQuery +from .filters import ColumnProxy, FilterExpression, col, raw +from .labels import Label, LocalizedLabel from .protocol import DataverseModel -from .record import QueryResult +from .query_builder import ExpandOption, QueryBuilder, QueryParams +from .record import QueryResult, Record +from .relationship import ( + CascadeConfiguration, + LookupAttributeMetadata, + ManyToManyRelationshipMetadata, + OneToManyRelationshipMetadata, + RelationshipInfo, +) +from .table_info import AlternateKeyInfo, ColumnInfo, TableInfo +from .upsert import UpsertItem -__all__ = ["col", "raw", "DataverseModel", "QueryResult"] +__all__ = [ + "BatchItemResponse", + "BatchResult", + "FetchXmlQuery", + "ColumnProxy", + "FilterExpression", + "col", + "raw", + "Label", + "LocalizedLabel", + "DataverseModel", + "ExpandOption", + "QueryBuilder", + "QueryParams", + "QueryResult", + "Record", + "CascadeConfiguration", + "LookupAttributeMetadata", + "ManyToManyRelationshipMetadata", + "OneToManyRelationshipMetadata", + "RelationshipInfo", + "AlternateKeyInfo", + "ColumnInfo", + "TableInfo", + "UpsertItem", +] diff --git a/src/PowerPlatform/Dataverse/operations/__init__.py b/src/PowerPlatform/Dataverse/operations/__init__.py index 19c8a9e5..d16f7fb9 100644 --- a/src/PowerPlatform/Dataverse/operations/__init__.py +++ b/src/PowerPlatform/Dataverse/operations/__init__.py @@ -8,6 +8,34 @@ SDK operations into logical groups: records, query, and tables. """ -from typing import List +from .batch import ( + BatchDataFrameOperations, + BatchOperations, + BatchQueryOperations, + BatchRecordOperations, + BatchRequest, + BatchTableOperations, + ChangeSet, + ChangeSetRecordOperations, +) +from .dataframe import DataFrameOperations +from .files import FileOperations +from .query import QueryOperations +from .records import RecordOperations +from .tables import TableOperations -__all__: List[str] = [] +__all__ = [ + "BatchDataFrameOperations", + "BatchOperations", + "BatchQueryOperations", + "BatchRecordOperations", + "BatchRequest", + "BatchTableOperations", + "ChangeSet", + "ChangeSetRecordOperations", + "DataFrameOperations", + "FileOperations", + "QueryOperations", + "RecordOperations", + "TableOperations", +] diff --git a/tests/unit/test_package_exports.py b/tests/unit/test_package_exports.py new file mode 100644 index 00000000..9c510285 --- /dev/null +++ b/tests/unit/test_package_exports.py @@ -0,0 +1,158 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +"""Tests that every symbol in __all__ is importable from each package namespace, +and that re-exported objects are identical to their originals.""" + +import unittest + + +class TestCoreExports(unittest.TestCase): + """Verify package-level exports for PowerPlatform.Dataverse.core. + + Checks that every symbol in __all__ is reachable from the package namespace + and that each re-export is the identical object as its source definition. + """ + + def test_all_symbols_importable(self): + """Every name listed in __all__ is accessible as an attribute of the package.""" + import PowerPlatform.Dataverse.core as m + + for name in m.__all__: + self.assertTrue(hasattr(m, name), f"{name!r} is in __all__ but missing from PowerPlatform.Dataverse.core") + + def test_identity(self): + """Re-exported objects are the same objects as their source definitions.""" + import PowerPlatform.Dataverse.core as m + from PowerPlatform.Dataverse.core.config import DataverseConfig, OperationContext + from PowerPlatform.Dataverse.core.errors import ( + DataverseError, + HttpError, + MetadataError, + SQLParseError, + ValidationError, + ) + from PowerPlatform.Dataverse.core.log_config import LogConfig + + self.assertIs(m.DataverseConfig, DataverseConfig) + self.assertIs(m.DataverseError, DataverseError) + self.assertIs(m.HttpError, HttpError) + self.assertIs(m.LogConfig, LogConfig) + self.assertIs(m.MetadataError, MetadataError) + self.assertIs(m.OperationContext, OperationContext) + self.assertIs(m.SQLParseError, SQLParseError) + self.assertIs(m.ValidationError, ValidationError) + + +class TestModelsExports(unittest.TestCase): + """Verify package-level exports for PowerPlatform.Dataverse.models. + + Checks that every symbol in __all__ is reachable from the package namespace + and that each re-export is the identical object as its source definition. + """ + + def test_all_symbols_importable(self): + """Every name listed in __all__ is accessible as an attribute of the package.""" + import PowerPlatform.Dataverse.models as m + + for name in m.__all__: + self.assertTrue(hasattr(m, name), f"{name!r} is in __all__ but missing from PowerPlatform.Dataverse.models") + + def test_identity(self): + """Re-exported objects are the same objects as their source definitions.""" + import PowerPlatform.Dataverse.models as m + from PowerPlatform.Dataverse.models.batch import BatchItemResponse, BatchResult + from PowerPlatform.Dataverse.models.fetchxml_query import FetchXmlQuery + from PowerPlatform.Dataverse.models.filters import ColumnProxy, FilterExpression, col, raw + from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel + from PowerPlatform.Dataverse.models.protocol import DataverseModel + from PowerPlatform.Dataverse.models.query_builder import ExpandOption, QueryBuilder, QueryParams + from PowerPlatform.Dataverse.models.record import QueryResult, Record + from PowerPlatform.Dataverse.models.relationship import ( + CascadeConfiguration, + LookupAttributeMetadata, + ManyToManyRelationshipMetadata, + OneToManyRelationshipMetadata, + RelationshipInfo, + ) + from PowerPlatform.Dataverse.models.table_info import AlternateKeyInfo, ColumnInfo, TableInfo + from PowerPlatform.Dataverse.models.upsert import UpsertItem + + self.assertIs(m.AlternateKeyInfo, AlternateKeyInfo) + self.assertIs(m.BatchItemResponse, BatchItemResponse) + self.assertIs(m.BatchResult, BatchResult) + self.assertIs(m.CascadeConfiguration, CascadeConfiguration) + self.assertIs(m.ColumnInfo, ColumnInfo) + self.assertIs(m.ColumnProxy, ColumnProxy) + self.assertIs(m.DataverseModel, DataverseModel) + self.assertIs(m.ExpandOption, ExpandOption) + self.assertIs(m.FetchXmlQuery, FetchXmlQuery) + self.assertIs(m.FilterExpression, FilterExpression) + self.assertIs(m.Label, Label) + self.assertIs(m.LocalizedLabel, LocalizedLabel) + self.assertIs(m.LookupAttributeMetadata, LookupAttributeMetadata) + self.assertIs(m.ManyToManyRelationshipMetadata, ManyToManyRelationshipMetadata) + self.assertIs(m.OneToManyRelationshipMetadata, OneToManyRelationshipMetadata) + self.assertIs(m.QueryBuilder, QueryBuilder) + self.assertIs(m.QueryParams, QueryParams) + self.assertIs(m.QueryResult, QueryResult) + self.assertIs(m.Record, Record) + self.assertIs(m.RelationshipInfo, RelationshipInfo) + self.assertIs(m.TableInfo, TableInfo) + self.assertIs(m.UpsertItem, UpsertItem) + self.assertIs(m.col, col) + self.assertIs(m.raw, raw) + + +class TestOperationsExports(unittest.TestCase): + """Verify package-level exports for PowerPlatform.Dataverse.operations. + + Checks that every symbol in __all__ is reachable from the package namespace + and that each re-export is the identical object as its source definition. + """ + + def test_all_symbols_importable(self): + """Every name listed in __all__ is accessible as an attribute of the package.""" + import PowerPlatform.Dataverse.operations as m + + for name in m.__all__: + self.assertTrue( + hasattr(m, name), f"{name!r} is in __all__ but missing from PowerPlatform.Dataverse.operations" + ) + + def test_identity(self): + """Re-exported objects are the same objects as their source definitions.""" + import PowerPlatform.Dataverse.operations as m + from PowerPlatform.Dataverse.operations.batch import ( + BatchDataFrameOperations, + BatchOperations, + BatchQueryOperations, + BatchRecordOperations, + BatchRequest, + BatchTableOperations, + ChangeSet, + ChangeSetRecordOperations, + ) + from PowerPlatform.Dataverse.operations.dataframe import DataFrameOperations + from PowerPlatform.Dataverse.operations.files import FileOperations + from PowerPlatform.Dataverse.operations.query import QueryOperations + from PowerPlatform.Dataverse.operations.records import RecordOperations + from PowerPlatform.Dataverse.operations.tables import TableOperations + + self.assertIs(m.BatchDataFrameOperations, BatchDataFrameOperations) + self.assertIs(m.BatchOperations, BatchOperations) + self.assertIs(m.BatchQueryOperations, BatchQueryOperations) + self.assertIs(m.BatchRecordOperations, BatchRecordOperations) + self.assertIs(m.BatchRequest, BatchRequest) + self.assertIs(m.BatchTableOperations, BatchTableOperations) + self.assertIs(m.ChangeSet, ChangeSet) + self.assertIs(m.ChangeSetRecordOperations, ChangeSetRecordOperations) + self.assertIs(m.DataFrameOperations, DataFrameOperations) + self.assertIs(m.FileOperations, FileOperations) + self.assertIs(m.QueryOperations, QueryOperations) + self.assertIs(m.RecordOperations, RecordOperations) + self.assertIs(m.TableOperations, TableOperations) + + +if __name__ == "__main__": + unittest.main()