odatadol
Read an OData v4 entity set as a Python Mapping, with the standard library only (urllib, json, xml.etree): zero runtime dependencies.
pip install odatadol
from odatadol import ODataReader, BasicAuth
store = ODataReader(
'https://example.com/odata/v4/Shop', 'Products',
key='ProductID', credentials=BasicAuth('reader', password),
)
len(store) # one $count request
store['P0042'] # one filtered request
list(store) # the key column only, in key order
dict(store.items()) # every record, read in bulk
It is read-only by construction: every request is a GET, and no function takes an HTTP verb.
The functions under the facade
ODataReader is a thin layer over plain functions, each usable on its own. They take the service root and the entity set name, then keyword-only options.
| function | does |
|---|---|
entity_count(service, entity, filter=) |
$count, sent with Accept: */* |
entity_records(service, entity, select=, filter=, orderby=, top=, page_size=) |
records, lazily, paged by $top/$skip or @odata.nextLink, annotations dropped; a read ends on an empty page, so a server that silently caps its pages loses nothing |
entity_columns(service, entity, select=, key=, version=, filter=, max_url=) |
a $select too wide for one URL, read as column groups and joined on key |
entity_metadata(service, entity) |
$metadata as {name: {'type': 'Edm.…', 'nullable': bool, 'annotations': {...}}} |
odata_get(url) |
the one request everything goes through |
from decimal import Decimal
from odatadol import entity_records, entity_metadata
entity_metadata(service, 'Products')['Price']
# {'type': 'Edm.Decimal', 'nullable': True}
newest = next(entity_records(
service, 'Products', select=['ProductID', 'ModifiedAt'],
filter='ModifiedAt ne null', orderby='ModifiedAt desc', top=1,
parse_float=Decimal, # keep every digit of a fractional number
))
Seams
Every function takes the same keyword arguments for what a host may need to swap:
fetch=(url, headers) -> (status, body bytes): the transport. Defaults tourllib_fetch(usefunctools.partial(urllib_fetch, timeout=...)to change its timeout). Tests passodatadol.testing.FakeService, a scripted server that records every URL it was asked for.credentials=:None(anonymous),BasicAuth(account, password),BearerToken(token), anything with aheader() -> strmethod, or a zero-argument callable returning one of those, so a keychain or file lookup happens only when a request is made. Neither the password nor theAuthorizationheader ever appears in an error, and credentials never follow a redirect or a@odata.nextLinkto another origin (a cross-origin nextLink is refused). A server that would page forever (ignoring$skip, or repeating its nextLink) is stopped with anODataError.max_url=(default 2000) andpage_size=(default 1000).parse_float=: applied to every fractional JSON number;decimal.Decimalkeeps every digit (defaultfloat).literal=(onODataReader): writes a key as a$filterliteral forstore[k]andk in store; defaults toodata_literal(str, numbers, bool,uuid.UUID,datetimeobjects). For anEdm.Guidkey that iteration returns as text, passliteral=str.
Wide reads are one moment, or they raise
A front end such as IIS refuses URLs past about 2,048 characters, answering its own 404 as if the entity did not exist. A $select of a hundred or more fields is past that, so entity_columns (and ODataReader with a wide select=) splits it into groups that fit max_url, reads each in key order, and joins them on the key.
A join of separate reads is only a true record if every read saw the same records in the same state, so:
version=names a property the server rewrites on every edit (a modification timestamp). It is read in every group and must agree per key; otherwise an edit landing between two reads would yield a record that never existed.- With a single group, a narrow key/version read follows it, because
$skippaging over a delete before the cursor and an insert after it skips a live record while the count still balances. - An empty or repeated key is refused (
ODataError): joining on it would merge different records. - Reads that disagree raise
UnstableRead(anODataError), saying how many records were new, gone or edited. The entity set changed during the read; retrying is safe.
Server quirks it copes with
Each was met against a real server and has a test:
$countwithAccept: text/plainrefused; odatadol sends*/*.- Property names with spaces must be double-quoted in
$select/$orderby(quote_name), as FileMaker's OData requires. - A percent-encoded
:inside a$filtertimestamp is a syntax error;:travels bare, as RFC 3986 allows. - Raw control characters (a bare CR) inside JSON strings; parsed with
json.loads(strict=False)and kept verbatim.
Writes
Not here, and not behind a flag on the reader. If writing (POST/PATCH) is ever added it will be a separate, opt-in class with its own transport seam (method, body and response headers, for ETags), so code holding a reader cannot reach a write by passing the wrong argument, and the reader's fetch=(url, headers) -> (status, bytes) stays as it is. A fetch may do its own retries (on 429 or 503, say); the reader needs nothing more from it.
Metadata
Release files for odatadol 0.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| odatadol-0.0.2.tar.gz | 30.1 kB | Without provenance | Details for odatadol-0.0.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| odatadol-0.0.2-py3-none-any.whl | Python 3 | none | any | Details for odatadol-0.0.2-py3-none-any.whl |
Total release size: 57.1 kB
Release files / odatadol-0.0.2.tar.gz
| Download URL | odatadol-0.0.2.tar.gz |
|---|---|
| Size | 30.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b323f7fa1cb393cb21deeb6b0c74c4576cef7479c38a278d388f1659cf9e23f0
|
|
BLAKE2b-256 checksum How to use checksums |
6186269ad0b3eb7f9b5baa377c9b723ffefc32a1af1e0bbff623b3c32fd798bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / odatadol-0.0.2-py3-none-any.whl
| Download URL | odatadol-0.0.2-py3-none-any.whl |
|---|---|
| Size | 27.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
47c51a1566c81028854db447798a13eca757e3b6fefb8f214bb0aeb10f17ae47
|
|
BLAKE2b-256 checksum How to use checksums |
619f1d00a39fd5a770cca3063bc2792af6675566dba367729073f9836e0e454e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for odatadol 0.0.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.