|
| 1 | + |
| 2 | +# Table of content |
| 3 | + |
| 4 | +1. [Code separation into multiple files](#Structure) |
| 5 | +2. [Defining OData version in the code](#version-specific-code) |
| 6 | +3. [Working with metadata and model](#Model) |
| 7 | + |
| 8 | +## Code separation into multiple files <a name="Structure"></a> |
| 9 | +The codebase is now split into logical units. This is in contrast to the single-file approach in previous releases. |
| 10 | +Reasoning behind that is to make code more readable, easier to understand but mainly to allow modularity for different |
| 11 | +OData versions. |
| 12 | + |
| 13 | +Root source folder, _pyodata/_, contains files that are to be used in all other parts of the library |
| 14 | +(e. g. config.py, exceptions.py). Folder Model contains code for parsing the OData Metadata, whereas folder Service |
| 15 | +contains code for consuming the OData Service. Both folders are to be used purely for OData version-independent code. |
| 16 | +Version dependent belongs to folders v2, v3, v4, respectively. |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +## Handling OData version specific code <a name="version-specific-code"></a> |
| 21 | +Class Version defines the interface for working with different OData versions. Each definition should be the same |
| 22 | +throughout out the runtime, hence all methods are static and children itself can not be instantiated. Most |
| 23 | +important are these methods: |
| 24 | +- `primitive_types() -> List['Typ']` is a method, which returns a list of supported primitive types in given version |
| 25 | +- `build_functions() -> Dict[type, Callable]:` is a methods, which returns a dictionary where, Elements classes are |
| 26 | +used as keys and build functions are used as values. |
| 27 | +- `annotations() -> Dict['Annotation', Callable]:` is a methods, which returns a dictionary where, Annotations classes |
| 28 | +are used as keys and build functions are used as values. |
| 29 | + |
| 30 | +The last two methods are the core change of this release. They allow us to link elements classes with different build |
| 31 | +functions in each version of OData. |
| 32 | + |
| 33 | +Note the type of dictionary key for builder functions. It is not a string representation of the class name but is |
| 34 | +rather type of the class itself. That helps us avoid magical string in the code. |
| 35 | + |
| 36 | +Also, note that because of this design all elements which are to be used by the end-user are imported here. |
| 37 | +Thus, the API for end-user is simplified as he/she should only import code which is directly exposed by this module |
| 38 | +(e. g. pyodata.v2.XXXElement...). |
| 39 | + |
| 40 | +```python |
| 41 | +class ODataVX(ODATAVersion): |
| 42 | + @staticmethod |
| 43 | + def build_functions(): |
| 44 | + return { |
| 45 | + ... |
| 46 | + StructTypeProperty: build_struct_type_property, |
| 47 | + NavigationTypeProperty: build_navigation_type_property, |
| 48 | + ... |
| 49 | + } |
| 50 | + |
| 51 | + @staticmethod |
| 52 | + def primitive_types() -> List[Typ]: |
| 53 | + return [ |
| 54 | + ... |
| 55 | + Typ('Null', 'null'), |
| 56 | + Typ('Edm.Binary', '', EdmDoubleQuotesEncapsulatedTypTraits()), |
| 57 | + Typ('Edm.Boolean', 'false', EdmBooleanTypTraits()), |
| 58 | + ... |
| 59 | + ] |
| 60 | + |
| 61 | + @staticmethod |
| 62 | + def annotations(): |
| 63 | + return { Unit: build_unit_annotation } |
| 64 | +``` |
| 65 | + |
| 66 | + |
| 67 | +### Version definition location |
| 68 | +Class defining specific should be located in the `__init__.py` file in the directory, which encapsulates the rest of |
| 69 | +appropriate version-specific code. |
| 70 | + |
| 71 | +## Working with metadata and model <a name="Model"></a> |
| 72 | +Code in the model is further separated into logical units. If any version-specific code is to be |
| 73 | +added into appropriate folders, it must shadow the file structure declared in the model. |
| 74 | + |
| 75 | +- *elements.py* contains the python representation of EDM elements(e. g. Schema, StructType...) |
| 76 | +- *type_taraits.py* contains classes describing conversions between python and JSON/XML representation of data |
| 77 | +- *builder.py* contains single class MetadataBuilder, which purpose is to parse the XML using lxml, |
| 78 | +check is namespaces are valid and lastly call build Schema and return the result. |
| 79 | +- *build_functions.py* contains code which transforms XML code into appropriate python representation. More on that in |
| 80 | +the next paragraph. |
| 81 | + |
| 82 | +### Build functions |
| 83 | +Build functions receive EDM element as etree nodes and return Python instance of a given element. In the previous release |
| 84 | +they were implemented as from_etree methods directly in the element class, but that presented a problem as the elements |
| 85 | +could not be reused among different versions of OData as the XML representation can vary widely. All functions are |
| 86 | +prefixed with build_ followed by the element class name (e. g. `build_struct_type_property`). |
| 87 | + |
| 88 | +Every function must return the element instance or raise an exception. In a case, that exception is raised and appropriate |
| 89 | +policy is set to non-fatal function must return dummy element instance(NullType). One exception to build a function that |
| 90 | +do not return element are annotations builders; as annotations are not self-contained elements but rather |
| 91 | +descriptors to existing ones. |
| 92 | + |
| 93 | +```python |
| 94 | +def build_entity_type(config: Config, type_node, schema=None): |
| 95 | + try: |
| 96 | + etype = build_element(StructType, config, type_node=type_node, typ=EntityType, schema=schema) |
| 97 | + |
| 98 | + for proprty in type_node.xpath('edm:Key/edm:PropertyRef', namespaces=config.namespaces): |
| 99 | + etype._key.append(etype.proprty(proprty.get('Name'))) |
| 100 | + |
| 101 | + ... |
| 102 | + |
| 103 | + return etype |
| 104 | + except (PyODataParserError, AttributeError) as ex: |
| 105 | + config.err_policy(ParserError.ENTITY_TYPE).resolve(ex) |
| 106 | + return NullType(type_node.get('Name')) |
| 107 | +``` |
| 108 | + |
| 109 | +### Building an element from metadata |
| 110 | +In the file model/elements.py, there is helper function build_element, which makes it easier to build element; |
| 111 | +rather than manually checking the OData version and then searching build_functions dictionary, we can pass the class type, |
| 112 | +config instance and lastly kwargs(etree node, schema etc...). The function then will call appropriate build function |
| 113 | +based on OData version declared in config witch the config and kwargs as arguments and then return the result. |
| 114 | +```Python |
| 115 | +build_element(EntitySet, config, entity_set_node=entity_set) |
| 116 | +``` |
| 117 | + |
| 118 | + |
| 119 | +// Author note: Should be StrucType removed from the definition of build_functions? |
0 commit comments