|
| 1 | +""" Reusable implementation of from_etree methods for the most of edm elements """ |
| 2 | + |
| 3 | +# pylint: disable=unused-argument, missing-docstring, invalid-name |
| 4 | +import logging |
| 5 | + |
| 6 | +from pyodata.config import Config |
| 7 | +from pyodata.exceptions import PyODataParserError, PyODataModelError |
| 8 | +from pyodata.model.elements import sap_attribute_get_bool, sap_attribute_get_string, StructType, StructTypeProperty, \ |
| 9 | + NavigationTypeProperty, Identifier, Types, EnumType, EnumMember, EntitySet, EndRole, ReferentialConstraint, \ |
| 10 | + PrincipalRole, DependentRole, Association, AssociationSetEndRole, AssociationSet, \ |
| 11 | + ValueHelper, ValueHelperParameter, FunctionImportParameter, \ |
| 12 | + FunctionImport, metadata_attribute_get, EntityType, ComplexType, Annotation |
| 13 | + |
| 14 | + |
| 15 | +def modlog(): |
| 16 | + return logging.getLogger("callbacks") |
| 17 | + |
| 18 | + |
| 19 | +def struct_type_property_from_etree(entity_type_property_node, config: Config): |
| 20 | + return StructTypeProperty( |
| 21 | + entity_type_property_node.get('Name'), |
| 22 | + Types.parse_type_name(entity_type_property_node.get('Type')), |
| 23 | + entity_type_property_node.get('Nullable'), |
| 24 | + entity_type_property_node.get('MaxLength'), |
| 25 | + entity_type_property_node.get('Precision'), |
| 26 | + entity_type_property_node.get('Scale'), |
| 27 | + # TODO: create a class SAP attributes |
| 28 | + sap_attribute_get_bool(entity_type_property_node, 'unicode', True), |
| 29 | + sap_attribute_get_string(entity_type_property_node, 'label'), |
| 30 | + sap_attribute_get_bool(entity_type_property_node, 'creatable', True), |
| 31 | + sap_attribute_get_bool(entity_type_property_node, 'updatable', True), |
| 32 | + sap_attribute_get_bool(entity_type_property_node, 'sortable', True), |
| 33 | + sap_attribute_get_bool(entity_type_property_node, 'filterable', True), |
| 34 | + sap_attribute_get_string(entity_type_property_node, 'filter-restriction'), |
| 35 | + sap_attribute_get_bool(entity_type_property_node, 'required-in-filter', False), |
| 36 | + sap_attribute_get_string(entity_type_property_node, 'text'), |
| 37 | + sap_attribute_get_bool(entity_type_property_node, 'visible', True), |
| 38 | + sap_attribute_get_string(entity_type_property_node, 'display-format'), |
| 39 | + sap_attribute_get_string(entity_type_property_node, 'value-list'), ) |
| 40 | + |
| 41 | + |
| 42 | +# pylint: disable=protected-access |
| 43 | +def struct_type_from_etree(type_node, config: Config, kwargs): |
| 44 | + name = type_node.get('Name') |
| 45 | + label = sap_attribute_get_string(type_node, 'label') |
| 46 | + is_value_list = sap_attribute_get_bool(type_node, 'value-list', False) |
| 47 | + |
| 48 | + stype = kwargs['type'](name, label, is_value_list) |
| 49 | + |
| 50 | + for proprty in type_node.xpath('edm:Property', namespaces=config.namespaces): |
| 51 | + stp = StructTypeProperty.from_etree(proprty, config) |
| 52 | + |
| 53 | + if stp.name in stype._properties: |
| 54 | + raise KeyError('{0} already has property {1}'.format(stype, stp.name)) |
| 55 | + |
| 56 | + stype._properties[stp.name] = stp |
| 57 | + |
| 58 | + # We have to update the property when |
| 59 | + # all properites are loaded because |
| 60 | + # there might be links between them. |
| 61 | + for ctp in list(stype._properties.values()): |
| 62 | + ctp.struct_type = stype |
| 63 | + |
| 64 | + return stype |
| 65 | + |
| 66 | + |
| 67 | +def navigation_type_property_from_etree(node, config: Config): |
| 68 | + return NavigationTypeProperty( |
| 69 | + node.get('Name'), node.get('FromRole'), node.get('ToRole'), Identifier.parse(node.get('Relationship'))) |
| 70 | + |
| 71 | + |
| 72 | +def complex_type_from_etree(etree, config: Config): |
| 73 | + return StructType.from_etree(etree, config, type=ComplexType) |
| 74 | + |
| 75 | + |
| 76 | +# pylint: disable=protected-access |
| 77 | +def entity_type_from_etree(etree, config: Config): |
| 78 | + etype = StructType.from_etree(etree, config, type=EntityType) |
| 79 | + |
| 80 | + for proprty in etree.xpath('edm:Key/edm:PropertyRef', namespaces=config.namespaces): |
| 81 | + etype._key.append(etype.proprty(proprty.get('Name'))) |
| 82 | + |
| 83 | + for proprty in etree.xpath('edm:NavigationProperty', namespaces=config.namespaces): |
| 84 | + navp = NavigationTypeProperty.from_etree(proprty, config) |
| 85 | + |
| 86 | + if navp.name in etype._nav_properties: |
| 87 | + raise KeyError('{0} already has navigation property {1}'.format(etype, navp.name)) |
| 88 | + |
| 89 | + etype._nav_properties[navp.name] = navp |
| 90 | + |
| 91 | + return etype |
| 92 | + |
| 93 | + |
| 94 | +# pylint: disable=protected-access, too-many-locals |
| 95 | +def enum_type_from_etree(type_node, config: Config, kwargs): |
| 96 | + ename = type_node.get('Name') |
| 97 | + is_flags = type_node.get('IsFlags') |
| 98 | + |
| 99 | + namespace = kwargs['namespace'] |
| 100 | + |
| 101 | + underlying_type = type_node.get('UnderlyingType') |
| 102 | + |
| 103 | + valid_types = { |
| 104 | + 'Edm.Byte': [0, 2 ** 8 - 1], |
| 105 | + 'Edm.Int16': [-2 ** 15, 2 ** 15 - 1], |
| 106 | + 'Edm.Int32': [-2 ** 31, 2 ** 31 - 1], |
| 107 | + 'Edm.Int64': [-2 ** 63, 2 ** 63 - 1], |
| 108 | + 'Edm.SByte': [-2 ** 7, 2 ** 7 - 1] |
| 109 | + } |
| 110 | + |
| 111 | + if underlying_type not in valid_types: |
| 112 | + raise PyODataParserError( |
| 113 | + f'Type {underlying_type} is not valid as underlying type for EnumType - must be one of {valid_types}') |
| 114 | + |
| 115 | + mtype = Types.from_name(underlying_type, config) |
| 116 | + etype = EnumType(ename, is_flags, mtype, namespace) |
| 117 | + |
| 118 | + members = type_node.xpath('edm:Member', namespaces=config.namespaces) |
| 119 | + |
| 120 | + next_value = 0 |
| 121 | + for member in members: |
| 122 | + name = member.get('Name') |
| 123 | + value = member.get('Value') |
| 124 | + |
| 125 | + if value is not None: |
| 126 | + next_value = int(value) |
| 127 | + |
| 128 | + vtype = valid_types[underlying_type] |
| 129 | + if not vtype[0] < next_value < vtype[1]: |
| 130 | + raise PyODataParserError(f'Value {next_value} is out of range for type {underlying_type}') |
| 131 | + |
| 132 | + emember = EnumMember(etype, name, next_value) |
| 133 | + etype._member.append(emember) |
| 134 | + |
| 135 | + next_value += 1 |
| 136 | + |
| 137 | + return etype |
| 138 | + |
| 139 | + |
| 140 | +def entity_set_from_etree(entity_set_node, config): |
| 141 | + name = entity_set_node.get('Name') |
| 142 | + et_info = Types.parse_type_name(entity_set_node.get('EntityType')) |
| 143 | + |
| 144 | + # TODO: create a class SAP attributes |
| 145 | + addressable = sap_attribute_get_bool(entity_set_node, 'addressable', True) |
| 146 | + creatable = sap_attribute_get_bool(entity_set_node, 'creatable', True) |
| 147 | + updatable = sap_attribute_get_bool(entity_set_node, 'updatable', True) |
| 148 | + deletable = sap_attribute_get_bool(entity_set_node, 'deletable', True) |
| 149 | + searchable = sap_attribute_get_bool(entity_set_node, 'searchable', False) |
| 150 | + countable = sap_attribute_get_bool(entity_set_node, 'countable', True) |
| 151 | + pageable = sap_attribute_get_bool(entity_set_node, 'pageable', True) |
| 152 | + topable = sap_attribute_get_bool(entity_set_node, 'topable', pageable) |
| 153 | + req_filter = sap_attribute_get_bool(entity_set_node, 'requires-filter', False) |
| 154 | + label = sap_attribute_get_string(entity_set_node, 'label') |
| 155 | + |
| 156 | + return EntitySet(name, et_info, addressable, creatable, updatable, deletable, searchable, countable, pageable, |
| 157 | + topable, req_filter, label) |
| 158 | + |
| 159 | + |
| 160 | +def end_role_from_etree(end_role_node, config): |
| 161 | + entity_type_info = Types.parse_type_name(end_role_node.get('Type')) |
| 162 | + multiplicity = end_role_node.get('Multiplicity') |
| 163 | + role = end_role_node.get('Role') |
| 164 | + |
| 165 | + return EndRole(entity_type_info, multiplicity, role) |
| 166 | + |
| 167 | + |
| 168 | +def referential_constraint_from_etree(referential_constraint_node, config: Config): |
| 169 | + principal = referential_constraint_node.xpath('edm:Principal', namespaces=config.namespaces) |
| 170 | + if len(principal) != 1: |
| 171 | + raise RuntimeError('Referential constraint must contain exactly one principal element') |
| 172 | + |
| 173 | + principal_name = principal[0].get('Role') |
| 174 | + if principal_name is None: |
| 175 | + raise RuntimeError('Principal role name was not specified') |
| 176 | + |
| 177 | + principal_refs = [] |
| 178 | + for property_ref in principal[0].xpath('edm:PropertyRef', namespaces=config.namespaces): |
| 179 | + principal_refs.append(property_ref.get('Name')) |
| 180 | + if not principal_refs: |
| 181 | + raise RuntimeError('In role {} should be at least one principal property defined'.format(principal_name)) |
| 182 | + |
| 183 | + dependent = referential_constraint_node.xpath('edm:Dependent', namespaces=config.namespaces) |
| 184 | + if len(dependent) != 1: |
| 185 | + raise RuntimeError('Referential constraint must contain exactly one dependent element') |
| 186 | + |
| 187 | + dependent_name = dependent[0].get('Role') |
| 188 | + if dependent_name is None: |
| 189 | + raise RuntimeError('Dependent role name was not specified') |
| 190 | + |
| 191 | + dependent_refs = [] |
| 192 | + for property_ref in dependent[0].xpath('edm:PropertyRef', namespaces=config.namespaces): |
| 193 | + dependent_refs.append(property_ref.get('Name')) |
| 194 | + if len(principal_refs) != len(dependent_refs): |
| 195 | + raise RuntimeError('Number of properties should be equal for the principal {} and the dependent {}' |
| 196 | + .format(principal_name, dependent_name)) |
| 197 | + |
| 198 | + return ReferentialConstraint( |
| 199 | + PrincipalRole(principal_name, principal_refs), DependentRole(dependent_name, dependent_refs)) |
| 200 | + |
| 201 | + |
| 202 | +# pylint: disable=protected-access |
| 203 | +def association_from_etree(association_node, config: Config): |
| 204 | + name = association_node.get('Name') |
| 205 | + association = Association(name) |
| 206 | + |
| 207 | + for end in association_node.xpath('edm:End', namespaces=config.namespaces): |
| 208 | + end_role = EndRole.from_etree(end, config) |
| 209 | + if end_role.entity_type_info is None: |
| 210 | + raise RuntimeError('End type is not specified in the association {}'.format(name)) |
| 211 | + association._end_roles.append(end_role) |
| 212 | + |
| 213 | + if len(association._end_roles) != 2: |
| 214 | + raise RuntimeError('Association {} does not have two end roles'.format(name)) |
| 215 | + |
| 216 | + refer = association_node.xpath('edm:ReferentialConstraint', namespaces=config.namespaces) |
| 217 | + if len(refer) > 1: |
| 218 | + raise RuntimeError('In association {} is defined more than one referential constraint'.format(name)) |
| 219 | + |
| 220 | + if not refer: |
| 221 | + referential_constraint = None |
| 222 | + else: |
| 223 | + referential_constraint = ReferentialConstraint.from_etree(refer[0], config) |
| 224 | + |
| 225 | + association._referential_constraint = referential_constraint |
| 226 | + |
| 227 | + return association |
| 228 | + |
| 229 | + |
| 230 | +def association_set_end_role_from_etree(end_node, config): |
| 231 | + role = end_node.get('Role') |
| 232 | + entity_set = end_node.get('EntitySet') |
| 233 | + |
| 234 | + return AssociationSetEndRole(role, entity_set) |
| 235 | + |
| 236 | + |
| 237 | +def association_set_from_etree(association_set_node, config: Config): |
| 238 | + end_roles = [] |
| 239 | + name = association_set_node.get('Name') |
| 240 | + association = Identifier.parse(association_set_node.get('Association')) |
| 241 | + |
| 242 | + end_roles_list = association_set_node.xpath('edm:End', namespaces=config.namespaces) |
| 243 | + if len(end_roles) > 2: |
| 244 | + raise PyODataModelError('Association {} cannot have more than 2 end roles'.format(name)) |
| 245 | + |
| 246 | + for end_role in end_roles_list: |
| 247 | + end_roles.append(AssociationSetEndRole.from_etree(end_role, config)) |
| 248 | + |
| 249 | + return AssociationSet(name, association.name, association.namespace, end_roles) |
| 250 | + |
| 251 | + |
| 252 | +def external_annotation_from_etree(annotations_node, config): |
| 253 | + target = annotations_node.get('Target') |
| 254 | + |
| 255 | + if annotations_node.get('Qualifier'): |
| 256 | + modlog().warning('Ignoring qualified Annotations of %s', target) |
| 257 | + return |
| 258 | + |
| 259 | + for annotation in annotations_node.xpath('edm:Annotation', namespaces=config.annotation_namespace): |
| 260 | + annot = Annotation.from_etree(target, config, annotation_node=annotation) |
| 261 | + if annot is None: |
| 262 | + continue |
| 263 | + yield annot |
| 264 | + |
| 265 | + |
| 266 | +def annotation_from_etree(target, config, kwargs): |
| 267 | + annotation_node = kwargs['annotation_node'] |
| 268 | + term = annotation_node.get('Term') |
| 269 | + |
| 270 | + if term in config.sap_annotation_value_list: |
| 271 | + return ValueHelper.from_etree(target, config, annotation_node=annotation_node) |
| 272 | + |
| 273 | + modlog().warning('Unsupported Annotation( %s )', term) |
| 274 | + return None |
| 275 | + |
| 276 | + |
| 277 | +def value_helper_from_etree(target, config, kwargs): |
| 278 | + label = None |
| 279 | + collection_path = None |
| 280 | + search_supported = False |
| 281 | + params_node = None |
| 282 | + |
| 283 | + annotation_node = kwargs['annotation_node'] |
| 284 | + for prop_value in annotation_node.xpath('edm:Record/edm:PropertyValue', namespaces=config.annotation_namespace): |
| 285 | + rprop = prop_value.get('Property') |
| 286 | + if rprop == 'Label': |
| 287 | + label = prop_value.get('String') |
| 288 | + elif rprop == 'CollectionPath': |
| 289 | + collection_path = prop_value.get('String') |
| 290 | + elif rprop == 'SearchSupported': |
| 291 | + search_supported = prop_value.get('Bool') |
| 292 | + elif rprop == 'Parameters': |
| 293 | + params_node = prop_value |
| 294 | + |
| 295 | + value_helper = ValueHelper(target, collection_path, label, search_supported) |
| 296 | + |
| 297 | + if params_node is not None: |
| 298 | + for prm in params_node.xpath('edm:Collection/edm:Record', namespaces=config.annotation_namespace): |
| 299 | + param = ValueHelperParameter.from_etree(prm, config) |
| 300 | + param.value_helper = value_helper |
| 301 | + value_helper._parameters.append(param) |
| 302 | + |
| 303 | + return value_helper |
| 304 | + |
| 305 | + |
| 306 | +def value_helper_parameter_from_etree(value_help_parameter_node, config): |
| 307 | + typ = value_help_parameter_node.get('Type') |
| 308 | + direction = config.sap_value_helper_directions[typ] |
| 309 | + local_prop_name = None |
| 310 | + list_prop_name = None |
| 311 | + for pval in value_help_parameter_node.xpath('edm:PropertyValue', namespaces=config.annotation_namespace): |
| 312 | + pv_name = pval.get('Property') |
| 313 | + if pv_name == 'LocalDataProperty': |
| 314 | + local_prop_name = pval.get('PropertyPath') |
| 315 | + elif pv_name == 'ValueListProperty': |
| 316 | + list_prop_name = pval.get('String') |
| 317 | + |
| 318 | + return ValueHelperParameter(direction, local_prop_name, list_prop_name) |
| 319 | + |
| 320 | + |
| 321 | +# pylint: disable=too-many-locals |
| 322 | +def function_import_from_etree(function_import_node, config: Config): |
| 323 | + name = function_import_node.get('Name') |
| 324 | + entity_set = function_import_node.get('EntitySet') |
| 325 | + http_method = metadata_attribute_get(function_import_node, 'HttpMethod') |
| 326 | + |
| 327 | + rt_type = function_import_node.get('ReturnType') |
| 328 | + rt_info = None if rt_type is None else Types.parse_type_name(rt_type) |
| 329 | + print(name, rt_type, rt_info) |
| 330 | + |
| 331 | + parameters = dict() |
| 332 | + for param in function_import_node.xpath('edm:Parameter', namespaces=config.namespaces): |
| 333 | + param_name = param.get('Name') |
| 334 | + param_type_info = Types.parse_type_name(param.get('Type')) |
| 335 | + param_nullable = param.get('Nullable') |
| 336 | + param_max_length = param.get('MaxLength') |
| 337 | + param_precision = param.get('Precision') |
| 338 | + param_scale = param.get('Scale') |
| 339 | + param_mode = param.get('Mode') |
| 340 | + |
| 341 | + parameters[param_name] = FunctionImportParameter(param_name, param_type_info, param_nullable, |
| 342 | + param_max_length, param_precision, param_scale, param_mode) |
| 343 | + |
| 344 | + return FunctionImport(name, rt_info, entity_set, parameters, http_method) |
0 commit comments