Skip to content

Commit d7550c7

Browse files
committed
Add support for TypeDefinition in OData V4
TypeDefinitions are simply aliases for primitive types. They can be annotated. Thus, reimplementation of annotation for V4 is included in this commit. Children of ODataVersion have to specify which annotations are supported and how they should be processed. Annotation are parsed using function 'build_annotation'. As annotation are always tied to specific element and there is no centralized repository of annotations this function must return void. http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part3-csdl/odata-v4.0-errata03-os-part3-csdl-complete.html#_Toc453752574
1 parent 93aeb4d commit d7550c7

File tree

13 files changed

+256
-115
lines changed

13 files changed

+256
-115
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1414
- Support for OData V4 primitive types - Martin Miksik
1515
- Support for navigation property in OData v4 - Martin Miksik
1616
- Support for EntitySet in OData v4 - Martin Miksik
17+
- Support for TypeDefinition in OData v4 - Martin Miksik
1718

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

pyodata/config.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
# pylint: disable=cyclic-import
99
if TYPE_CHECKING:
10-
from pyodata.model.elements import Typ # noqa
10+
from pyodata.model.elements import Typ, Annotation # noqa
1111

1212

1313
class ODATAVersion(ABC):
@@ -22,7 +22,6 @@ def __init__(self):
2222
# Separate dictionary of all registered types (primitive, complex and collection variants) for each child
2323
Types: Dict[str, 'Typ'] = dict()
2424

25-
2625
@staticmethod
2726
@abstractmethod
2827
def primitive_types() -> List['Typ']:
@@ -33,6 +32,11 @@ def primitive_types() -> List['Typ']:
3332
def build_functions() -> Dict[type, Callable]:
3433
""" Here we define which elements are supported and what is their python representation"""
3534

35+
@staticmethod
36+
@abstractmethod
37+
def annotations() -> Dict['Annotation', Callable]:
38+
""" Here we define which annotations are supported and what is their python representation"""
39+
3640

3741
class Config:
3842
# pylint: disable=too-many-instance-attributes,missing-docstring
@@ -74,8 +78,8 @@ def __init__(self,
7478
self._odata_version = odata_version
7579

7680
self._sap_value_helper_directions = None
77-
self._sap_annotation_value_list = None
7881
self._annotation_namespaces = None
82+
self._aliases: Dict[str, str] = dict()
7983

8084
def err_policy(self, error: ParserError) -> ErrorPolicy:
8185
""" Returns error policy for given error. If custom error policy fo error is set, then returns that."""
@@ -112,8 +116,12 @@ def sap_value_helper_directions(self):
112116
return self._sap_value_helper_directions
113117

114118
@property
115-
def sap_annotation_value_list(self):
116-
return self._sap_annotation_value_list
119+
def aliases(self) -> Dict[str, str]:
120+
return self._aliases
121+
122+
@aliases.setter
123+
def aliases(self, value: Dict[str, str]):
124+
self._aliases = value
117125

118126
@property
119127
def annotation_namespace(self):

pyodata/model/build_functions.py

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import copy
55
import logging
66

7+
from pyodata.policies import ParserError
78
from pyodata.config import Config
8-
from pyodata.exceptions import PyODataParserError
9+
from pyodata.exceptions import PyODataParserError, PyODataModelError
910
from pyodata.model.elements import sap_attribute_get_bool, sap_attribute_get_string, StructType, StructTypeProperty, \
1011
Types, EnumType, EnumMember, EntitySet, ValueHelper, ValueHelperParameter, FunctionImportParameter, \
11-
FunctionImport, metadata_attribute_get, EntityType, ComplexType, Annotation, build_element
12+
FunctionImport, metadata_attribute_get, EntityType, ComplexType, build_element
1213

13-
from pyodata.v4 import ODataV4
14-
import pyodata.v4.elements as v4
14+
# pylint: disable=cyclic-import
15+
# When using `import xxx as yyy` it is not a problem and we need this dependency
16+
import pyodata.v4 as v4
1517

1618

1719
def modlog():
@@ -172,39 +174,15 @@ def build_entity_set(config, entity_set_node):
172174
req_filter = sap_attribute_get_bool(entity_set_node, 'requires-filter', False)
173175
label = sap_attribute_get_string(entity_set_node, 'label')
174176

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)
177+
if config.odata_version == v4.ODataV4:
178+
return v4.EntitySet(name, et_info, addressable, creatable, updatable, deletable, searchable, countable,
179+
pageable, topable, req_filter, label, nav_prop_bins)
178180

179181
return EntitySet(name, et_info, addressable, creatable, updatable, deletable, searchable, countable, pageable,
180182
topable, req_filter, label)
181183

182184

183-
def build_external_annotation(config, annotations_node):
184-
target = annotations_node.get('Target')
185-
186-
if annotations_node.get('Qualifier'):
187-
modlog().warning('Ignoring qualified Annotations of %s', target)
188-
return
189-
190-
for annotation in annotations_node.xpath('edm:Annotation', namespaces=config.annotation_namespace):
191-
annot = build_element(Annotation, config, target=target, annotation_node=annotation)
192-
if annot is None:
193-
continue
194-
yield annot
195-
196-
197-
def build_annotation(config, target, annotation_node):
198-
term = annotation_node.get('Term')
199-
200-
if term in config.sap_annotation_value_list:
201-
return build_element(ValueHelper, config, target=target, annotation_node=annotation_node)
202-
203-
modlog().warning('Unsupported Annotation( %s )', term)
204-
return None
205-
206-
207-
def build_value_helper(config, target, annotation_node):
185+
def build_value_helper(config, target, annotation_node, schema):
208186
label = None
209187
collection_path = None
210188
search_supported = False
@@ -229,7 +207,31 @@ def build_value_helper(config, target, annotation_node):
229207
param.value_helper = value_helper
230208
value_helper._parameters.append(param)
231209

232-
return value_helper
210+
try:
211+
try:
212+
value_helper.entity_set = schema.entity_set(
213+
value_helper.collection_path, namespace=value_helper.element_namespace)
214+
except KeyError:
215+
raise RuntimeError(f'Entity Set {value_helper.collection_path} '
216+
f'for {value_helper} does not exist')
217+
218+
try:
219+
vh_type = schema.typ(value_helper.proprty_entity_type_name,
220+
namespace=value_helper.element_namespace)
221+
except KeyError:
222+
raise RuntimeError(f'Target Type {value_helper.proprty_entity_type_name} '
223+
f'of {value_helper} does not exist')
224+
225+
try:
226+
target_proprty = vh_type.proprty(value_helper.proprty_name)
227+
except KeyError:
228+
raise RuntimeError(f'Target Property {value_helper.proprty_name} '
229+
f'of {vh_type} as defined in {value_helper} does not exist')
230+
231+
value_helper.proprty = target_proprty
232+
target_proprty.value_helper = value_helper
233+
except (RuntimeError, PyODataModelError) as ex:
234+
config.err_policy(ParserError.ANNOTATION).resolve(ex)
233235

234236

235237
def build_value_helper_parameter(config, value_help_parameter_node):

pyodata/model/builder.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Metadata Builder Implementation"""
22

3-
import collections
43
import io
54
from lxml import etree
65

@@ -24,9 +23,6 @@
2423
}
2524

2625

27-
SAP_ANNOTATION_VALUE_LIST = ['com.sap.vocabularies.Common.v1.ValueList']
28-
29-
3026
# pylint: disable=protected-access
3127
class MetadataBuilder:
3228
"""Metadata builder"""
@@ -99,7 +95,6 @@ def build(self):
9995
self._config.namespaces = namespaces
10096

10197
self._config._sap_value_helper_directions = SAP_VALUE_HELPER_DIRECTIONS
102-
self._config._sap_annotation_value_list = SAP_ANNOTATION_VALUE_LIST
10398
self._config._annotation_namespaces = ANNOTATION_NAMESPACES
10499

105100
self.update_alias(self.get_aliases(xml, self._config), self._config)
@@ -111,31 +106,32 @@ def build(self):
111106
def get_aliases(edmx, config: Config):
112107
"""Get all aliases"""
113108

114-
aliases = collections.defaultdict(set)
109+
# aliases = collections.defaultdict(set)
110+
aliases = {}
115111
edm_root = edmx.xpath('/edmx:Edmx', namespaces=config.namespaces)
116112
if edm_root:
117113
edm_ref_includes = edm_root[0].xpath('edmx:Reference/edmx:Include', namespaces=config.annotation_namespace)
118114
for ref_incl in edm_ref_includes:
119115
namespace = ref_incl.get('Namespace')
120116
alias = ref_incl.get('Alias')
121117
if namespace is not None and alias is not None:
122-
aliases[namespace].add(alias)
118+
aliases[alias] = namespace
119+
# aliases[namespace].add(alias)
123120

124121
return aliases
125122

126123
@staticmethod
127124
def update_alias(aliases, config: Config):
128125
"""Update config with aliases"""
129-
130-
namespace, suffix = config.sap_annotation_value_list[0].rsplit('.', 1)
131-
config._sap_annotation_value_list.extend([alias + '.' + suffix for alias in aliases[namespace]])
132-
126+
config.aliases = aliases
133127
helper_direction_keys = list(config.sap_value_helper_directions.keys())
128+
134129
for direction_key in helper_direction_keys:
135130
namespace, suffix = direction_key.rsplit('.', 1)
136-
for alias in aliases[namespace]:
137-
config._sap_value_helper_directions[alias + '.' + suffix] = \
138-
config.sap_value_helper_directions[direction_key]
131+
for alias, alias_namespace in aliases.items():
132+
if alias_namespace == namespace:
133+
config._sap_value_helper_directions[alias + '.' + suffix] = \
134+
config.sap_value_helper_directions[direction_key]
139135

140136

141137
def schema_from_xml(metadata_xml, namespaces=None):

0 commit comments

Comments
 (0)