Skip to content
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
35 changes: 35 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
; indicate this is the root of the project
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4

[*.py]
max_line_length = 88

[{*.hcl,*.nomad}]
indent_size = 2

[{*.tfvars,*.tf}]
indent_size = 2

[{*.yml,*.yaml}]
indent_size = 2

[{.babelrc,.eslintrc,jest.config,.stylelintrc,bowerrc,*.json,*.jsb3,*.jsb2}]
indent_size = 2

[Makefile]
indent_size = 4
indent_style = tab

[Jenkinsfile]
indent_size = 2

[*.md]
max_line_length = 80
34 changes: 34 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
default_language_version:
python: python3.8

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
exclude: assets/*
- id: debug-statements
- id: no-commit-to-branch
- id: mixed-line-ending
args: [--fix=lf]
- id: detect-private-key
- id: detect-aws-credentials
args: [--allow-missing-credentials]
- id: check-merge-conflict
- id: requirements-txt-fixer

- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.19.0
hooks:
- id: markdownlint
language_version: system

- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
pass_filenames: false
args: [".", "--exclude", "migrations/*",]
42 changes: 21 additions & 21 deletions odata/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@

.. code-block:: python

>>> from odata import ODataService
>>> Service = ODataService(url, reflect_entities=True)
>>> Product = Service.entities['Product']
>> from odata import ODataService
>> Service = ODataService(url, reflect_entities=True)
>> Product = Service.entities['Product']

>>> prod = Service.query(Product).get(1234)
>>> prod.GetAvailabilityDate()
>> prod = Service.query(Product).get(1234)
>> prod.GetAvailabilityDate()
datetime.datetime(2018, 6, 1, 12, 0, 0)

>>> import datetime
>>> GetExampleDecimal = Service.functions['GetExampleDecimal']
>>> GetExampleDecimal(Date=datetime.datetime.now())
>> import datetime
>> GetExampleDecimal = Service.functions['GetExampleDecimal']
>> GetExampleDecimal(Date=datetime.datetime.now())
Decimal('34.0')


Expand Down Expand Up @@ -49,9 +49,9 @@ class _GetExampleDecimal(Service.Function):

.. code-block:: python

>>> import datetime
>>> # calls GET http://service/GetExampleDecimal(Date=2017-01-01T12:00:00Z)
>>> GetExampleDecimal(Date=datetime.datetime.now())
>>import datetime
>># calls GET http://service/GetExampleDecimal(Date=2017-01-01T12:00:00Z)
>>GetExampleDecimal(Date=datetime.datetime.now())
Decimal('34.0')


Expand Down Expand Up @@ -92,19 +92,19 @@ class Product(Service.Entity):

.. code-block:: python

>>> # collection bound Action. calls POST http://service/Product/ODataService.RemoveAllReservations
>>> Product.RemoveAllReservations()
>> # collection bound Action. calls POST http://service/Product/ODataService.RemoveAllReservations
>> Product.RemoveAllReservations()
True

>>> # if the Action is instance bound, call the Action from the Product instance instead
>>> from decimal import Decimal
>>> prod = Service.query(Product).get(1234)
>>> # calls POST http://service/Product(1234)/ODataService.ReserveAmount
>>> prod.ReserveAmount(Amount=Decimal('5.0'))
>> # if the Action is instance bound, call the Action from the Product instance instead
>> from decimal import Decimal
>> prod = Service.query(Product).get(1234)
>> # calls POST http://service/Product(1234)/ODataService.ReserveAmount
>> prod.ReserveAmount(Amount=Decimal('5.0'))
True

>>> # calls GET http://service/Product(1234)/ODataService.GetAvailabilityDate()
>>> prod.GetAvailabilityDate()
>> # calls GET http://service/Product(1234)/ODataService.GetAvailabilityDate()
>> prod.GetAvailabilityDate()
datetime.datetime(2018, 6, 1, 12, 0, 0)


Expand Down Expand Up @@ -246,7 +246,7 @@ def _callable(self, connection, url, query, **kwargs):
query_options = query._get_options()

response_data = self._execute_http(connection, url, query_options, kwargs)
response_data = (response_data or {}).get('value')
response_data = (response_data or {}).get('value', response_data)

simple_types_values = self.__odata_service__.metadata.property_types.values()

Expand Down
24 changes: 18 additions & 6 deletions odata/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ODataConnection(object):
base_headers = {
'Accept': 'application/json',
'OData-Version': '4.0',
'User-Agent': 'python-odata {0}'.format(version),
'User-Agent': 'python-odata {0}'.format(__version__),
}
timeout = 90

Expand Down Expand Up @@ -88,18 +88,21 @@ def _handle_odata_error(self, response):
ie = odata_error['innererror']
detailed_message = ie.get('message') or detailed_message

msg = ' | '.join([status_code, code, message, detailed_message])
msg = ' | '.join([str(status_code), str(code), str(message), str(detailed_message)])
err = ODataError(msg)
err.status_code = status_code
err.code = code
err.message = message
err.detailed_message = detailed_message
raise err

def execute_get(self, url, params=None):
def execute_get(self, url, params=None, extra_headers=None):
headers = {}
headers.update(self.base_headers)

if extra_headers:
headers.update(extra_headers)

self.log.info(u'GET {0}'.format(url))
if params:
self.log.info(u'Query: {0}'.format(params))
Expand All @@ -116,12 +119,15 @@ def execute_get(self, url, params=None):
msg = u'Unsupported response Content-Type: {0}'.format(response_ct)
raise ODataError(msg)

def execute_post(self, url, data, params=None):
def execute_post(self, url, data, params=None, extra_headers=None):
headers = {
'Content-Type': 'application/json',
}
headers.update(self.base_headers)

if extra_headers:
headers.update(extra_headers)

data = json.dumps(data)

self.log.info(u'POST {0}'.format(url))
Expand All @@ -136,12 +142,15 @@ def execute_post(self, url, data, params=None):
return response.json()
# no exceptions here, POSTing to Actions may not return data

def execute_patch(self, url, data):
def execute_patch(self, url, data, extra_headers=None):
headers = {
'Content-Type': 'application/json',
}
headers.update(self.base_headers)

if extra_headers:
headers.update(extra_headers)

data = json.dumps(data)

self.log.info(u'PATCH {0}'.format(url))
Expand All @@ -150,10 +159,13 @@ def execute_patch(self, url, data):
response = self._do_patch(url, data=data, headers=headers)
self._handle_odata_error(response)

def execute_delete(self, url):
def execute_delete(self, url, extra_headers=None):
headers = {}
headers.update(self.base_headers)

if extra_headers:
headers.update(extra_headers)

self.log.info(u'DELETE {0}'.format(url))

response = self._do_delete(url, headers=headers)
Expand Down
15 changes: 8 additions & 7 deletions odata/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@

class Context:

def __init__(self, session=None, auth=None):
def __init__(self, session=None, auth=None, base_url=None):
self.log = logging.getLogger('odata.context')
self.connection = ODataConnection(session=session, auth=auth)
self.base_url = base_url

def query(self, entitycls):
q = Query(entitycls, connection=self.connection)
return q
return Query(entitycls, connection=self.connection, base_url=self.base_url)

def call(self, action_or_function, **parameters):
"""
Expand Down Expand Up @@ -50,7 +50,7 @@ def delete(self, entity):
entity.__odata__.persisted = False
self.log.info(u'Success')

def save(self, entity, force_refresh=True):
def save(self, entity, force_refresh=True, extra_headers=None):
"""
Creates a POST or PATCH call to the service. If the entity already has
a primary key, an update is called. Otherwise the entity is inserted
Expand All @@ -59,11 +59,12 @@ def save(self, entity, force_refresh=True):
:param entity: Model instance to insert or update
:type entity: EntityBase
:param force_refresh: Read full entity data again from service after PATCH call
:param extra_headers: Add custom headers on patch, post (Example:B1S-ReplaceCollectionsOnPatch=true)
:raises ODataConnectionError: Invalid data or serverside error. Server returned an HTTP error code
"""

if self.is_entity_saved(entity):
self._update_existing(entity, force_refresh=force_refresh)
self._update_existing(entity, force_refresh=force_refresh, extra_headers=extra_headers)
else:
self._insert_new(entity)

Expand Down Expand Up @@ -95,7 +96,7 @@ def _insert_new(self, entity):

self.log.info(u'Success')

def _update_existing(self, entity, force_refresh=True):
def _update_existing(self, entity, force_refresh=True, extra_headers=None):
"""
Creates a PATCH call to the service, sending only the modified values

Expand All @@ -116,7 +117,7 @@ def _update_existing(self, entity, force_refresh=True):

url = es.instance_url

saved_data = self.connection.execute_patch(url, patch_data)
saved_data = self.connection.execute_patch(url, patch_data, extra_headers)
es.reset()

if saved_data is None and force_refresh:
Expand Down
20 changes: 20 additions & 0 deletions odata/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ def __new__(cls, *args, **kwargs):

return i

@classmethod
def create(cls, raw_data):
i = super(EntityBase, cls).__new__(cls)
i.__odata__ = es = EntityState(i)

for prop_name, prop in es.navigation_properties:
if prop.name in raw_data:
expanded_data = raw_data.pop(prop.name)
if prop.is_collection:
es.nav_cache[prop.name] = dict(collection=prop.instances_from_data(expanded_data))
else:
es.nav_cache[prop.name] = dict(single=prop.instances_from_data(expanded_data))

for prop_name, prop in es.properties:
i.__odata__[prop.name] = raw_data.get(prop.name)

i.__odata__.persisted = False

return i

def __repr__(self):
clsname = self.__class__.__name__
display_string = self.__odata__.id or clsname
Expand Down
2 changes: 1 addition & 1 deletion odata/enumtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, name, enum_class=EnumType):
self.enum_class = enum_class

def serialize(self, value):
return value.name
return value.name if hasattr(value, "name") else value

def deserialize(self, value):
return self.enum_class[value]
22 changes: 16 additions & 6 deletions odata/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class MetaData(object):
_annotation_term_computed = 'Org.OData.Core.V1.Computed'

def __init__(self, service):
self.url = service.url + '$metadata/'
self.url = service.url + '$metadata'
self.connection = service.default_context.connection
self.service = service

Expand Down Expand Up @@ -249,10 +249,19 @@ def get_entity_or_prop_from_type(typename):
self.log.info('Loaded {0} entity sets, total {1} types'.format(len(sets), len(all_types)))
return base_class, sets, all_types

def load_document(self):
def load_document(self, raw_mode=False):
self.log.info('Loading metadata document: {0}'.format(self.url))
response = self.connection._do_get(self.url)
return ET.fromstring(response.content)

if self.service.schema_path is None:
response = self.connection._do_get(self.url)
response = response.content
else:
with open(self.service.schema_path) as fp:
response = fp.read()

if raw_mode:
return response.decode("utf-8")
return ET.fromstring(response)

def _parse_action(self, xmlq, action_element, schema_name):
action = {
Expand All @@ -269,9 +278,10 @@ def _parse_action(self, xmlq, action_element, schema_name):
# bound actions are named SchemaNamespace.ActionName
action['fully_qualified_name'] = '.'.join([schema_name, action['name']])

for parameter_element in xmlq(action_element, 'edm:Parameter'):
for i, parameter_element in enumerate(xmlq(action_element, 'edm:Parameter')):
parameter_name = parameter_element.attrib['Name']
if action['is_bound'] and parameter_name == 'bindingParameter':

if i == 0 and action['is_bound']:
action['is_bound_to'] = parameter_element.attrib['Type']
continue

Expand Down
6 changes: 3 additions & 3 deletions odata/navproperty.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

.. code-block:: python

>>> order = Service.query(Order).first()
>>> order.Shipper
>> order = Service.query(Order).first()
>> order.Shipper
<Entity(Shipper:3)>
>>> order.Shipper.CompanyName
>> order.Shipper.CompanyName
'Federal Shipping'

When creating new instances, relationships can be assigned via navigation
Expand Down
Loading