Skip to content

Commit 7d4326f

Browse files
lubosmjfilak-sap
authored andcommitted
Handle association set ends with same entity sets
In the metadata document, it is allowed to declare same Entity Sets in different ends of one Association Set. Therefore, we cannot utilize dictionary to store such ends (values can be overwritten). In this commit, there is added a new class AssociationSetEndRole which represents one end within the Association Set. End roles are stored in a list, in the same manner like end roles in the Association. Also, each end now contains a reference to a corresponding entity set.
1 parent f501ae0 commit 7d4326f

File tree

4 files changed

+135
-33
lines changed

4 files changed

+135
-33
lines changed

pyodata/v2/model.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -898,17 +898,19 @@ def from_etree(schema_nodes):
898898
'Association {} does not exist in namespace {}'
899899
.format(assoc_set.association_type_name, assoc_set.association_type_namespace))
900900

901-
for key, value in list(assoc_set.end_roles.items()):
902-
# Check if entity set exists in current scheme
901+
for end in assoc_set.end_roles:
902+
# Check if an entity set exists in the current scheme
903+
# and add a reference to the corresponding entity set
903904
try:
904-
schema.entity_set(key, namespace)
905+
entity_set = schema.entity_set(end.entity_set_name, namespace)
906+
end.entity_set = entity_set
905907
except KeyError:
906908
raise PyODataModelError('EntitySet {} does not exist in Schema Namespace {}'
907-
.format(key, namespace))
909+
.format(end.entity_set_name, namespace))
908910
# Check if role is defined in Association
909-
if assoc_set.association_type.end_by_role(value) is None:
911+
if assoc_set.association_type.end_by_role(end.role) is None:
910912
raise PyODataModelError('Role {} is not defined in association {}'
911-
.format(value, assoc_set.association_type_name))
913+
.format(end.role, assoc_set.association_type_name))
912914

913915
decl.association_sets[assoc_set.name] = assoc_set
914916

@@ -1556,6 +1558,46 @@ def from_etree(association_node):
15561558
return association
15571559

15581560

1561+
class AssociationSetEndRole:
1562+
def __init__(self, role, entity_set_name):
1563+
self._role = role
1564+
self._entity_set_name = entity_set_name
1565+
self._entity_set = None
1566+
1567+
def __repr__(self):
1568+
return "{0}({1})".format(self.__class__.__name__, self.role)
1569+
1570+
@property
1571+
def role(self):
1572+
return self._role
1573+
1574+
@property
1575+
def entity_set_name(self):
1576+
return self._entity_set_name
1577+
1578+
@property
1579+
def entity_set(self):
1580+
return self._entity_set
1581+
1582+
@entity_set.setter
1583+
def entity_set(self, value):
1584+
if self._entity_set:
1585+
raise PyODataModelError('Cannot replace {0} of {1} to {2}'.format(self._entity_set, self, value))
1586+
1587+
if value.name != self._entity_set_name:
1588+
raise PyODataModelError(
1589+
'Assigned entity set {0} differentiates from the declared {1}'.format(value, self._entity_set_name))
1590+
1591+
self._entity_set = value
1592+
1593+
@staticmethod
1594+
def from_etree(end_node):
1595+
role = end_node.get('Role')
1596+
entity_set = end_node.get('EntitySet')
1597+
1598+
return AssociationSetEndRole(role, entity_set)
1599+
1600+
15591601
class AssociationSet:
15601602
def __init__(self, name, association_type_name, association_type_namespace, end_roles):
15611603
self._name = name
@@ -1587,6 +1629,18 @@ def association_type_namespace(self):
15871629
def end_roles(self):
15881630
return self._end_roles
15891631

1632+
def end_by_role(self, end_role):
1633+
try:
1634+
return next((end for end in self._end_roles if end.role == end_role))
1635+
except StopIteration:
1636+
raise KeyError('Association set {} has no End with Role {}'.format(self._name, end_role))
1637+
1638+
def end_by_entity_set(self, entity_set):
1639+
try:
1640+
return next((end for end in self._end_roles if end.entity_set_name == entity_set))
1641+
except StopIteration:
1642+
raise KeyError('Association set {} has no End with Entity Set {}'.format(self._name, entity_set))
1643+
15901644
@association_type.setter
15911645
def association_type(self, value):
15921646
if self._association_type is not None:
@@ -1595,7 +1649,7 @@ def association_type(self, value):
15951649

15961650
@staticmethod
15971651
def from_etree(association_set_node):
1598-
end_roles = {}
1652+
end_roles = []
15991653
name = association_set_node.get('Name')
16001654
scheme_namespace, association_type_name = association_set_node.get('Association').split('.', 1)
16011655

@@ -1604,9 +1658,7 @@ def from_etree(association_set_node):
16041658
raise PyODataModelError('Association {} cannot have more than 2 end roles'.format(name))
16051659

16061660
for end_role in end_roles_list:
1607-
entity_set = end_role.get('EntitySet')
1608-
role = end_role.get('Role')
1609-
end_roles[entity_set] = role
1661+
end_roles.append(AssociationSetEndRole.from_etree(end_role))
16101662

16111663
return AssociationSet(name, association_type_name, scheme_namespace, end_roles)
16121664

pyodata/v2/service.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -789,9 +789,9 @@ def nav(self, nav_property):
789789
association_info.namespace)
790790

791791
navigation_entity_set = None
792-
for entity_set in association_set.end_roles:
793-
if association_set.end_roles[entity_set] == navigation_property.to_role.role:
794-
navigation_entity_set = self._service.schema.entity_set(entity_set, association_info.namespace)
792+
for end in association_set.end_roles:
793+
if association_set.end_by_entity_set(end.entity_set_name).role == navigation_property.to_role.role:
794+
navigation_entity_set = self._service.schema.entity_set(end.entity_set_name, association_info.namespace)
795795

796796
if not navigation_entity_set:
797797
raise PyODataException('No association set for role {}'.format(navigation_property.to_role))
@@ -997,12 +997,12 @@ def nav(self, nav_property, key):
997997
association_info.name)
998998

999999
navigation_entity_set = None
1000-
for entity_set in association_set.end_roles:
1001-
if association_set.end_roles[entity_set] == navigation_property.to_role.role:
1002-
navigation_entity_set = self._service.schema.entity_set(entity_set)
1000+
for end in association_set.end_roles:
1001+
if association_set.end_by_entity_set(end.entity_set_name).role == navigation_property.to_role.role:
1002+
navigation_entity_set = self._service.schema.entity_set(end.entity_set_name)
10031003

10041004
if not navigation_entity_set:
1005-
raise PyODataException('No association set for role {}'.format(navigation_property.to_role))
1005+
raise PyODataException('No association set for role {} {}'.format(navigation_property.to_role, association_set.end_roles))
10061006

10071007
roles = navigation_property.association.end_roles
10081008
if all((role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE for role in roles)):

tests/conftest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ def metadata():
128128
</Dependent>
129129
</ReferentialConstraint>
130130
</Association>
131-
132131
<Association Name="CustomerOrders">
133132
<End Role="CustomerRole" Type="EXAMPLE_SRV.Customer" Multiplicity="1"/>
134133
<End Role="OrdersRole" Type="EXAMPLE_SRV.Order" Multiplicity="*"/>
@@ -141,7 +140,10 @@ def metadata():
141140
</Dependent>
142141
</ReferentialConstraint>
143142
</Association>
144-
143+
<Association Name="toSelfMaster" sap:content-version="1">
144+
<End Type="EXAMPLE_SRV.MasterEntity" Multiplicity="1" Role="FromRole_toSelfMaster" />
145+
<End Type="EXAMPLE_SRV.MasterEntity" Multiplicity="0..1" Role="ToRole_toSelfMaster" />
146+
</Association>
145147
<EntityContainer Name="EXAMPLE_SRV" m:IsDefaultEntityContainer="true" sap:supported-formats="atom json xlsx">
146148
<EntitySet Name="MasterEntities" EntityType="EXAMPLE_SRV.MasterEntity" sap:label="Master entities" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:searchable="true" sap:content-version="1"/>
147149
<EntitySet Name="DataValueHelp" EntityType="EXAMPLE_SRV.DataEntity" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:searchable="true" sap:content-version="1"/>
@@ -161,6 +163,10 @@ def metadata():
161163
<End EntitySet="Cars" Role="FromRole_toCarIDPic" />
162164
<End EntitySet="CarIDPics" Role="ToRole_toCarIDPic" />
163165
</AssociationSet>
166+
<AssociationSet Name="toSelfMasterSet" Association="EXAMPLE_SRV.toSelfMaster" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:content-version="1">
167+
<End EntitySet="MasterEntities" Role="FromRole_toSelfMaster" />
168+
<End EntitySet="MasterEntities" Role="ToRole_toSelfMaster" />
169+
</AssociationSet>
164170
</EntityContainer>
165171
<Annotations xmlns="http://docs.oasis-open.org/odata/ns/edm" Target="EXAMPLE_SRV.MasterEntity/Data">
166172
<Annotation Term="com.sap.vocabularies.Common.v1.ValueList">

tests/test_model_v2.py

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
from datetime import datetime
55
import pytest
6-
import pyodata.v2.model
7-
from pyodata.v2.model import Edmx, Typ, StructTypeProperty, Types, EntityType, EdmStructTypeSerializer
6+
from pyodata.v2.model import Edmx, Typ, StructTypeProperty, Types, EntityType, EdmStructTypeSerializer,\
7+
Association, AssociationSet, EndRole, AssociationSetEndRole
88
from pyodata.exceptions import PyODataException, PyODataModelError
99

1010

@@ -163,10 +163,13 @@ def test_schema_entity_sets(schema):
163163
def test_edmx_associations(schema):
164164
"""Test parsing of associations and association sets"""
165165

166-
assert set((association.name for association in schema.associations)) == {'toCarIDPic',
167-
'toDataEntity',
168-
'CustomerOrders',
169-
'AssociationEmployeeAddress'}
166+
assert set((association.name for association in schema.associations)) == {
167+
'toCarIDPic',
168+
'toDataEntity',
169+
'CustomerOrders',
170+
'AssociationEmployeeAddress',
171+
'toSelfMaster'
172+
}
170173

171174
association = schema.association('toDataEntity')
172175
assert str(association) == 'Association(toDataEntity)'
@@ -187,14 +190,31 @@ def test_edmx_associations(schema):
187190
assert dependent_role.name == 'ToRole_toDataEntity'
188191
assert dependent_role.property_names == ['Name']
189192

190-
assert set((association_set.name for association_set in schema.association_sets)) == {'toDataEntitySet',
191-
'AssociationEmployeeAddress_AssocSet',
192-
'CustomerOrder_AssocSet',
193-
'toCarIDPicSet'}
193+
assert set((association_set.name for association_set in schema.association_sets)) == {
194+
'toDataEntitySet',
195+
'AssociationEmployeeAddress_AssocSet',
196+
'CustomerOrder_AssocSet',
197+
'toCarIDPicSet',
198+
'toSelfMasterSet'
199+
}
200+
194201
association_set = schema.association_set('toDataEntitySet')
195202
assert str(association_set) == 'AssociationSet(toDataEntitySet)'
196203
assert association_set.association_type.name == 'toDataEntity'
197-
assert association_set.end_roles == {'DataValueHelp': 'ToRole_toDataEntity', 'MasterEntities': 'FromRole_toDataEntity'}
204+
205+
# check associated references to entity sets
206+
association_set = schema.association_set('toDataEntitySet')
207+
entity_sets = {end.entity_set.name for end in association_set.end_roles}
208+
assert entity_sets == {'MasterEntities', 'DataValueHelp'}
209+
210+
end_roles = {(end.entity_set_name, end.role) for end in association_set.end_roles}
211+
assert end_roles == {('DataValueHelp', 'ToRole_toDataEntity'), ('MasterEntities', 'FromRole_toDataEntity')}
212+
213+
# same entity sets in different ends
214+
association_set = schema.association_set('toSelfMasterSet')
215+
assert str(association_set) == 'AssociationSet(toSelfMasterSet)'
216+
end_roles = {(end.entity_set_name, end.role) for end in association_set.end_roles}
217+
assert end_roles == {('MasterEntities', 'ToRole_toSelfMaster'), ('MasterEntities', 'FromRole_toSelfMaster')}
198218

199219
# with namespace
200220
association_set = schema.association_set_by_association('CustomerOrders', namespace='EXAMPLE_SRV_SETS')
@@ -686,10 +706,10 @@ def test_edmx_entity_sets(schema):
686706
def test_edmx_association_end_by_role():
687707
"""Test the method end_by_role of the class Association"""
688708

689-
end_from = pyodata.v2.model.EndRole(None, pyodata.v2.model.EndRole.MULTIPLICITY_ONE, 'From')
690-
end_to = pyodata.v2.model.EndRole(None, pyodata.v2.model.EndRole.MULTIPLICITY_ZERO_OR_ONE, 'To')
709+
end_from = EndRole(None, EndRole.MULTIPLICITY_ONE, 'From')
710+
end_to = EndRole(None, EndRole.MULTIPLICITY_ZERO_OR_ONE, 'To')
691711

692-
association = pyodata.v2.model.Association('FooBar')
712+
association = Association('FooBar')
693713
association.end_roles.append(end_from)
694714
association.end_roles.append(end_to)
695715

@@ -699,3 +719,27 @@ def test_edmx_association_end_by_role():
699719
with pytest.raises(KeyError) as typ_ex_info:
700720
association.end_by_role('Blah')
701721
assert typ_ex_info.value.args[0] == 'Association FooBar has no End with Role Blah'
722+
723+
724+
def test_edmx_association_set_end_by_role():
725+
"""Test the method end_by_role of the class AssociationSet"""
726+
727+
end_from = AssociationSetEndRole('From', 'EntitySet')
728+
end_to = AssociationSetEndRole('To', 'EntitySet')
729+
730+
association_set = AssociationSet('FooBar', 'Foo', 'EXAMPLE_SRV', [end_from, end_to])
731+
732+
assert association_set.end_by_role(end_from.role) == end_from
733+
assert association_set.end_by_role(end_to.role) == end_to
734+
735+
736+
def test_edmx_association_set_end_by_entity_set():
737+
"""Test the method end_by_entity_set of the class AssociationSet"""
738+
739+
end_from = AssociationSetEndRole('From', 'EntitySet1')
740+
end_to = AssociationSetEndRole('To', 'EntitySet2')
741+
742+
association_set = AssociationSet('FooBar', 'Foo', 'EXAMPLE_SRV', [end_from, end_to])
743+
744+
assert association_set.end_by_entity_set(end_from.entity_set_name) == end_from
745+
assert association_set.end_by_entity_set(end_to.entity_set_name) == end_to

0 commit comments

Comments
 (0)