Skip to content

Commit 17f4834

Browse files
committed
Add basic layout of design documentation
1 parent f160c93 commit 17f4834

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

deisgn-doc/file-hierarchy.png

133 KB
Loading

deisgn-doc/pyodata-source.md

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
![New file hierarchy in one picture](file-hierarchy.png)
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?

deisgn-doc/pyodata-tests.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Table of content
2+
3+
1. [Structure of tests](#Structure)
4+
2. [Testing build functions](#)
5+
3. [Testing policies](#)

0 commit comments

Comments
 (0)