Skip to content

Commit 8c0fa7e

Browse files
tpellissier-msfttpellissierclaude
authored
Add RelationshipInfo typed return model (#114)
* Add RelationshipInfo typed return model for relationship methods 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) <noreply@anthropic.com> * Use shared OData type constants from common.constants instead of local 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) <noreply@anthropic.com> * Address review: raise on unknown type, reuse factories, list all fields 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) <noreply@anthropic.com> * Reorganize models: relationship.py (domain-based) + labels.py 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) <noreply@anthropic.com> * Fix create_lookup_field docstring to list all returned fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use direct key access instead of .get() for internal relationship dicts 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) <noreply@anthropic.com> * Address review: remove defaults in from_api_response, delete accidental 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) <noreply@anthropic.com> * Fix test: missing required API fields should raise KeyError Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: tpellissier <tpellissier@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 78eb5dd commit 8c0fa7e

File tree

12 files changed

+579
-233
lines changed

12 files changed

+579
-233
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ client.tables.delete("new_Product")
232232

233233
#### Create One-to-Many Relationship
234234
```python
235-
from PowerPlatform.Dataverse.models.metadata import (
235+
from PowerPlatform.Dataverse.models.relationship import (
236236
LookupAttributeMetadata,
237237
OneToManyRelationshipMetadata,
238238
Label,
@@ -264,7 +264,7 @@ print(f"Created lookup field: {result['lookup_schema_name']}")
264264

265265
#### Create Many-to-Many Relationship
266266
```python
267-
from PowerPlatform.Dataverse.models.metadata import ManyToManyRelationshipMetadata
267+
from PowerPlatform.Dataverse.models.relationship import ManyToManyRelationshipMetadata
268268

269269
relationship = ManyToManyRelationshipMetadata(
270270
schema_name="new_employee_project",

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,12 @@ client.tables.delete("new_Product")
317317
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).
318318

319319
```python
320-
from PowerPlatform.Dataverse.models.metadata import (
320+
from PowerPlatform.Dataverse.models.relationship import (
321321
LookupAttributeMetadata,
322322
OneToManyRelationshipMetadata,
323323
ManyToManyRelationshipMetadata,
324-
Label,
325-
LocalizedLabel,
326324
)
325+
from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel
327326

328327
# Create a one-to-many relationship: Department (1) -> Employee (N)
329328
# This adds a "Department" lookup field to the Employee table

examples/advanced/relationships.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@
2020
import time
2121
from azure.identity import InteractiveBrowserCredential
2222
from PowerPlatform.Dataverse.client import DataverseClient
23-
from PowerPlatform.Dataverse.models.metadata import (
23+
from PowerPlatform.Dataverse.models.relationship import (
2424
LookupAttributeMetadata,
2525
OneToManyRelationshipMetadata,
2626
ManyToManyRelationshipMetadata,
27-
Label,
28-
LocalizedLabel,
2927
CascadeConfiguration,
3028
)
29+
from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel
3130
from PowerPlatform.Dataverse.common.constants import (
3231
CASCADE_BEHAVIOR_NO_CASCADE,
3332
CASCADE_BEHAVIOR_REMOVE_LINK,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ client.tables.delete("new_Product")
232232

233233
#### Create One-to-Many Relationship
234234
```python
235-
from PowerPlatform.Dataverse.models.metadata import (
235+
from PowerPlatform.Dataverse.models.relationship import (
236236
LookupAttributeMetadata,
237237
OneToManyRelationshipMetadata,
238238
Label,
@@ -264,7 +264,7 @@ print(f"Created lookup field: {result['lookup_schema_name']}")
264264

265265
#### Create Many-to-Many Relationship
266266
```python
267-
from PowerPlatform.Dataverse.models.metadata import ManyToManyRelationshipMetadata
267+
from PowerPlatform.Dataverse.models.relationship import ManyToManyRelationshipMetadata
268268

269269
relationship = ManyToManyRelationshipMetadata(
270270
schema_name="new_employee_project",

src/PowerPlatform/Dataverse/data/_relationships.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ def _create_one_to_many_relationship(
3535
Posts to /RelationshipDefinitions with OneToManyRelationshipMetadata.
3636
3737
:param lookup: Lookup attribute metadata (LookupAttributeMetadata instance).
38-
:type lookup: ~PowerPlatform.Dataverse.models.metadata.LookupAttributeMetadata
38+
:type lookup: ~PowerPlatform.Dataverse.models.relationship.LookupAttributeMetadata
3939
:param relationship: Relationship metadata (OneToManyRelationshipMetadata instance).
40-
:type relationship: ~PowerPlatform.Dataverse.models.metadata.OneToManyRelationshipMetadata
40+
:type relationship: ~PowerPlatform.Dataverse.models.relationship.OneToManyRelationshipMetadata
4141
:param solution: Optional solution unique name to add the relationship to.
4242
:type solution: ``str`` | ``None``
4343
@@ -80,7 +80,7 @@ def _create_many_to_many_relationship(
8080
Posts to /RelationshipDefinitions with ManyToManyRelationshipMetadata.
8181
8282
:param relationship: Relationship metadata (ManyToManyRelationshipMetadata instance).
83-
:type relationship: ~PowerPlatform.Dataverse.models.metadata.ManyToManyRelationshipMetadata
83+
:type relationship: ~PowerPlatform.Dataverse.models.relationship.ManyToManyRelationshipMetadata
8484
:param solution: Optional solution unique name to add the relationship to.
8585
:type solution: ``str`` | ``None``
8686
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
"""Label models for Dataverse metadata."""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any, Dict, List, Optional
9+
from dataclasses import dataclass
10+
11+
from ..common.constants import (
12+
ODATA_TYPE_LOCALIZED_LABEL,
13+
ODATA_TYPE_LABEL,
14+
)
15+
16+
17+
@dataclass
18+
class LocalizedLabel:
19+
"""
20+
Represents a localized label with a language code.
21+
22+
:param label: The text of the label.
23+
:type label: str
24+
:param language_code: The language code (LCID), e.g., 1033 for English.
25+
:type language_code: int
26+
:param additional_properties: Optional dict of additional properties to include
27+
in the Web API payload. These are merged last and can override default values.
28+
:type additional_properties: Optional[Dict[str, Any]]
29+
"""
30+
31+
label: str
32+
language_code: int
33+
additional_properties: Optional[Dict[str, Any]] = None
34+
35+
def to_dict(self) -> Dict[str, Any]:
36+
"""
37+
Convert to Web API JSON format.
38+
39+
Example::
40+
41+
>>> label = LocalizedLabel(label="Account", language_code=1033)
42+
>>> label.to_dict()
43+
{
44+
'@odata.type': 'Microsoft.Dynamics.CRM.LocalizedLabel',
45+
'Label': 'Account',
46+
'LanguageCode': 1033
47+
}
48+
"""
49+
result = {
50+
"@odata.type": ODATA_TYPE_LOCALIZED_LABEL,
51+
"Label": self.label,
52+
"LanguageCode": self.language_code,
53+
}
54+
if self.additional_properties:
55+
result.update(self.additional_properties)
56+
return result
57+
58+
59+
@dataclass
60+
class Label:
61+
"""
62+
Represents a label that can have multiple localized versions.
63+
64+
:param localized_labels: List of LocalizedLabel instances.
65+
:type localized_labels: List[LocalizedLabel]
66+
:param user_localized_label: Optional user-specific localized label.
67+
:type user_localized_label: Optional[LocalizedLabel]
68+
:param additional_properties: Optional dict of additional properties to include
69+
in the Web API payload. These are merged last and can override default values.
70+
:type additional_properties: Optional[Dict[str, Any]]
71+
"""
72+
73+
localized_labels: List[LocalizedLabel]
74+
user_localized_label: Optional[LocalizedLabel] = None
75+
additional_properties: Optional[Dict[str, Any]] = None
76+
77+
def to_dict(self) -> Dict[str, Any]:
78+
"""
79+
Convert to Web API JSON format.
80+
81+
Example::
82+
83+
>>> label = Label(localized_labels=[LocalizedLabel("Account", 1033)])
84+
>>> label.to_dict()
85+
{
86+
'@odata.type': 'Microsoft.Dynamics.CRM.Label',
87+
'LocalizedLabels': [
88+
{'@odata.type': '...', 'Label': 'Account', 'LanguageCode': 1033}
89+
],
90+
'UserLocalizedLabel': {'@odata.type': '...', 'Label': 'Account', ...}
91+
}
92+
"""
93+
result = {
94+
"@odata.type": ODATA_TYPE_LABEL,
95+
"LocalizedLabels": [ll.to_dict() for ll in self.localized_labels],
96+
}
97+
# Use explicit user_localized_label, or default to first localized label
98+
if self.user_localized_label:
99+
result["UserLocalizedLabel"] = self.user_localized_label.to_dict()
100+
elif self.localized_labels:
101+
result["UserLocalizedLabel"] = self.localized_labels[0].to_dict()
102+
if self.additional_properties:
103+
result.update(self.additional_properties)
104+
return result
105+
106+
107+
__all__ = ["LocalizedLabel", "Label"]

0 commit comments

Comments
 (0)