Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Odata v4 #77

Draft
wants to merge 36 commits into
base: wip-odata-v4
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3515a36
Splits python representation of metadata and its parser
mamiksik Sep 5, 2019
c93cccd
Add separate type repository for each child of ODATAVersion
mamiksik Oct 11, 2019
2b0f622
Add support for OData V4 primitive types
mamiksik Oct 11, 2019
f2b854b
Add implementation of schema for OData V4
mamiksik Oct 11, 2019
aa68a10
Change the implementation of dynamic dispatch function `from_etree`
Oct 14, 2019
03e866b
Fix test_types_repository_separation test
mamiksik Oct 18, 2019
27302d2
Add support for NavigationTypeProperty in V4
mamiksik Oct 18, 2019
c7f8f72
Remove enum type from OData V2
mamiksik Oct 30, 2019
5ab9113
Add support for EntitySet in OData V4
mamiksik Oct 30, 2019
9c47514
Add support for TypeDefinition in OData V4
mamiksik Nov 1, 2019
21c944d
Change implementation of struct type build functions
mamiksik Nov 8, 2019
92a12bc
Add V4 to pyodata cmd interface
mamiksik Nov 16, 2019
37382de
Fix import error in python 3.6
mamiksik Nov 27, 2019
41d26fb
Fix parsing datetime containing timezone information for python 3.6
mamiksik Nov 29, 2019
de69e21
Change default value of precision if non is provided in metadata
mamiksik Nov 30, 2019
25a782d
Add untracked files to the .gitignore
mamiksik Dec 4, 2019
474cd3a
Fix type hinting for ErrorPolicy's children
mamiksik Dec 13, 2019
2d6ccaa
Add permissive parsing for TypeDefinition
mamiksik Dec 13, 2019
961c950
Changes all manually raised exception to be child of PyODataException
mamiksik Dec 19, 2019
9837b3a
Add more comprehensive tests for ODATA V4
mamiksik Dec 20, 2019
75100cb
Change parsing of path values for navigation property bindings
mamiksik Jan 2, 2020
b6d1081
Fix error when printing navigation property without partner value - M…
mamiksik Jan 3, 2020
2e0d451
wip - I need to test the dockerfile on another machine
mamiksik Jan 3, 2020
7fba358
Add type annotations to the majority of variables and functions in Se…
mamiksik Feb 28, 2020
06320bf
V4 service test implementation
mamiksik Apr 10, 2020
f160c93
More types!
mamiksik Apr 10, 2020
17f4834
Add basic layout of design documentation
mamiksik Apr 24, 2020
1d0193d
More documentation
mamiksik Apr 24, 2020
bdd010c
Documentation add annotation and fix typos
mamiksik May 1, 2020
29a71bf
client: beautify version dealing
filak-sap Jun 10, 2020
b3e033b
config: forward type declarations & explicit None
filak-sap Jun 10, 2020
9638dfe
Add optional dependency into extras_require is setup.py.
mamiksik Jun 22, 2020
005854c
Add logging for policy ignore
mamiksik Jun 22, 2020
9ef4cf5
Fix formatting of policy.py
mamiksik Jun 22, 2020
fab1712
Add types aliases in version.py
mamiksik Jun 22, 2020
d9348cd
Add types to builder.py
mamiksik Jun 22, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ venv
dist
.idea
.coverage
.htmlcov/
4 changes: 3 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=locally-disabled
disable=locally-disabled,duplicate-code

# As parts of definitions are the same even for different versions,
# pylint detects them as duplicate code which it is not. Disabling pylint 'duplicate-code' inside module did not work.

[REPORTS]

Expand Down
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ install:
- pip install .
- pip install -r dev-requirements.txt
- pip install -r requirements.txt
- pip install -r optional-requirements.txt
- pip install bandit

# command to run tests
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Client can be created from local metadata - Jakub Filak
- support all standard EDM schema versions - Jakub Filak
- Splits python representation of metadata and metadata parsing - Martin Miksik
- Separate type repositories for individual versions of OData - Martin Miksik
- Support for OData V4 primitive types - Martin Miksik
- Support for navigation property in OData v4 - Martin Miksik
- Support for EntitySet in OData v4 - Martin Miksik
- Support for TypeDefinition in OData v4 - Martin Miksik
- Support for TypeDefinition in OData v4 - Martin Miksik
- Add V4 to pyodata cmd interface - Martin Miksik
- Permissive parsing for TypeDefinition
- Changes all manually raised exception to be child of PyODataException - Martin Miksik
- More comprehensive tests for ODATA V4 - Martin Miksik
- Majority of variables and functions in Service V2 are now type annotated - Martin Miksik

### Changed
- Implementation and naming schema of `from_etree` - Martin Miksik
- Build functions of struct types now handle invalid metadata independently. - Martin Miksik
- Default value of precision if non is provided in metadata - Martin Miksik
- Parsing of path values for navigation property bindings - Martin Miksik

### Fixed
- make sure configured error policies are applied for Annotations referencing
unknown type/member - Martin Miksik
- Race condition in `test_types_repository_separation` - Martin Miksik
- Import error while using python version prior to 3.7 - Martin Miksik
- Parsing datetime containing timezone information for python 3.6 and lower - Martin Miksik
- Type hinting for ErrorPolicy's children - Martin Miksik
- Error when printing navigation property without partner value - Martin Miksik

## [1.3.0]

Expand Down
23 changes: 22 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ PYTHON_MODULE_FILES=$(shell find $(PYTHON_MODULE) -type f -name '*.py')
TESTS_DIR=tests
TESTS_UNIT_DIR=$(TESTS_DIR)
TESTS_UNIT_FILES=$(shell find $(TESTS_UNIT_DIR) -type f -name '*.py')
TESTS_OLINGO_SERVER=$(TESTS_DIR)/olingo_server

PYTHON_BIN=python3

Expand All @@ -26,6 +27,9 @@ COVERAGE_HTML_DIR=.htmlcov
COVERAGE_HTML_ARGS=$(COVERAGE_REPORT_ARGS) -d $(COVERAGE_HTML_DIR)
COVERAGE_REPORT_FILES=$(PYTHON_BINARIES) $(PYTHON_MODULE_FILES)

DOCKER_BIN=docker
DOCKER_NAME=pyodata_olingo

all: check

.PHONY=check
Expand Down Expand Up @@ -56,4 +60,21 @@ doc:

.PHONY=clean
clean:
rm --preserve-root -rf $(COVERAGE_HTML_DIR) .coverage
rm --preserve-root -rf $(COVERAGE_HTML_DIR) .coverage; true
$(DOCKER_BIN) rmi pyodata_olingo; true
$(DOCKER_BIN) rm --force pyodata_olingo; true

.PHONY=olingo
build_olingo:
$(DOCKER_BIN) rmi $(DOCKER_NAME); true
$(DOCKER_BIN) build -t $(DOCKER_NAME) $(TESTS_OLINGO_SERVER)

run_olingo:
$(DOCKER_BIN) run -d -it -p 8888:8080 --name $(DOCKER_NAME) $(DOCKER_NAME):latest

stop_olingo:
$(DOCKER_BIN) stop $(DOCKER_NAME)
$(DOCKER_BIN) rm --force $(DOCKER_NAME)

attach_olingo:
$(DOCKER_BIN) attach $(DOCKER_NAME)
24 changes: 20 additions & 4 deletions bin/pyodata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import sys
from argparse import ArgumentParser

import pyodata
from pyodata.v2.model import PolicyFatal, PolicyWarning, PolicyIgnore, ParserError, Config
from pyodata.policies import PolicyFatal, PolicyWarning, PolicyIgnore, ParserError
from pyodata.config import Config

from pyodata.v2 import ODataV2
from pyodata.v4 import ODataV4

import requests

Expand Down Expand Up @@ -42,8 +46,13 @@ def print_out_metadata_info(args, client):
print(f' + {prop.name}({prop.typ.name})')

for prop in es.entity_type.nav_proprties:
print(f' + {prop.name}({prop.to_role.entity_type_name})')

if client.schema.config.odata_version == ODataV2:
print(f' + {prop.name}({prop.to_role.entity_type_name})')
else:
if prop.partner:
print(f' + {prop.name}({prop.partner.name})')
else:
print(f' + {prop.name}')
for fs in client.schema.function_imports:
print(f'{fs.http_method} {fs.name}')

Expand Down Expand Up @@ -90,6 +99,7 @@ def _parse_args(argv):
help='Specify metadata parser default error handler')
parser.add_argument('--custom-error-policy', action='append', type=str,
help='Specify metadata parser custom error handlers in the form: TARGET=POLICY')
parser.add_argument('--version', default=2, choices=[2, 4], type=int)

parser.set_defaults(func=print_out_metadata_info)

Expand Down Expand Up @@ -145,10 +155,16 @@ def _main(argv):

def get_config():
if config is None:
return Config()
version = ODataV4
if args.version == 2:
version = ODataV2

return Config(odata_version=version)

return config

config = get_config()

if args.default_error_policy:
config = get_config()
config.set_default_error_policy(ERROR_POLICIES[args.default_error_policy]())
Expand Down
131 changes: 131 additions & 0 deletions deisgn-doc/Changes-in-pyodata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@

# Table of content

1. [Code separation into multiple files](#Structure)
2. [Defining OData version in the code](#version-specific-code)
3. [Working with metadata and model](#Model)
3. [Annotations](#Annotations)
4. [GeoJson optional depencency](#GeoJson)

## Code separation into multiple files <a name="Structure"></a>
The codebase is now split into logical units. This is in contrast to the single-file approach in previous releases.
Reasoning behind that is to make code more readable, easier to understand but mainly to allow modularity for different
OData versions.

Root source folder, _pyodata/_, contains files that are to be used in all other parts of the library
(e. g. config.py, exceptions.py). Folder Model contains code for parsing the OData Metadata, whereas folder Service
contains code for consuming the OData Service. Both folders are to be used purely for OData version-independent code.
Version dependent belongs to folders v2, v3, v4, respectively.

![New file hierarchy in one picture](file-hierarchy.png)

## Handling OData version specific code <a name="version-specific-code"></a>
Class Version defines the interface for working with different OData versions. Each definition should be the same
throughout out the runtime, hence all methods are static and children itself can not be instantiated. Most
important are these methods:
- `primitive_types() -> List['Typ']` is a method, which returns a list of supported primitive types in given version
- `build_functions() -> Dict[type, Callable]:` is a methods, which returns a dictionary where, Elements classes are
used as keys and build functions are used as values.
- `annotations() -> Dict['Annotation', Callable]:` is a methods, which returns a dictionary where, Annotations classes
are used as keys and build functions are used as values.

The last two methods are the core change of this release. They allow us to link elements classes with different build
functions in each version of OData.

Note the type of dictionary key for builder functions. It is not a string representation of the class name but is
rather type of the class itself. That helps us avoid magical string in the code.

Also, note that because of this design all elements which are to be used by the end-user are imported here.
Thus, the API for end-user is simplified as he/she should only import code which is directly exposed by this module
(e. g. pyodata.v2.XXXElement...).

```python
class ODataVX(ODATAVersion):
@staticmethod
def build_functions():
return {
...
StructTypeProperty: build_struct_type_property,
NavigationTypeProperty: build_navigation_type_property,
...
}

@staticmethod
def primitive_types() -> List[Typ]:
return [
...
Typ('Null', 'null'),
Typ('Edm.Binary', '', EdmDoubleQuotesEncapsulatedTypTraits()),
Typ('Edm.Boolean', 'false', EdmBooleanTypTraits()),
...
]

@staticmethod
def annotations():
return { Unit: build_unit_annotation }
```


### Version definition location
Class defining specific should be located in the `__init__.py` file in the directory, which encapsulates the rest of
appropriate version-specific code.

## Working with metadata and model <a name="Model"></a>
Code in the model is further separated into logical units. If any version-specific code is to be
added into appropriate folders, it must shadow the file structure declared in the model.

- *elements.py* contains the python representation of EDM elements(e. g. Schema, StructType...)
- *type_taraits.py* contains classes describing conversions between python and JSON/XML representation of data
- *builder.py* contains single class MetadataBuilder, which purpose is to parse the XML using lxml,
check is namespaces are valid and lastly call build Schema and return the result.
- *build_functions.py* contains code which transforms XML code into appropriate python representation. More on that in
the next paragraph.

### Build functions
Build functions receive EDM element as etree nodes and return Python instance of a given element. In the previous release
they were implemented as from_etree methods directly in the element class, but that presented a problem as the elements
could not be reused among different versions of OData as the XML representation can vary widely. All functions are
prefixed with build_ followed by the element class name (e. g. `build_struct_type_property`).

Every function must return the element instance or raise an exception. In a case, that exception is raised and appropriate
policy is set to non-fatal function must return dummy element instance(NullType). One exception to build a function that
do not return element are annotations builders; as annotations are not self-contained elements but rather
descriptors to existing ones.

```python
def build_entity_type(config: Config, type_node, schema=None):
try:
etype = build_element(StructType, config, type_node=type_node, typ=EntityType, schema=schema)

for proprty in type_node.xpath('edm:Key/edm:PropertyRef', namespaces=config.namespaces):
etype._key.append(etype.proprty(proprty.get('Name')))

...

return etype
except (PyODataParserError, AttributeError) as ex:
config.err_policy(ParserError.ENTITY_TYPE).resolve(ex)
return NullType(type_node.get('Name'))
```

### Building an element from metadata
In the file model/elements.py, there is helper function build_element, which makes it easier to build element;
rather than manually checking the OData version and then searching build_functions dictionary, we can pass the class type,
config instance and lastly kwargs(etree node, schema etc...). The function then will call appropriate build function
based on OData version declared in config witch the config and kwargs as arguments and then return the result.
```Python
build_element(EntitySet, config, entity_set_node=entity_set)
```

## Annotations <a name="Annotations"></a>
Annotations are handle bit diferently to the rest of EDM elements. That is due to that, annocation do not represent
standalone elements/instances in resulting Model. Annotations are procesed by build_anotation, build functio expect
_target_(an element to annotate) and _Annotation Term_(Name of the annotatio), build_annotation does not return any value.
Anotation term is searched in annotations dictionary defined in the OData version subclass.

## GeoJson optional depencency <a name="GeoJson"></a>
OData V4 introduced support for multiple standardized geolocation types. To use them GeoJson depencency is required, but
as it is likely that not everyone will use these types the depencency is optional and stored in requirments-optional.txt


// Author note: Should be StrucType removed from the definition of build_functions?
51 changes: 51 additions & 0 deletions deisgn-doc/Changes-in-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Table of content

1. [Tests file structure](#Structure)
2. [Test and classes](#Clases)
3. [Using metadata templates](#Templates)


## Tests file structure <a name="Structure"></a>
The tests are split into multiple files: test_build_functions, test_build_functions_with_policies,
test_elements, test_type_traits. The diference between test_build_functions and test_build_functions_with_policies is
that the later is for testing on invalid metadata.


## Test and classes <a name="Classes"></a>
In previous versions all tests were written as a standalone function, however, due to that, it is hard to orientate in
the code and it makes hard to know which test cases are related and which are not. To avoid that, tests in this release
are bundled together in inappropriate places. Such as when testing build_function(see the example below). Also, bundling
makes it easy to run all related tests at once, without having to run the whole test suit, thus making it faster to debug.

```python
class TestSchema:
def test_types(self, schema):
assert isinstance(schema.complex_type('Location'), ComplexType)
...
assert isinstance(schema.entity_set('People'), EntitySet)

def test_property_type(self, schema):
person = schema.entity_type('Person')
...
assert repr(person.proprty('Weight').typ) == 'Typ(Weight)'
assert repr(person.proprty('AddressInfo').typ) == 'Collection(ComplexType(Location))'
...
```

## Using metadata templates <a name="Templates"></a>
For testing the V4 there are two sets of metadata. `tests/v4/metadata.xml` is filed with test entities, types, sets etc.
while the `tests/v4/metadata.template.xml` is only metadata skeleton. The latter is useful when there is a need to be
sure that any other metadata arent influencing the result, when custom elements are needed for a specific test or when
you are working with invalid metadata.

To use the metadata template the Ninja2 is requited. Ninja2 is a template engine which can load up the template XML and
fill it with provided data. Fixture template_builder is available to all tests. Calling the fixture with an array of EMD
elements will return MetadataBuilder preloaded with your custom data.

```python
faulty_entity = """
<EntityType Name="Restaurant">
<NavigationProperty Name="Location" Type="Position"/>
</EntityType> """
builder, config = template_builder(ODataV4, schema_elements=[faulty_entity])
```
2 changes: 2 additions & 0 deletions deisgn-doc/TOC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [Changes in PyOdata](Changes-in-pyodata.md)
- [Changes in Tests](Changes-in-tests.md)
Binary file added deisgn-doc/file-hierarchy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pytest-cov
codecov
flake8
sphinx
jinja2
2 changes: 1 addition & 1 deletion docs/usage/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The User Guide
versionThe User Guide
--------------

* [Initialization](initialization.rst)
Expand Down
5 changes: 4 additions & 1 deletion docs/usage/initialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ For parser to use your custom configuration, it needs to be passed as an argumen
.. code-block:: python

import pyodata
from pyodata.v2.model import PolicyFatal, PolicyWarning, PolicyIgnore, ParserError, Config
from pyodata.v2 import ODataV2
from pyodata.policies import PolicyFatal, PolicyWarning, PolicyIgnore, ParserError
from pyodata.config import Config
import requests

SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'
Expand All @@ -132,6 +134,7 @@ For parser to use your custom configuration, it needs to be passed as an argumen
}

custom_config = Config(
ODataV2,
xml_namespaces=namespaces,
default_error_policy=PolicyFatal(),
custom_error_policies={
Expand Down
1 change: 1 addition & 0 deletions optional-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
geojson
Loading