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

ComplexType support #18

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -5,3 +5,4 @@
dist
build/
doc/_build/
venv/
59 changes: 40 additions & 19 deletions odata/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,29 @@ def _parse_function(self, xmlq, function_element, schema_name):
function['return_type'] = type_name
return function

def _parse_property(self, xmlq, entity_pks, property_):

p_name = property_.attrib['Name']
p_type = property_.attrib['Type']

is_collection, p_type = self._type_is_collection(p_type)
is_computed_value = False

for annotation in xmlq(property_, 'edm:Annotation'):
annotation_term = annotation.attrib.get('Term', '')
annotation_bool = annotation.attrib.get('Bool') == 'true'
if annotation_term == self._annotation_term_computed:
is_computed_value = annotation_bool

return {
'name': p_name,
'type': p_type,
'is_primary_key': p_name in entity_pks,
'is_collection': is_collection,
'is_computed_value': is_computed_value,
}


def _parse_entity(self, xmlq, entity_element, schema_name, schema_alias):
entity_name = entity_element.attrib['Name']

Expand All @@ -356,25 +379,7 @@ def _parse_entity(self, xmlq, entity_element, schema_name, schema_alias):
entity_pks[pk_property_name] = 0

for entity_property in xmlq(entity_element, 'edm:Property'):
p_name = entity_property.attrib['Name']
p_type = entity_property.attrib['Type']

is_collection, p_type = self._type_is_collection(p_type)
is_computed_value = False

for annotation in xmlq(entity_property, 'edm:Annotation'):
annotation_term = annotation.attrib.get('Term', '')
annotation_bool = annotation.attrib.get('Bool') == 'true'
if annotation_term == self._annotation_term_computed:
is_computed_value = annotation_bool

entity['properties'].append({
'name': p_name,
'type': p_type,
'is_primary_key': p_name in entity_pks,
'is_collection': is_collection,
'is_computed_value': is_computed_value,
})
entity['properties'].append(self._parse_property(xmlq, entity_pks, entity_property))

for nav_property in xmlq(entity_element, 'edm:NavigationProperty'):
p_name = nav_property.attrib['Name']
Expand Down Expand Up @@ -409,6 +414,18 @@ def _parse_enumtype(self, xmlq, enumtype_element, schema_name):
})
return enum

def _parse_complextype(self, xmlq, complextype_element, schema_name):
complex_name = complextype_element.attrib['Name']
complex_ = {
'name': complex_name,
'fully_qualified_name': '.'.join([schema_name, complex_name]),
'properties': []
}
for complex_property in xmlq(complextype_element, 'edm:Property'):
complex_['properties'].append(self._parse_property(xmlq, {}, complex_property))
return complex_


def parse_document(self, doc):
schemas = []
container_sets = {}
Expand Down Expand Up @@ -442,6 +459,10 @@ def xmlq(node, xpath):
entity = self._parse_entity(xmlq, entity_type, schema_name, schema_alias)
schema_dict['entities'].append(entity)

for complex_type in xmlq(schema, 'edm:ComplexType'):
complex_ = self._parse_complextype(xmlq, complex_type, schema_name)
schema_dict['complex_types'].append(complex_)

schemas.append(schema_dict)

for schema in xmlq(doc, 'edmx:DataServices/edm:Schema'):
Expand Down
9 changes: 9 additions & 0 deletions odata/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from odata.property import StringProperty, IntegerProperty, DecimalProperty, \
NavigationProperty, DatetimeProperty
from odata.enumtype import EnumType, EnumTypeProperty
from odata.complextype import ComplexType, ComplexTypeProperty

url = 'http://unittest.server.local/odata/'
Service = ODataService(url)
Expand Down Expand Up @@ -49,6 +50,13 @@ class ColorSelection(EnumType):
Green = 3


class Dimensions(ComplexType):
properties = dict(Height=DecimalProperty,
Weight=StringProperty,
Length=DecimalProperty,
)


class Product(Service.Entity):
__odata_type__ = 'ODataTest.Objects.Product'
__odata_collection__ = 'ProductParts'
Expand All @@ -59,6 +67,7 @@ class Product(Service.Entity):
price = DecimalProperty('Price')
color_selection = EnumTypeProperty('ColorSelection',
enum_class=ColorSelection)
dimensions = ComplexTypeProperty('Dimensions', type_class=Dimensions)

DemoAction = DemoAction()
DemoCollectionAction = DemoCollectionAction()
Expand Down
5 changes: 5 additions & 0 deletions odata/tests/demo_metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
<Property Name="ManufacturerID" Type="Edm.Int32" Nullable="false"/>
<Property Name="SalesAmount" Type="Edm.Decimal"/>
</EntityType>
<ComplexType Name="Dimensions">
<Property Name="Height" Nullable="false" Type="Edm.Decimal" />
<Property Name="Weight" Nullable="false" Type="Edm.String" />
<Property Name="Length" Nullable="false" Type="Edm.Decimal" />
</ComplexType>
<EnumType Name="ColorSelection">
<Member Name="Black" Value="0"/>
<Member Name="Red" Value="1"/>
Expand Down
58 changes: 58 additions & 0 deletions odata/tests/test_complextype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-

import json
from decimal import Decimal
from unittest import TestCase

import requests
import responses

from odata.tests import Service, Product, Dimensions


class TestComplextype(TestCase):

def test_insert_value(self):
def request_callback(request):
content = json.loads(request.body)
content['ProductID'] = 1
self.assertIn('Dimensions', content)
self.assertEqual(content.get('Dimensions'), {'Height': 10.1})
headers = {}
return requests.codes.ok, headers, json.dumps(content)

with responses.RequestsMock() as rsps:
rsps.add_callback(rsps.POST,
Product.__odata_url__(),
callback=request_callback,
content_type='application/json')

new_product = Product()
new_product.name = 'Test Product'
new_product.dimensions = Dimensions({'Height': 10.1})
Service.save(new_product)

def test_read_value(self):
expected_height = Decimal('11')
expected_weight = u'110 kg.'
expected_length = Decimal('8')
test_product_values = dict(
ProductID=1,
ProductName='Test Product',
Category='',
ColorSelection='Red',
Dimensions={'Height': float(expected_height),
'Weight': expected_weight,
'Length': float(expected_length)},
Price=0.0,
)
with responses.RequestsMock() as rsps:
rsps.add(rsps.GET, Product.__odata_url__(),
content_type='application/json',
json=dict(value=[test_product_values]))

product = Service.query(Product).get(1)
self.assertIsInstance(product.dimensions, Dimensions)
assert product.dimensions['Height'] == expected_height
assert product.dimensions['Weight'] == expected_weight
assert product.dimensions['Length'] == expected_length