odatadol#
Read an OData v4 entity set as a Mapping, with the standard library only.
>>> from odatadol import ODataReader, BasicAuth
>>> store = ODataReader('https://example.com/odata', 'Products',
... key='ProductID', credentials=BasicAuth('me', 'pw'))
>>> len(store); store[42]; list(store)
Under the facade are plain functions, usable on their own:
odata_get(): the one (GET) request, through afetchseam.entity_count(),entity_records():$countand paged records, with$select/$filter/$orderby/$toppassed through.entity_columns(): a$selecttoo wide for one URL, read as column groups joined on a key and checked to be one moment (UnstableReadotherwise).entity_metadata():$metadataas{name: {type, nullable, annotations}}.
Read-only by construction: no function takes an HTTP verb.
Functions
|
|
|
|
|
The default transport: one |
|
|
|
The URL of an entity set, optionally with a path suffix and query options. |
|
A property name as |
|
How many records the entity set holds now (or match |
|
Records of an entity set, lazily, page by page, with OData annotations removed. |
|
Every record with every |
|
The declared properties of one entity set's type, in declaration order. |
|
The properties of |
Classes
|
The records of |
|
An account and its password, for HTTP Basic. |
|
An OAuth2 or similar bearer token. |
Exceptions
|
The service answered with an error. |
|
Reads of one entity set disagreed about which records exist, or their versions. |
- class odatadol.BasicAuth(account, password)[source]#
Bases:
objectAn account and its password, for HTTP Basic. The password is never shown.
>>> BasicAuth('reader', 'secret') BasicAuth(account='reader') >>> BasicAuth('reader', 'secret').header() 'Basic cmVhZGVyOnNlY3JldA=='
- class odatadol.BearerToken(token)[source]#
Bases:
objectAn OAuth2 or similar bearer token. Never shown.
- exception odatadol.ODataError(message, *, status=None, code='')[source]#
Bases:
RuntimeErrorThe service answered with an error. Carries the HTTP status and the OData code.
- class odatadol.ODataReader(service, entity, *, key, select=None, filter=None, version=None, credentials=None, fetch=None, max_url=2000, page_size=1000, parse_float=None, literal=None)[source]#
Bases:
MappingThe records of
entityatservice, as{key value: record}.- Parameters:
service (
str) – The OData service root URL.entity (
str) – The entity set name.key (
str) – The property records are addressed, ordered and joined by. It must be unique and never empty within the reader’sfilter.select (
Iterable[str] |None) – The properties a record holds.Nonefetches all of them; a wide list is split into column groups undermax_urlfor bulk reads.filter (
str|None) – A$filterscoping the reader: keys outside it are not in it.version (
str|None) – A property rewritten on every edit, checked per key across column groups so a bulk read is one moment (seeodatadol.read.entity_columns()).credentials (
Union[None,_HasHeader,Callable[[],Any]]) –None, aBasicAuthorBearerToken, or a callable returning one.fetch (
Callable[[str,Mapping[str,str]],tuple[int,bytes]] |None) – The transport,(url, headers) -> (status, bytes).literal (
Callable[[Any],str] |None) – Writes a key as a$filterliteral, forstore[k]andk in store. Defaults toodata_literal(); anEdm.Guidkey read back as text by iteration needsliteral=str. RaisesTypeErrorfor a key it cannot write, which the reader reports as missing.parse_float (
Callable[[str],Any] |None) – Applied to fractional JSON numbers (decimal.Decimalkeeps every digit).
- exception odatadol.UnstableRead(message, *, status=None, code='')[source]#
Bases:
ODataErrorReads of one entity set disagreed about which records exist, or their versions.
- odatadol.entity_columns(service, entity, *, select, key, version=None, filter=None, max_url=2000, page_size=1000, credentials=None, fetch=None, parse_float=None)[source]#
Every record with every
selectfield, however many fields that is, as one moment.Splits
selectinto groups whose URL fitsmax_url, reads each group inkeyorder, and joins them onkey. Records come back in key order, each with exactly theselectfields, inselectorder.**A join of reads is only one moment if every read saw the same records in the same state.** So
version(a property the server rewrites on every edit, such as a modification timestamp) is read in every group and must agree per key; without it, an edit landing between two groups yields a record that never existed: the old name beside the new price. And there are always at least two reads: a single group is followed by a narrowkey/versionread, because$skippaging over a delete before the cursor and an insert after it skips a live record while the count still balances.- Raises:
UnstableRead – If reads disagree about which keys exist, their order, or their versions. The entity set changed during the read; retrying is safe.
ODataError – If
keyis empty or repeats: the join would silently merge different records into one.ValueError – If one field cannot fit under
max_urlat all.
- Return type:
- odatadol.entity_count(service, entity, *, filter=None, credentials=None, fetch=None)[source]#
How many records the entity set holds now (or match
filter). One request.- Return type:
- odatadol.entity_metadata(service, entity, *, credentials=None, fetch=None)[source]#
The declared properties of one entity set’s type, in declaration order.
entitymay name the entity set (resolved through the container) or the type.
- odatadol.entity_records(service, entity, *, select=None, filter=None, orderby=None, top=None, page_size=1000, credentials=None, fetch=None, parse_float=None)[source]#
Records of an entity set, lazily, page by page, with OData annotations removed.
Follows
@odata.nextLinkwhen the server sends one, and otherwise pages with$top/$skipuntil an empty page (a short page may be a server capping its pages without a nextLink, so it is not taken as the end).orderbyshould name a unique key: paging without a stable order can skip or repeat a record that moves between pages.- Parameters:
select (
Iterable[str] |None) – Properties to fetch (quoted as needed).Nonefetches all of them.filter (
str|None) – A$filterexpression, sent as written.orderby (
str|None) – A$orderbyexpression, sent as written (quote spaced names yourself withquote_name()).top (
int|None) – The most records to yield in all;Nonefor every one.page_size (
int) – Records asked for per request.parse_float (
Callable[[str],Any] |None) – Called on every JSON number with a fraction;decimal.Decimalkeeps every digit. Defaults tofloat.
- Return type:
- odatadol.entity_url(service, entity, *, suffix='', query=None)[source]#
The URL of an entity set, optionally with a path suffix and query options.
None-valued options are dropped, and:is not percent-encoded.>>> entity_url('https://example.com/odata/', 'Products', suffix='/$count', ... query={'$filter': 'ModifiedAt gt 2026-01-01T00:00:00Z', '$top': None}) 'https://example.com/odata/Products/$count?$filter=ModifiedAt%20gt%202026-01-01T00:00:00Z'
- Return type:
- odatadol.odata_get(url, *, credentials=None, accept='application/json', fetch=None)[source]#
GETone OData URL and return the body. The only request odatadol makes.- Raises:
ODataError – On any non-2xx status, with the service’s error code and message. The message names the URL (without userinfo) and never the
Authorizationheader.- Return type:
- odatadol.odata_literal(value)[source]#
valuewritten as an OData$filterliteral.>>> odata_literal("O'Brien"), odata_literal(42), odata_literal(True) ("'O''Brien'", '42', 'true') >>> odata_literal(float('nan')), odata_literal(Decimal('1E+2')) ('NaN', '100') >>> odata_literal(uuid.UUID(int=1)), odata_literal(datetime.date(2026, 1, 2)) ('00000000-0000-0000-0000-000000000001', '2026-01-02')
A
stris always a quoted string literal: pass auuid.UUIDfor anEdm.Guidand adatetimeobject for a date or time.- Return type:
- odatadol.parse_entity_metadata(body, entity)[source]#
The properties of
entityin a CSDL document, inherited ones first.entitymay be an entity set, a qualified type (Namespace.NameorAlias.Name) or, when only one schema declares it, a type’s short name. Annotations are merged from inline<Annotation>and out-of-line<Annotations Target=...>elements.
- odatadol.quote_name(name)[source]#
A property name as
$select/$orderbyaccept it.>>> quote_name('Status'), quote_name('Unit Price'), quote_name('Category/Name') ('Status', '"Unit Price"', 'Category/Name')
A bare
Unit Priceis a syntax error at the space; servers that allow such names (FileMaker’s OData, for one) take them double-quoted. A standard OData name is never quoted.- Raises:
ValueError – For a name holding a
", which no quoting can carry.- Return type:
- odatadol.redact(url)[source]#
urlwithout anyuser:password@it carries.>>> redact('https://user:pw@example.com:8443/odata/Products?$top=1') 'https://example.com:8443/odata/Products?$top=1' >>> redact('https://u:p@[::1]:8443/x') 'https://[::1]:8443/x'
- Return type:
- odatadol.urllib_fetch(url, headers, *, timeout=120.0)[source]#
The default transport: one
GETwithurllib. An HTTP error is returned, not raised, soodata_get()reads every status the same way.A redirect to another origin is followed without the
Authorizationheader. Usefunctools.partial()to changetimeout.- Raises:
ODataError – If no HTTP answer came back (DNS, refused connection, timeout, a malformed URL), naming the URL without any userinfo it carries.
- Return type:
Modules
An entity type's declared properties, read from the service's CSDL |
|
Reading an entity set: its URL, its count, its records page by page, and wide reads. |
|
A read-only |
|
A scripted OData service for tests: no network, no password. |
|
The one request odatadol makes: an authenticated |