Welcome to python-odata’s documentation!¶
What is this?¶
A simple library to consume an OData 4.0 endpoint. For example, an endpoint created with Microsoft’s WebAPI 2.2. This library exposes the OData entities in a manner that mimics some of the modern ORM libraries for easy usage.
Features:
- Supports OData version 4.0 with JSON format
- Supports creating, reading, updating and deleting data
- Supports simple queries on EntitySets
- Powered by the excellent Requests library
Not currently supported:
- ATOM format
- ComplexTypes
- Streams
- Most of the more intricate querying options
Project source code and issue tracker: GitHub
Code example¶
Connecting to a service and building entity classes from the service’s metadata:
from odata import ODataService
url = 'http://services.odata.org/V4/Northwind/Northwind.svc/'
Service = ODataService(url, reflect_entities=True)
Fetch the Order entity from reflected classes:
Order = Service.entities['Order']
Query some orders:
query = Service.query(Order)
query = query.filter(Order.Name.startswith('Demo'))
query = query.order_by(Order.ShippedDate.desc())
for order in query:
print(order.Name)
Topics¶
Connecting to an endpoint¶
A service object represents a single endpoint which has at least one EntitySet.
It is the cornerstone of this library. If you have multiple endpoints to
connect to, create multiple instances of ODataService
. All Entity
objects are each bound to exactly one Service and cannot be used across
multiple services.
Optionally, the Service object can connect to the endpoint and request its
metadata document. This document will then be used to build Entity objects
corresponding to each EntitySet provided by the endpoint. This operation
requires a working network connection to the endpoint. Creating an instance with
reflect_entities=False
will not cause any network activity.
Authentication¶
auth
and session
keyword arguments to ODataService
are
passed as-is to Requests calls, so most of the same guides can be used.
HTTP Basic authentication:
>>> from requests.auth import HTTPBasicAuth
>>> my_auth = HTTPBasicAuth('username', 'password')
>>> Service = ODataService('url', auth=my_auth)
NTLM Auth (for services like Microsoft Dynamics 2016):
>>> import requests
>>> from requests_ntlm import HttpNtlmAuth
>>> my_session = requests.Session()
>>> my_session.auth = HttpNtlmAuth('domain\username', 'password')
>>> my_session.get('basic url') # should return 200 OK
>>> Service = ODataService('url', session=my_session)
API¶
-
class
odata.service.
ODataService
(url, base=None, reflect_entities=False, session=None, auth=None)¶ Parameters: - url – Endpoint address. Must be an address that can be appended with
$metadata
- base – Custom base class to use for entities
- reflect_entities – Create a request to the service for its metadata, and create entity classes automatically
- session – Custom Requests session to use for communication with the endpoint
- auth – Custom Requests auth object to use for credentials
Raises: ODataConnectionError – Fetching metadata failed. Server returned an HTTP error code
-
Action
= None¶ A baseclass for this service’s Actions
-
Base
= None¶ Entity base class. Either a custom one given in init or a generated one. Can be used to define entities
-
Function
= None¶ A baseclass for this service’s Functions
-
actions
= None¶ A dictionary containing all the automatically created unbound Action callables. Empty if the service is created with
reflect_entities=False
-
create_context
(auth=None, session=None)¶ Create new context to use for session-like usage
Parameters: - auth – Custom Requests auth object to use for credentials
- session – Custom Requests session to use for communication with the endpoint
Returns: Context instance
Return type: Context
-
delete
(entity)¶ Creates a DELETE call to the service, deleting the entity
Raises: ODataConnectionError – Delete not allowed or a serverside error. Server returned an HTTP error code
-
describe
(entity)¶ Print a debug screen of an entity instance
Parameters: entity – Entity instance to describe
-
entities
= None¶ A dictionary containing all the automatically created Entity classes. Empty if the service is created with
reflect_entities=False
-
functions
= None¶ A dictionary containing all the automatically created unbound Function callables. Empty if the service is created with
reflect_entities=False
-
is_entity_saved
(entity)¶ Returns boolean indicating entity’s status
-
query
(entitycls)¶ Start a new query for given entity class
Parameters: entitycls – Entity to query Returns: Query object
-
save
(entity, force_refresh=True)¶ 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 as new. Updating an entity will only send the changed values
Parameters: - entity – Model instance to insert or update
- force_refresh – Read full entity data again from service after PATCH call
Raises: ODataConnectionError – Invalid data or serverside error. Server returned an HTTP error code
-
types
= None¶ A dictionary containing all types (EntityType, EnumType) created during reflection. Empty if the service is created with
reflect_entities=False
- url – Endpoint address. Must be an address that can be appended with
Querying¶
Entities can be queried from a service with a Query object:
query = Service.query(Order)
Adding filters and other options always creates a new Query object with the given directives:
>>> query.filter(Order.Name == 'Foo')
<Query for <Order>>
This makes object chaining possible:
>>> first_order = query.filter(...).filter(...).order_by(...).first()
The resulting objects can be fetched with first()
,
one()
, all()
, get()
or
just iterating the Query object itself. Network is not accessed until one of
these ways is triggered.
Navigation properties can be loaded in the same request with
expand()
:
>>> query.expand(Order.Shipper, Order.Customer)
>>> order = query.first()
API¶
-
class
odata.query.
Query
(entitycls, connection=None, options=None)¶ This class should not be instantiated directly, but from a
ODataService
object.-
all
()¶ Returns a list of all Entity instances that match the current query options. Iterates through all results with multiple requests fired if necessary, exhausting the query
Returns: A list of Entity instances
-
expand
(*values)¶ Set
$expand
query parameterParameters: values – Entity.Property
instanceReturns: Query instance
-
filter
(value)¶ Set
$filter
query parameter. Can be called multiple times. Multiplefilter()
calls are concatenated with ‘and’Parameters: value – Property comparison. For example, Entity.Property == 2
Returns: Query instance
-
first
()¶ Return the first Entity instance that matches current query
Returns: Entity instance or None
-
get
(*pk, **composite_keys)¶ Return a Entity with the given primary key
Parameters: - pk – Primary key value
- composite_keys – Primary key values for Entities with composite keys
Returns: Entity instance or None
-
limit
(value)¶ Set
$top
query parameterParameters: value – Number of records to return Returns: Query instance
-
offset
(value)¶ Set
$skip
query parameterParameters: value – Number of records to skip Returns: Query instance
-
one
()¶ Return only one resulting Entity
Returns: Entity instance
Raises: - NoResultsFound – Zero results returned
- MultipleResultsFound – Multiple results returned
-
order_by
(*values)¶ Set
$orderby
query parameterParameters: values – One of more of Property.asc() or Property.desc() Returns: Query instance
-
raw
(query_params)¶ Execute a query with custom parameters. Allows queries that
Query
does not support otherwise. Results are not converted to Entity objects>>> query = Service.query(MyEntity) >>> query.raw({'$filter': 'EntityId eq 123456'}) [{'EntityId': 123456, 'Name': 'Example entity'}]
Parameters: query_params (dict) – A dictionary of query params containing $filter, $orderby, etc. Returns: Query result
-
select
(*values)¶ Set properties to fetch instead of full Entity objects
Returns: Raw JSON values for given properties
-
Entity classes¶
The data model can be created manually if you wish to use separate property names from the data keys, or define custom methods for your objects.
Custom Entity class¶
Each Service instance has their separate base classes for Entities, Actions and Functions. Use them to define your model:
class Product(Service.Entity):
__odata_type__ = 'ProductDataService.Objects.Product'
__odata_collection__ = 'Products'
name = StringProperty('ProductName')
quantity_in_storage = IntegerProperty('QuantityInStorage')
Note that the type (EntityType) and collection (EntitySet) must be defined. These are used in querying and saving data.
Custom base class¶
Define a base. These properties and methods are shared by all objects in the endpoint.
from odata.entity import declarative_base
from odata.property import IntegerProperty, StringProperty, DatetimeProperty
class MyBase(declaractive_base()):
id = IntegerProperty('Id', primary_key=True)
created_date = DatetimeProperty('Created')
modified_date = DatetimeProperty('Modified')
def did_somebody_touch_this(self):
return self.created_date != self.modified_date
Define a model:
class Product(MyBase):
__odata_type__ = 'ProductDataService.Objects.Product'
__odata_collection__ = 'Products'
name = StringProperty('ProductName')
quantity_in_storage = IntegerProperty('QuantityInStorage')
def is_product_available(self):
return self.quantity_in_storage > 0
Use the base to init ODataService
:
Service = ODataService(url, base=MyBase)
Unlike reflection, this does not require any network connections. Now you can use the Product class to create new objects or query existing ones:
query = Service.query(Product)
query = query.filter(Product.name.startswith('Kettle'))
for product in query:
print(product.name, product.is_product_available())
Actions, Functions¶
Actions and Functions are set up automatically when using reflection. Unbound
callables are collected in actions
and
functions
. Bound callables are assigned
to the Entity classes they are bound to.
>>> from odata import ODataService
>>> Service = ODataService(url, reflect_entities=True)
>>> Product = Service.entities['Product']
>>> 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())
Decimal('34.0')
Unbound action/functions¶
Similar to Entities, Functions are subclassed from the Service baseclasses
Action
and
Function
:
Service = ODataService(url)
class _GetExampleDecimal(Service.Function):
name = 'GetExampleDecimal'
parameters = dict(
Date=DatetimeProperty,
)
return_type = DecimalProperty
GetExampleDecimal = _GetExampleDecimal()
Usage:
>>> import datetime
>>> # calls GET http://service/GetExampleDecimal(Date=2017-01-01T12:00:00Z)
>>> GetExampleDecimal(Date=datetime.datetime.now())
Decimal('34.0')
Bound action/function¶
Bound functions are otherwise the same, but the instanced object should be set under the Entity it belongs to:
class _GetAvailabilityDate(Service.Function):
name = 'ODataService.GetAvailabilityDate'
parameters = dict()
return_type = DatetimeProperty
class _RemoveAllReservations(Service.Action):
name = 'ODataService.RemoveAllReservations'
parameters = dict()
return_type = BooleanProperty
class _ReserveAmount(Service.Action):
name = 'ODataService.ReserveAmount'
parameters = dict(
Amount=DecimalProperty,
)
return_type = BooleanProperty
class Product(Service.Entity):
Id = IntegerProperty('Id', primary_key=True)
Name = StringProperty('Name')
GetAvailabilityDate = GetAvailabilityDate()
RemoveAllReservations = _RemoveAllReservations()
ReserveAmount = _ReserveAmount()
Usage:
>>> # 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'))
True
>>> # calls GET http://service/Product(1234)/ODataService.GetAvailabilityDate()
>>> prod.GetAvailabilityDate()
datetime.datetime(2018, 6, 1, 12, 0, 0)
API¶
-
class
odata.action.
Action
¶ Baseclass for all Actions. Should not be used directly, use the subclass
Action
instead.-
name
¶ Action’s fully qualified name. Bound Actions are prefixed with their schema’s name (
SchemaName.ActionName
). Required when subclassing
-
parameters
¶ Dictionary. Defines all the keyword arguments and their types the Action can accept. Key names must match with ones accepted by the server. Required when subclassing
-
return_type
¶ Reference to either Entity class or Property class. Defines the return value’s type for this Action. Required when subclassing
-
return_type_collection
¶ Reference to either Entity class or Property class. Defines the return value’s type for this Action when retuning multiple values. Required when subclassing
-
bound_to_collection
¶ Action is bound to Entity collection. If True, Action is not available in Entity class instances. If False, Action is not available in Entity class objects. Defaults to false
-
-
class
odata.action.
ActionCallable
(actionbase_instance, url, errmsg=None)¶ A helper class for ActionBase, representing a callable
-
class
odata.action.
Function
¶ Baseclass for all Functions. Should not be used directly, use the subclass
Function
instead.-
name
¶ Function’s fully qualified name. Function Actions are prefixed with their schema’s name (
SchemaName.FunctionName
). Required when subclassing
-
parameters
¶ Dictionary. Defines all the keyword arguments and their types the Function can accept. Key names must match with ones accepted by the server. Required when subclassing
-
return_type
¶ Reference to either Entity class or Property class. Defines the return value’s type for this Function. Required when subclassing
-
return_type_collection
¶ Reference to either Entity class or Property class. Defines the return value’s type for this Function when retuning multiple values. Required when subclassing
-
bound_to_collection
¶ Function is bound to Entity collection. If True, Function is not available in Entity class instances. If False, Function is not available in Entity class objects. Defaults to false
-
Entity properties¶
Entities are a collection of Properties of different types. These classes implement the default set of types used in endpoints.
At entity class level, these properties are used in querying entities:
>>> import datetime
>>> Order.ShippedDate > datetime.datetime.now()
'ShippedDate gt 2016-02-19T12:02:04.956226'
>>> Service.query(Order).filter(Order.OrderID == 1234)
Once the entity is instanced, the properties act as data getters and setters:
>>> order = Order()
>>> order.ShippedDate = datetime.datetime.now()
>>> Service.save(order)
Setting a new value to a property marks the property as dirty, and will be
sent to the endpoint when save()
is
called.
This behavior is similar to SQLAlchemy’s ORM.
Creating new property types¶
All properties must subclass PropertyBase
, and implement the
serialization methods. You can then use the created Property class in your
custom defined entities. Replacing the default types is not supported.
-
class
odata.property.
PropertyBase
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ A base class for all properties.
Parameters: - name – Name of the property in the endpoint
- primary_key – This property is a primary key
- is_collection – This property contains multiple values
-
deserialize
(value)¶ Called when deserializing the value from JSON to Python. Implement this method when creating a new Property class
Parameters: value – Value received in JSON Returns: Value that will be passed to Python
-
escape_value
(value)¶ Called when escaping the property value for usage in Query string. Implement this method when creating a new Property class
Parameters: value – Value of this property Returns: Escaped value that can be used in Query string
-
serialize
(value)¶ Called when serializing the value to JSON. Implement this method when creating a new Property class
Parameters: value – Value given in Python code Returns: Value that will be used in JSON
Types¶
-
class
odata.property.
BooleanProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a boolean value
-
class
odata.property.
DatetimeProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a datetime object. JSON does not support date objects natively so dates are transmitted as ISO-8601 formatted strings
-
class
odata.property.
DecimalProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a decimal value. JSON does not support this directly, so the value will be transmitted as a float
-
class
odata.property.
FloatProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a float value
-
class
odata.property.
IntegerProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a plain old integer
-
class
odata.property.
StringProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a unicode string
-
class
odata.property.
UUIDProperty
(name, primary_key=False, is_collection=False, is_computed_value=False)¶ Property that stores a UUID (also known as GUID) value. JSON does not support this directly, so the value will be transmitted as a string. Unlike
StringProperty
, it does not escape quotes as query filters do not use quotes for UUID
Exceptions¶
Error classes used by this library.
-
exception
odata.exceptions.
MultipleResultsFound
(*args, **kwargs)¶ Raised when
one()
is called but multiple results returned
-
exception
odata.exceptions.
NoResultsFound
(*args, **kwargs)¶ Raised when
one()
is called but zero results returned
-
exception
odata.exceptions.
ODataConnectionError
(*args, **kwargs)¶ Raised when the endpoint responds with an HTTP error code
-
exception
odata.exceptions.
ODataError
(*args, **kwargs)¶ Base class for python-odata errors. All other errors are subclassed from this. Raising any other exception class is a bug and should be reported
-
code
= None¶ Error code supplied by server
-
detailed_message
= None¶ Detailed error message supplied by server
-
message
= None¶ Error message supplied by server
-
status_code
= None¶ HTTP status
-
-
exception
odata.exceptions.
ODataReflectionError
(*args, **kwargs)¶ Raised when MetaData is unable to reflect types