odatadol.read#

Reading an entity set: its URL, its count, its records page by page, and wide reads.

The functions take the service root and the entity set name first, then keyword-only options, and share three seams: credentials=, fetch= and, for anything returning records, parse_float=.

Server quirks met in practice, each with a default that copes:

  • $count is asked for with Accept: */*: some servers refuse a bare text/plain.

  • A name that is not an OData identifier (Unit Price) is double-quoted in $select/$orderby (quote_name()).

  • : travels bare in the query string: a percent-encoded : inside a $filter timestamp is a syntax error on some servers.

  • JSON is parsed with strict=False: raw control characters (a bare CR line break) inside strings are kept verbatim instead of failing the page.

  • A front end may refuse URLs past ~2k characters, so entity_columns() splits a wide $select into column groups under max_url and joins them on a key.

Module Attributes

DEFAULT_PAGE_SIZE

small enough that one slow page is not a timeout, large enough that tens of thousands of records are a few dozen requests.

MAX_URL_CHARS

The longest URL entity_columns() sends.

Functions

entity_url(service, entity, *[, suffix, query])

The URL of an entity set, optionally with a path suffix and query options.

quote_name(name)

A property name as $select/$orderby accept it.

entity_count(service, entity, *[, filter, ...])

How many records the entity set holds now (or match filter).

entity_records(service, entity, *[, select, ...])

Records of an entity set, lazily, page by page, with OData annotations removed.

entity_columns(service, entity, *, select, key)

Every record with every select field, however many fields that is, as one moment.

Exceptions

UnstableRead(message, *[, status, code])

Reads of one entity set disagreed about which records exist, or their versions.

odatadol.read.DEFAULT_PAGE_SIZE = 1000#

small enough that one slow page is not a timeout, large enough that tens of thousands of records are a few dozen requests.

Type:

Records per request

odatadol.read.MAX_URL_CHARS = 2000#

The longest URL entity_columns() sends. IIS’s default query-string ceiling is 2,048 characters, and past it IIS answers its own 404 as if the entity did not exist.

exception odatadol.read.UnstableRead(message, *, status=None, code='')[source]#

Bases: ODataError

Reads of one entity set disagreed about which records exist, or their versions.

odatadol.read.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 select field, however many fields that is, as one moment.

Splits select into groups whose URL fits max_url, reads each group in key order, and joins them on key. Records come back in key order, each with exactly the select fields, in select order.

**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 narrow key/version read, because $skip paging 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 key is empty or repeats: the join would silently merge different records into one.

  • ValueError – If one field cannot fit under max_url at all.

Return type:

list[dict[str, Any]]

odatadol.read.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:

int

odatadol.read.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.nextLink when the server sends one, and otherwise pages with $top/$skip until an empty page (a short page may be a server capping its pages without a nextLink, so it is not taken as the end). orderby should 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). None fetches all of them.

  • filter (str | None) – A $filter expression, sent as written.

  • orderby (str | None) – A $orderby expression, sent as written (quote spaced names yourself with quote_name()).

  • top (int | None) – The most records to yield in all; None for 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.Decimal keeps every digit. Defaults to float.

Return type:

Iterator[dict[str, Any]]

odatadol.read.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:

str

odatadol.read.quote_name(name)[source]#

A property name as $select/$orderby accept it.

>>> quote_name('Status'), quote_name('Unit Price'), quote_name('Category/Name')
('Status', '"Unit Price"', 'Category/Name')

A bare Unit Price is 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:

str