Add RelationshipInfo typed return model#114
Conversation
Introduces RelationshipInfo dataclass as the return type for all relationship operations (create_one_to_many_relationship, create_many_to_many_relationship, get_relationship, create_lookup_field). Factory methods: from_one_to_many(), from_many_to_many(), from_api_response() which detects 1:N vs N:N from @odata.type in API responses. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR introduces a typed RelationshipInfo dataclass as the return type for relationship operations, replacing raw dictionaries with a structured model that provides attribute access and better IDE support. The changes align with modern Python API design patterns by offering factory methods for creating instances from different sources.
Changes:
- Added
RelationshipInfodataclass with factory methods for one-to-many, many-to-many, and API response parsing - Updated four relationship operations in
TableOperationsto returnRelationshipInfoinstead ofDict[str, Any] - Added comprehensive test coverage with 10 new model tests and updated operation tests
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/PowerPlatform/Dataverse/models/relationship_info.py |
New typed model with dataclass pattern and three factory methods for different relationship types |
src/PowerPlatform/Dataverse/operations/tables.py |
Updated return types and implementations for create_one_to_many_relationship, create_many_to_many_relationship, get_relationship, and create_lookup_field to return RelationshipInfo |
tests/unit/models/test_relationship_info.py |
New comprehensive test suite covering all factory methods and edge cases |
tests/unit/test_tables_operations.py |
Updated operation tests to assert RelationshipInfo instances and validate attribute access |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…l duplicates Import ODATA_TYPE_ONE_TO_MANY_RELATIONSHIP and ODATA_TYPE_MANY_TO_MANY_RELATIONSHIP from common.constants to maintain consistency with the rest of the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
since we have the RelationshipInfo now, _create_one_to_many_relationship etc. can probably use that instead of creating a new dict |
…ds in docstrings - from_api_response raises ValueError for unrecognized @odata.type (Dataverse only supports OneToMany and ManyToMany) - from_api_response delegates to from_one_to_many/from_many_to_many instead of constructing cls() directly - Docstrings on create methods now list all returned fields explicitly Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Move relationship input models (CascadeConfiguration, LookupAttributeMetadata, OneToManyRelationshipMetadata, ManyToManyRelationshipMetadata) from metadata.py into relationship.py alongside RelationshipInfo output model. Extract Label/LocalizedLabel into labels.py. Delete metadata.py. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Missing keys from our own OData layer would be a bug; fail loudly with KeyError instead of silently defaulting to empty string. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
It would probably work fine, but I might prefer keeping the model types (public, fixed contract) uncoupled to the odata layer (private methods, we can reimplement as we please). How do you feel about that aspect? |
…al design doc - Use direct key access for required API fields (ReferencedEntity, etc.) instead of .get() with empty string defaults - Keep .get() only for ReferencingEntityNavigationPropertyName which may be absent in the API response - Remove docs/design/relationships_integration.md (old design doc accidentally included in reorg commit) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Resolve merge conflicts: - Remove redundant models/relationship_info.py (RelationshipInfo lives in models/relationship.py since PR #114) - Update imports in tables.py and tests to use models.relationship - Use direct dict access for required keys (main's convention) - Use main's detailed :rtype: docstrings - Fix test_relationship_info.py to match main's ValueError behavior for unrecognized @odata.type
Resolve merge conflicts: - Remove redundant models/relationship_info.py (RelationshipInfo lives in models/relationship.py since PR #114) - Update imports in tables.py and tests to use models.relationship - Use direct dict access for required keys (main's convention) - Use main's detailed :rtype: docstrings - Fix test_relationship_info.py to match main's ValueError behavior for unrecognized @odata.type
## Summary Add end-to-end relationship tests that validate the full relationship API lifecycle against a live Dataverse environment. This is the primary pre-GA validation for Tim's relationship PRs (#88, #105, #114). ## Changes ### New: `tests/e2e/test_relationships_e2e.py` 11 curated e2e tests covering: | Test Class | Tests | Coverage | |---|---|---| | `TestOneToManyCore` | 1 | Full 1:N lifecycle: create, get, delete, field assertions | | `TestLookupField` | 1 | Convenience `create_lookup_field` to system table | | `TestManyToMany` | 2 | N:N lifecycle + nonexistent returns None | | `TestDataThroughRelationships` | 4 | `@odata.bind`, `$expand`, `$filter` on lookup, update binding | | `TestCascadeBehaviors` | 2 | Restrict blocks delete; Cascade deletes children | | `TestTypeDetection` | 1 | `get_relationship` distinguishes 1:N vs N:N | ### Updated: `examples/basic/functional_testing.py` - Added relationship testing section covering 1:N core API, convenience API, N:N, get, and delete - Added relationship imports and retry helpers ### Updated: `pyproject.toml` - Added `[tool.pytest.ini_options]` with `testpaths = ["tests/unit"]` - Default `pytest` runs only unit tests; e2e tests require explicit invocation ## How to run e2e tests ```bash # Set your Dataverse org URL export DATAVERSE_URL=https://yourorg.crm.dynamics.com # Run relationship e2e tests pytest tests/e2e/ -v -s ``` The tests authenticate via `InteractiveBrowserCredential` and create/delete temporary tables (prefixed `test_E2E*`). ## E2E Test Results (from `.scratch/` comprehensive suite) Ran 30 tests against `https://aurorabapenv71aff.crm10.dynamics.com`: - 25/30 passed on first run - 5 failures were test bugs (not SDK bugs), all fixed: - Metadata propagation timing (increased retries) - Navigation property name casing (`$expand` needs server-assigned nav prop) - `IsValidForAdvancedFind` requires `BooleanManagedProperty` complex type ## Finding: SDK inconsistency to address before GA `create_one_to_many_relationship()` returns `lookup_schema_name` as the user-provided SchemaName, but `$expand` requires the server-assigned `ReferencingEntityNavigationPropertyName` (which may differ in casing). The e2e tests work around this by calling `get_relationship()` after create to get the correct nav prop name. This should be harmonized before GA. ## Checklist - [x] 398 unit tests pass - [x] 11 e2e tests collected by pytest - [x] Default `pytest` excludes e2e (runs unit only) - [x] Code formatted with black - [x] Branch rebased on origin/main --------- Co-authored-by: Saurabh Badenkal <[email protected]>
Summary
RelationshipInfodataclass as the return type for all relationship operationsDict[str, Any]returns with typed model providing attribute accessfrom_one_to_many(),from_many_to_many(),from_api_response()(auto-detects type from@odata.type)Changed methods
tables.create_one_to_many_relationship()→ returnsRelationshipInfotables.create_many_to_many_relationship()→ returnsRelationshipInfotables.get_relationship()→ returnsOptional[RelationshipInfo]tables.create_lookup_field()→ returnsRelationshipInfoTest plan
🤖 Generated with Claude Code