Skip to content

Commit 93aeb4d

Browse files
committed
Add support for EntitySet in OData V4
1 parent e9d1180 commit 93aeb4d

File tree

8 files changed

+194
-10
lines changed

8 files changed

+194
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Separate type repositories for individual versions of OData - Martin Miksik
1414
- Support for OData V4 primitive types - Martin Miksik
1515
- Support for navigation property in OData v4 - Martin Miksik
16+
- Support for EntitySet in OData v4 - Martin Miksik
1617

1718
### Changed
1819
- Implementation and naming schema of `from_etree` - Martin Miksik

pyodata/model/build_functions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
Types, EnumType, EnumMember, EntitySet, ValueHelper, ValueHelperParameter, FunctionImportParameter, \
1111
FunctionImport, metadata_attribute_get, EntityType, ComplexType, Annotation, build_element
1212

13+
from pyodata.v4 import ODataV4
14+
import pyodata.v4.elements as v4
15+
1316

1417
def modlog():
1518
return logging.getLogger("callbacks")
@@ -153,6 +156,10 @@ def build_entity_set(config, entity_set_node):
153156
name = entity_set_node.get('Name')
154157
et_info = Types.parse_type_name(entity_set_node.get('EntityType'))
155158

159+
nav_prop_bins = []
160+
for nav_prop_bin in entity_set_node.xpath('edm:NavigationPropertyBinding', namespaces=config.namespaces):
161+
nav_prop_bins.append(build_element('NavigationPropertyBinding', config, node=nav_prop_bin, et_info=et_info))
162+
156163
# TODO: create a class SAP attributes
157164
addressable = sap_attribute_get_bool(entity_set_node, 'addressable', True)
158165
creatable = sap_attribute_get_bool(entity_set_node, 'creatable', True)
@@ -165,6 +172,10 @@ def build_entity_set(config, entity_set_node):
165172
req_filter = sap_attribute_get_bool(entity_set_node, 'requires-filter', False)
166173
label = sap_attribute_get_string(entity_set_node, 'label')
167174

175+
if config.odata_version == ODataV4:
176+
return v4.EntitySet(name, et_info, addressable, creatable, updatable, deletable, searchable, countable, pageable,
177+
topable, req_filter, label, nav_prop_bins)
178+
168179
return EntitySet(name, et_info, addressable, creatable, updatable, deletable, searchable, countable, pageable,
169180
topable, req_filter, label)
170181

pyodata/model/elements.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,10 @@ def nav_proprties(self):
659659
return list(self._nav_properties.values())
660660

661661
def nav_proprty(self, property_name):
662-
return self._nav_properties[property_name]
662+
try:
663+
return self._nav_properties[property_name]
664+
except KeyError as ex:
665+
raise PyODataModelError(f'{self} does not contain navigation property {property_name}') from ex
663666

664667

665668
class EntitySet(Identifier):

pyodata/policies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ class ParserError(Enum):
1212
""" Represents all the different errors the parser is able to deal with."""
1313
PROPERTY = auto()
1414
NAVIGATION_PROPERTY = auto()
15+
NAVIGATION_PROPERTY_BIDING = auto()
1516
ANNOTATION = auto()
1617
ASSOCIATION = auto()
1718

1819
ENUM_TYPE = auto()
1920
ENTITY_TYPE = auto()
21+
ENTITY_SET = auto()
2022
COMPLEX_TYPE = auto()
2123

2224

pyodata/v4/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pyodata.model.type_traits import EdmBooleanTypTraits, EdmIntTypTraits
77
from pyodata.model.elements import Typ, Schema, EnumType, ComplexType, StructType, StructTypeProperty, EntityType
88

9-
from pyodata.v4.elements import NavigationTypeProperty
9+
from pyodata.v4.elements import NavigationTypeProperty, NavigationPropertyBinding, EntitySet
1010
from pyodata.v4.type_traits import EdmDateTypTraits, GeoTypeTraits, EdmDoubleQuotesEncapsulatedTypTraits, \
1111
EdmTimeOfDay, EdmDateTimeOffsetTypTraits, EdmDuration
1212

@@ -23,9 +23,11 @@ def build_functions():
2323
StructTypeProperty: build_functions.build_struct_type_property,
2424
StructType: build_functions.build_struct_type,
2525
NavigationTypeProperty: build_functions_v4.build_navigation_type_property,
26+
NavigationPropertyBinding: build_functions_v4.build_navigation_property_binding,
2627
EnumType: build_functions.build_enum_type,
2728
ComplexType: build_functions.build_complex_type,
2829
EntityType: build_functions.build_entity_type,
30+
EntitySet: build_functions.build_entity_set,
2931
Schema: build_functions_v4.build_schema,
3032
}
3133

pyodata/v4/build_functions.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from pyodata.model.elements import ComplexType, Schema, EnumType, NullType, build_element, EntityType, Types,\
99
StructTypeProperty
1010
from pyodata.policies import ParserError
11-
from pyodata.v4.elements import NavigationTypeProperty, NullProperty, ReferentialConstraint
11+
from pyodata.v4.elements import NavigationTypeProperty, NullProperty, ReferentialConstraint, NavigationPropertyBinding, \
12+
to_path_info, EntitySet
1213

1314

1415
# pylint: disable=protected-access,too-many-locals,too-many-branches,too-many-statements
@@ -104,10 +105,33 @@ def build_schema(config: Config, schema_nodes):
104105
config.err_policy(ParserError.NAVIGATION_PROPERTY).resolve(ex)
105106
nav_prop.partner = NullProperty(nav_prop.partner_info.name)
106107

107-
# TODO: Then, process Associations nodes because they refer EntityTypes and they are referenced by AssociationSets.
108-
# TODO: Then, process EntitySet, FunctionImport and AssociationSet nodes.
109-
# TODO: Finally, process Annotation nodes when all Scheme nodes are completely processed.
108+
# Process entity sets
109+
for schema_node in schema_nodes:
110+
namespace = schema_node.get('Namespace')
111+
decl = schema._decls[namespace]
112+
113+
for entity_set in schema_node.xpath('edm:EntityContainer/edm:EntitySet', namespaces=config.namespaces):
114+
try:
115+
eset = build_element(EntitySet, config, entity_set_node=entity_set)
116+
eset.entity_type = schema.entity_type(eset.entity_type_info[1], namespace=eset.entity_type_info[0])
117+
decl.entity_sets[eset.name] = eset
118+
except (PyODataParserError, KeyError) as ex:
119+
config.err_policy(ParserError.ENTITY_SET).resolve(ex)
120+
121+
# After all entity sets are parsed resolve the individual bindings among them and entity types
122+
for entity_set in schema.entity_sets:
123+
for nav_prop_bin in entity_set.navigation_property_bindings:
124+
path_info = nav_prop_bin.path_info
125+
try:
126+
nav_prop_bin.path = schema.entity_type(path_info.type,
127+
namespace=path_info.namespace).nav_proprty(path_info.proprty)
128+
nav_prop_bin.target = schema.entity_set(nav_prop_bin.target_info)
129+
except (PyODataModelError, KeyError) as ex:
130+
config.err_policy(ParserError.NAVIGATION_PROPERTY_BIDING).resolve(ex)
131+
nav_prop_bin.path = NullType(path_info.type)
132+
nav_prop_bin.target = NullType(nav_prop_bin.target_info)
110133

134+
# TODO: Finally, process Annotation nodes when all Scheme nodes are completely processed.
111135
return schema
112136

113137

@@ -125,3 +149,8 @@ def build_navigation_type_property(config: Config, node):
125149
partner,
126150
node.get('contains_target'),
127151
ref_cons)
152+
153+
154+
def build_navigation_property_binding(config: Config, node, et_info):
155+
return NavigationPropertyBinding(to_path_info(node.get('Path'), et_info), node.get('Target'))
156+

pyodata/v4/elements.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
""" Repository of elements specific to the ODATA V4"""
22
from typing import Optional, List
33

4+
import collections
5+
6+
from pyodata.model import elements
47
from pyodata.exceptions import PyODataModelError
5-
from pyodata.model.elements import VariableDeclaration, StructType
8+
from pyodata.model.elements import VariableDeclaration, StructType, TypeInfo
9+
10+
PathInfo = collections.namedtuple('PathInfo', 'namespace type proprty')
11+
12+
13+
def to_path_info(value: str, et_info: TypeInfo):
14+
""" Helper function for parsing Path attribute on NavigationPropertyBinding property """
15+
if '/' in value:
16+
parts = value.split('.')
17+
entity_name, property_name = parts[-1].split('/')
18+
return PathInfo('.'.join(parts[:-1]), entity_name, property_name)
19+
else:
20+
return PathInfo(et_info.namespace, et_info.name, value)
621

722

823
class NullProperty:
@@ -90,3 +105,61 @@ def partner(self, value: StructType):
90105
@property
91106
def referential_constraints(self) -> List[ReferentialConstraint]:
92107
return self._referential_constraints
108+
109+
110+
class NavigationPropertyBinding:
111+
""" Describes which entity set of navigation property contains related entities
112+
https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/csprd06/odata-csdl-xml-v4.01-csprd06.html#sec_NavigationPropertyBinding
113+
"""
114+
115+
def __init__(self, path_info: PathInfo, target_info: str):
116+
self._path_info = path_info
117+
self._target_info = target_info
118+
self._path: Optional[NavigationTypeProperty] = None
119+
self._target: Optional['EntitySet'] = None
120+
121+
def __repr__(self):
122+
return f"{self.__class__.__name__}({self.path}, {self.target})"
123+
124+
def __str__(self):
125+
return f"{self.__class__.__name__}({self.path}, {self.target})"
126+
127+
@property
128+
def path_info(self) -> PathInfo:
129+
return self._path_info
130+
131+
@property
132+
def target_info(self):
133+
return self._target_info
134+
135+
@property
136+
def path(self) -> Optional[NavigationTypeProperty]:
137+
return self._path
138+
139+
@path.setter
140+
def path(self, value: NavigationTypeProperty):
141+
self._path = value
142+
143+
@property
144+
def target(self) -> Optional['EntitySet']:
145+
return self._target
146+
147+
@target.setter
148+
def target(self, value: 'EntitySet'):
149+
self._target = value
150+
151+
152+
class EntitySet(elements.EntitySet):
153+
""" EntitySet complaint with OData V4
154+
https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/csprd06/odata-csdl-xml-v4.01-csprd06.html#sec_EntitySet
155+
"""
156+
def __init__(self, name, entity_type_info, addressable, creatable, updatable, deletable, searchable, countable,
157+
pageable, topable, req_filter, label, navigation_property_bindings):
158+
super(EntitySet, self).__init__(name, entity_type_info, addressable, creatable, updatable, deletable,
159+
searchable, countable, pageable, topable, req_filter, label)
160+
161+
self._navigation_property_bindings = navigation_property_bindings
162+
163+
@property
164+
def navigation_property_bindings(self) -> List[NavigationPropertyBinding]:
165+
return self._navigation_property_bindings

tests/test_model_v4.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
import geojson
44
import pytest
55

6-
from pyodata.policies import PolicyIgnore
6+
from pyodata.policies import PolicyIgnore, ParserError
77
from pyodata.model.builder import MetadataBuilder
88
from pyodata.exceptions import PyODataModelError, PyODataException, PyODataParserError
99
from pyodata.model.type_traits import TypTraits
10-
from pyodata.model.elements import Types, TypeInfo, NullType
10+
from pyodata.model.elements import Types, TypeInfo, Schema, NullType
1111

1212
from pyodata.config import Config
1313
from pyodata.v4 import ODataV4
1414
from tests.conftest import metadata
15-
from v4 import NavigationTypeProperty
15+
from pyodata.v4.elements import NavigationTypeProperty, EntitySet, NavigationPropertyBinding
1616

1717

1818
def test_type_traits():
@@ -174,6 +174,69 @@ def test_referential_constraint(schema_v4):
174174
'ReferentialConstraint(StructTypeProperty(CategoryID), StructTypeProperty(ID))'
175175

176176

177+
def test_navigation_property_binding(schema_v4: Schema):
178+
"""Test parsing of navigation property bindings on EntitySets"""
179+
eset: EntitySet = schema_v4.entity_set('People')
180+
assert str(eset) == 'EntitySet(People)'
181+
182+
nav_prop_biding: NavigationPropertyBinding = eset.navigation_property_bindings[0]
183+
assert repr(nav_prop_biding) == "NavigationPropertyBinding(NavigationTypeProperty(Friends), EntitySet(People))"
184+
185+
186+
def test_invalid_property_binding_on_entity_set(xml_builder_factory):
187+
"""Test parsing of invalid property bindings on EntitySets"""
188+
schema = """
189+
<EntityType Name="Person">
190+
<NavigationProperty Name="Friends" Type="Collection(MightySchema.Person)" Partner="Friends" />
191+
</EntityType>
192+
<EntityContainer Name="DefaultContainer">
193+
<EntitySet Name="People" EntityType="{}">
194+
<NavigationPropertyBinding Path="{}" Target="{}" />
195+
</EntitySet>
196+
</EntityContainer>
197+
"""
198+
199+
etype, path, target = 'MightySchema.Person', 'Friends', 'People'
200+
201+
xml_builder = xml_builder_factory()
202+
xml_builder.add_schema('MightySchema', schema.format(etype, 'Mistake', target))
203+
xml = xml_builder.serialize()
204+
205+
with pytest.raises(PyODataModelError) as ex_info:
206+
MetadataBuilder(xml, Config(ODataV4)).build()
207+
assert ex_info.value.args[0] == 'EntityType(Person) does not contain navigation property Mistake'
208+
209+
try:
210+
MetadataBuilder(xml, Config(ODataV4, custom_error_policies={
211+
ParserError.NAVIGATION_PROPERTY_BIDING: PolicyIgnore()
212+
})).build()
213+
except BaseException as ex:
214+
raise pytest.fail(f'IgnorePolicy was supposed to silence "{ex}" but it did not.')
215+
216+
xml_builder = xml_builder_factory()
217+
xml_builder.add_schema('MightySchema', schema.format('Mistake', path, target))
218+
xml = xml_builder.serialize()
219+
220+
with pytest.raises(KeyError) as ex_info:
221+
MetadataBuilder(xml, Config(ODataV4)).build()
222+
assert ex_info.value.args[0] == 'EntityType Mistake does not exist in any Schema Namespace'
223+
224+
try:
225+
MetadataBuilder(xml, Config(ODataV4, custom_error_policies={
226+
ParserError.ENTITY_SET: PolicyIgnore()
227+
})).build()
228+
except BaseException as ex:
229+
raise pytest.fail(f'IgnorePolicy was supposed to silence "{ex}" but it did not.')
230+
231+
xml_builder = xml_builder_factory()
232+
xml_builder.add_schema('MightySchema', schema.format(etype, path, 'Mistake'))
233+
xml = xml_builder.serialize()
234+
235+
with pytest.raises(KeyError) as ex_info:
236+
MetadataBuilder(xml, Config(ODataV4)).build()
237+
assert ex_info.value.args[0] == 'EntitySet Mistake does not exist in any Schema Namespace'
238+
239+
177240
def test_enum_parsing(schema_v4):
178241
"""Test correct parsing of enum"""
179242

0 commit comments

Comments
 (0)