> built 2026-10-29 03:42 UTC from 708a714 (main) · odatadol 0.0.1. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# odatadol

Read an OData v4 entity set as a Python `Mapping`, with the standard library only (`urllib`, `json`, `xml.etree`): zero runtime dependencies.

```default
pip install odatadol
```

```python
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                                                                                                                                  |
```python
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 to `urllib_fetch` (use `functools.partial(urllib_fetch, timeout=...)` to change its timeout). Tests pass `odatadol.testing.FakeService`, a scripted server that records every URL it was asked for.
- `credentials=`: `None` (anonymous), `BasicAuth(account, password)`, `BearerToken(token)`, anything with a `header() -> str` method, 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 the `Authorization` header ever appears in an error, and credentials never follow a redirect or a `@odata.nextLink` to another origin (a cross-origin nextLink is refused). A server that would page forever (ignoring `$skip`, or repeating its nextLink) is stopped with an `ODataError`.
- `max_url=` (default 2000) and `page_size=` (default 1000).
- `parse_float=`: applied to every fractional JSON number; `decimal.Decimal` keeps every digit (default `float`).
- `literal=` (on `ODataReader`): writes a key as a `$filter` literal for `store[k]` and `k in store`; defaults to `odata_literal` (str, numbers, bool, `uuid.UUID`, `datetime` objects). For an `Edm.Guid` key that iteration returns as text, pass `literal=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 `$skip` paging 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` (an `ODataError`), 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:

- `$count` with `Accept: text/plain` refused; 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 `$filter` timestamp 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.

<p class="epythet-aggregates">This documentation as a single file: <a href="odatadol.md">odatadol.md</a> (Markdown, for agents).</p>


# _autosummary/odatadol.html.md

# odatadol

Read an OData v4 entity set as a Mapping, with the standard library only.

```pycon
>>> 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()`](_autosummary/odatadol.html.md#odatadol.odata_get): the one (`GET`) request, through a `fetch` seam.
- [`entity_count()`](_autosummary/odatadol.html.md#odatadol.entity_count), [`entity_records()`](_autosummary/odatadol.html.md#odatadol.entity_records): `$count` and paged records, with
  `$select`/`$filter`/`$orderby`/`$top` passed through.
- [`entity_columns()`](_autosummary/odatadol.html.md#odatadol.entity_columns): a `$select` too wide for one URL, read as column groups
  joined on a key and checked to be one moment ([`UnstableRead`](_autosummary/odatadol.html.md#odatadol.UnstableRead) otherwise).
- [`entity_metadata()`](_autosummary/odatadol.html.md#odatadol.entity_metadata): `$metadata` as `{name: {type, nullable, annotations}}`.

Read-only by construction: no function takes an HTTP verb.

### Functions

| [`odata_literal`](_autosummary/odatadol.html.md#odatadol.odata_literal)(value)                               | `value` written as an OData `$filter` literal.                                                                 |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| [`odata_get`](_autosummary/odatadol.html.md#odatadol.odata_get)(url, \*[, credentials, accept, fetch])   | `GET` one OData URL and return the body.                                                                       |
| [`urllib_fetch`](_autosummary/odatadol.html.md#odatadol.urllib_fetch)(url, headers, \*[, timeout])          | The default transport: one `GET` with [`urllib`](https://docs.python.org/3/library/urllib.html#module-urllib). |
| [`redact`](_autosummary/odatadol.html.md#odatadol.redact)(url)                                        | `url` without any `user:password@` it carries.                                                                 |
| [`entity_url`](_autosummary/odatadol.html.md#odatadol.entity_url)(service, entity, \*[, suffix, query])   | The URL of an entity set, optionally with a path suffix and query options.                                     |
| [`quote_name`](_autosummary/odatadol.html.md#odatadol.quote_name)(name)                                   | A property name as `$select`/`$orderby` accept it.                                                             |
| [`entity_count`](_autosummary/odatadol.html.md#odatadol.entity_count)(service, entity, \*[, filter, ...])   | How many records the entity set holds now (or match `filter`).                                                 |
| [`entity_records`](_autosummary/odatadol.html.md#odatadol.entity_records)(service, entity, \*[, select, ...]) | Records of an entity set, lazily, page by page, with OData annotations removed.                                |
| [`entity_columns`](_autosummary/odatadol.html.md#odatadol.entity_columns)(service, entity, \*, select, key)   | Every record with every `select` field, however many fields that is, as one moment.                            |
| [`entity_metadata`](_autosummary/odatadol.html.md#odatadol.entity_metadata)(service, entity, \*[, ...])        | The declared properties of one entity set's type, in declaration order.                                        |
| [`parse_entity_metadata`](_autosummary/odatadol.html.md#odatadol.parse_entity_metadata)(body, entity)                | The properties of `entity` in a CSDL document, inherited ones first.                                           |

### Classes

| [`ODataReader`](_autosummary/odatadol.html.md#odatadol.ODataReader)(service, entity, \*, key[, ...])   | The records of `entity` at `service`, as `{key value: record}`.   |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`BasicAuth`](_autosummary/odatadol.html.md#odatadol.BasicAuth)(account, password)                   | An account and its password, for HTTP Basic.                      |
| [`BearerToken`](_autosummary/odatadol.html.md#odatadol.BearerToken)(token)                             | An OAuth2 or similar bearer token.                                |

### Exceptions

| [`ODataError`](_autosummary/odatadol.html.md#odatadol.ODataError)(message, \*[, status, code])   | The service answered with an error.                                             |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`UnstableRead`](_autosummary/odatadol.html.md#odatadol.UnstableRead)(message, \*[, status, code]) | Reads of one entity set disagreed about which records exist, or their versions. |

### *class* odatadol.BasicAuth(account, password)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An account and its password, for HTTP Basic. The password is never shown.

```pycon
>>> BasicAuth('reader', 'secret')
BasicAuth(account='reader')
>>> BasicAuth('reader', 'secret').header()
'Basic cmVhZGVyOnNlY3JldA=='
```

### *class* odatadol.BearerToken(token)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An OAuth2 or similar bearer token. Never shown.

### *exception* odatadol.ODataError(message, , status=None, code='')

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The 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)

Bases: [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)

The records of `entity` at `service`, as `{key value: record}`.

* **Parameters:**
  * **service** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The OData service root URL.
  * **entity** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The entity set name.
  * **key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The property records are addressed, ordered and joined by. It must be
    unique and never empty within the reader’s `filter`.
  * **select** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The properties a record holds. `None` fetches all of them; a wide
    list is split into column groups under `max_url` for bulk reads.
  * **filter** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `$filter` scoping the reader: keys outside it are not in it.
  * **version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A property rewritten on every edit, checked per key across column
    groups so a bulk read is one moment (see
    [`odatadol.read.entity_columns()`](_autosummary/odatadol.read.html.md#odatadol.read.entity_columns)).
  * **credentials** ([`Union`](https://docs.python.org/3/library/typing.html#typing.Union)[[`None`](https://docs.python.org/3/builtins/constants.html#None), `_HasHeader`, [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – `None`, a [`BasicAuth`](_autosummary/odatadol.transport.html.md#odatadol.transport.BasicAuth) or
    [`BearerToken`](_autosummary/odatadol.transport.html.md#odatadol.transport.BearerToken), or a callable returning one.
  * **fetch** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The transport, `(url, headers) -> (status, bytes)`.
  * **literal** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Writes a key as a `$filter` literal, for `store[k]` and
    `k in store`. Defaults to [`odata_literal()`](_autosummary/odatadol.html.md#odatadol.odata_literal); an `Edm.Guid` key read
    back as text by iteration needs `literal=str`. Raises `TypeError` for a
    key it cannot write, which the reader reports as missing.
  * **parse_float** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Applied to fractional JSON numbers (`decimal.Decimal` keeps
    every digit).

#### items()

Every `(key, record)`, read in bulk.

* **Return type:**
  [`ItemsView`](https://docs.python.org/3/library/collections.abc.html#collections.abc.ItemsView)

#### values()

Every record, read in bulk.

* **Return type:**
  [`ValuesView`](https://docs.python.org/3/library/collections.abc.html#collections.abc.ValuesView)

### *exception* odatadol.UnstableRead(message, , status=None, code='')

Bases: [`ODataError`](_autosummary/odatadol.transport.html.md#odatadol.transport.ODataError)

Reads 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)

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**](_autosummary/odatadol.html.md#odatadol.UnstableRead) – If reads disagree about which keys exist, their order, or their
        versions. The entity set changed during the read; retrying is safe.
  * [**ODataError**](_autosummary/odatadol.html.md#odatadol.ODataError) – If `key` is empty or repeats: the join would silently merge
        different records into one.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If one field cannot fit under `max_url` at all.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.entity_count(service, entity, , filter=None, credentials=None, fetch=None)

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

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### odatadol.entity_metadata(service, entity, , credentials=None, fetch=None)

The declared properties of one entity set’s type, in declaration order.

`entity` may name the entity set (resolved through the container) or the type.

* **Raises:**
  [**ODataError**](_autosummary/odatadol.html.md#odatadol.ODataError) – If the service declares no such entity, naming the ones it has.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.entity_records(service, entity, , select=None, filter=None, orderby=None, top=None, page_size=1000, credentials=None, fetch=None, parse_float=None)

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`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Properties to fetch (quoted as needed). `None` fetches all of them.
  * **filter** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `$filter` expression, sent as written.
  * **orderby** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `$orderby` expression, sent as written (quote spaced names
    yourself with [`quote_name()`](_autosummary/odatadol.html.md#odatadol.quote_name)).
  * **top** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The most records to yield in all; `None` for every one.
  * **page_size** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Records asked for per request.
  * **parse_float** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Called on every JSON number with a fraction; `decimal.Decimal`
    keeps every digit. Defaults to `float`.
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.entity_url(service, entity, , suffix='', query=None)

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

`None`-valued options are dropped, and `:` is not percent-encoded.

```pycon
>>> 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`](https://docs.python.org/3/builtins/stdtypes.html#str)

### odatadol.odata_get(url, , credentials=None, accept='application/json', fetch=None)

`GET` one OData URL and return the body. The only request odatadol makes.

* **Raises:**
  [**ODataError**](_autosummary/odatadol.html.md#odatadol.ODataError) – On any non-2xx status, with the service’s error code and message.
      The message names the URL (without userinfo) and never the
      `Authorization` header.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### odatadol.odata_literal(value)

`value` written as an OData `$filter` literal.

```pycon
>>> 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 `str` is always a quoted string literal: pass a [`uuid.UUID`](https://docs.python.org/3/library/uuid.html#uuid.UUID) for an
`Edm.Guid` and a [`datetime`](https://docs.python.org/3/library/datetime.html#module-datetime) object for a date or time.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### odatadol.parse_entity_metadata(body, entity)

The properties of `entity` in a CSDL document, inherited ones first.

`entity` may be an entity set, a qualified type (`Namespace.Name` or
`Alias.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.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.quote_name(name)

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

```pycon
>>> 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**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – For a name holding a `"`, which no quoting can carry.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### odatadol.redact(url)

`url` without any `user:password@` it carries.

```pycon
>>> 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:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### odatadol.urllib_fetch(url, headers, , timeout=120.0)

The default transport: one `GET` with [`urllib`](https://docs.python.org/3/library/urllib.html#module-urllib). An HTTP error is returned,
not raised, so [`odata_get()`](_autosummary/odatadol.html.md#odatadol.odata_get) reads every status the same way.

A redirect to another origin is followed without the `Authorization` header.
Use [`functools.partial()`](https://docs.python.org/3/library/functools.html#functools.partial) to change `timeout`.

* **Raises:**
  [**ODataError**](_autosummary/odatadol.html.md#odatadol.ODataError) – If no HTTP answer came back (DNS, refused connection, timeout, a
      malformed URL), naming the URL without any userinfo it carries.
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

### Modules

| [`metadata`](_autosummary/odatadol.metadata.html.md#module-odatadol.metadata)   | An entity type's declared properties, read from the service's CSDL `$metadata`.      |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`read`](_autosummary/odatadol.read.html.md#module-odatadol.read)           | Reading an entity set: its URL, its count, its records page by page, and wide reads. |
| [`store`](_autosummary/odatadol.store.html.md#module-odatadol.store)         | A read-only `Mapping` over one OData entity set, keyed on a declared key.            |
| [`testing`](_autosummary/odatadol.testing.html.md#module-odatadol.testing)     | A scripted OData service for tests: no network, no password.                         |
| [`transport`](_autosummary/odatadol.transport.html.md#module-odatadol.transport) | The one request odatadol makes: an authenticated `GET`, through a swappable `fetch`. |


# _autosummary/odatadol.metadata.html.md

# odatadol.metadata

An entity type’s declared properties, read from the service’s CSDL `$metadata`.

[`entity_metadata()`](_autosummary/odatadol.metadata.html.md#odatadol.metadata.entity_metadata) returns `{name: {'type': ..., 'nullable': ...,
'annotations': {...}}}` in declaration order, with Edm type names intact
(`Edm.Decimal`, `Edm.Binary`, `Collection(Edm.String)`), so a caller can decide how
to render a value or which properties are not worth fetching (a binary, a stream, a
server-side global). `annotations` is present only when the property has any.

### Module Attributes

| [`EDM_NAMESPACE`](_autosummary/odatadol.metadata.html.md#odatadol.metadata.EDM_NAMESPACE)   | The CSDL namespace of OData v4.   |
|------------------------------------------------------------------|-----------------------------------|

### Functions

| [`entity_metadata`](_autosummary/odatadol.metadata.html.md#odatadol.metadata.entity_metadata)(service, entity, \*[, ...])   | The declared properties of one entity set's type, in declaration order.   |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`parse_entity_metadata`](_autosummary/odatadol.metadata.html.md#odatadol.metadata.parse_entity_metadata)(body, entity)           | The properties of `entity` in a CSDL document, inherited ones first.      |

### odatadol.metadata.EDM_NAMESPACE *= 'http://docs.oasis-open.org/odata/ns/edm'*

The CSDL namespace of OData v4.

### odatadol.metadata.entity_metadata(service, entity, , credentials=None, fetch=None)

The declared properties of one entity set’s type, in declaration order.

`entity` may name the entity set (resolved through the container) or the type.

* **Raises:**
  [**ODataError**](_autosummary/odatadol.html.md#odatadol.ODataError) – If the service declares no such entity, naming the ones it has.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.metadata.parse_entity_metadata(body, entity)

The properties of `entity` in a CSDL document, inherited ones first.

`entity` may be an entity set, a qualified type (`Namespace.Name` or
`Alias.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.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]


# _autosummary/odatadol.read.html.md

# 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()`](_autosummary/odatadol.read.html.md#odatadol.read.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()`](_autosummary/odatadol.read.html.md#odatadol.read.entity_columns) splits a
  wide `$select` into column groups under `max_url` and joins them on a key.

### Module Attributes

| [`DEFAULT_PAGE_SIZE`](_autosummary/odatadol.read.html.md#odatadol.read.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`](_autosummary/odatadol.read.html.md#odatadol.read.MAX_URL_CHARS)       | The longest URL [`entity_columns()`](_autosummary/odatadol.read.html.md#odatadol.read.entity_columns) sends.                                     |

### Functions

| [`entity_url`](_autosummary/odatadol.read.html.md#odatadol.read.entity_url)(service, entity, \*[, suffix, query])   | The URL of an entity set, optionally with a path suffix and query options.          |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| [`quote_name`](_autosummary/odatadol.read.html.md#odatadol.read.quote_name)(name)                                   | A property name as `$select`/`$orderby` accept it.                                  |
| [`entity_count`](_autosummary/odatadol.read.html.md#odatadol.read.entity_count)(service, entity, \*[, filter, ...])   | How many records the entity set holds now (or match `filter`).                      |
| [`entity_records`](_autosummary/odatadol.read.html.md#odatadol.read.entity_records)(service, entity, \*[, select, ...]) | Records of an entity set, lazily, page by page, with OData annotations removed.     |
| [`entity_columns`](_autosummary/odatadol.read.html.md#odatadol.read.entity_columns)(service, entity, \*, select, key)   | Every record with every `select` field, however many fields that is, as one moment. |

### Exceptions

| [`UnstableRead`](_autosummary/odatadol.read.html.md#odatadol.read.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()`](_autosummary/odatadol.read.html.md#odatadol.read.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='')

Bases: [`ODataError`](_autosummary/odatadol.transport.html.md#odatadol.transport.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)

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**](_autosummary/odatadol.read.html.md#odatadol.read.UnstableRead) – If reads disagree about which keys exist, their order, or their
        versions. The entity set changed during the read; retrying is safe.
  * [**ODataError**](_autosummary/odatadol.html.md#odatadol.ODataError) – If `key` is empty or repeats: the join would silently merge
        different records into one.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If one field cannot fit under `max_url` at all.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.read.entity_count(service, entity, , filter=None, credentials=None, fetch=None)

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

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#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)

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`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Properties to fetch (quoted as needed). `None` fetches all of them.
  * **filter** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `$filter` expression, sent as written.
  * **orderby** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `$orderby` expression, sent as written (quote spaced names
    yourself with [`quote_name()`](_autosummary/odatadol.read.html.md#odatadol.read.quote_name)).
  * **top** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The most records to yield in all; `None` for every one.
  * **page_size** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Records asked for per request.
  * **parse_float** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Called on every JSON number with a fraction; `decimal.Decimal`
    keeps every digit. Defaults to `float`.
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

### odatadol.read.entity_url(service, entity, , suffix='', query=None)

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

`None`-valued options are dropped, and `:` is not percent-encoded.

```pycon
>>> 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`](https://docs.python.org/3/builtins/stdtypes.html#str)

### odatadol.read.quote_name(name)

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

```pycon
>>> 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**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – For a name holding a `"`, which no quoting can carry.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/odatadol.store.html.md

# odatadol.store

A read-only `Mapping` over one OData entity set, keyed on a declared key.

[`ODataReader`](_autosummary/odatadol.store.html.md#odatadol.store.ODataReader) is a thin facade over [`odatadol.read`](_autosummary/odatadol.read.html.md#module-odatadol.read): `len` is one
`$count`, iterating reads only the key column, `store[k]` and `k in store` are one
filtered request each, and `items()`/`values()` read in bulk (column groups joined on
the key when `select` is wide), never one request per key.

```pycon
>>> from odatadol.testing import FakeService
>>> server = FakeService([{'ProductID': 1, 'Name': 'Tea'}, {'ProductID': 2, 'Name': 'Jam'}])
>>> store = ODataReader('https://example.com/odata', 'Products', key='ProductID',
...                     fetch=server)
>>> len(store), list(store), store[2]
(2, [1, 2], {'ProductID': 2, 'Name': 'Jam'})
>>> 3 in store
False
```

Writes are not here, and not behind a flag: a writer, if one is ever needed, is a
separate class, so code holding a reader cannot reach one by passing the wrong argument.

### Functions

| [`odata_literal`](_autosummary/odatadol.store.html.md#odatadol.store.odata_literal)(value)   | `value` written as an OData `$filter` literal.   |
|-------------------------------------------------------------------------|--------------------------------------------------|

### Classes

| [`ODataReader`](_autosummary/odatadol.store.html.md#odatadol.store.ODataReader)(service, entity, \*, key[, ...])   | The records of `entity` at `service`, as `{key value: record}`.   |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|

### *class* odatadol.store.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)

Bases: [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)

The records of `entity` at `service`, as `{key value: record}`.

* **Parameters:**
  * **service** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The OData service root URL.
  * **entity** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The entity set name.
  * **key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The property records are addressed, ordered and joined by. It must be
    unique and never empty within the reader’s `filter`.
  * **select** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The properties a record holds. `None` fetches all of them; a wide
    list is split into column groups under `max_url` for bulk reads.
  * **filter** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `$filter` scoping the reader: keys outside it are not in it.
  * **version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A property rewritten on every edit, checked per key across column
    groups so a bulk read is one moment (see
    [`odatadol.read.entity_columns()`](_autosummary/odatadol.read.html.md#odatadol.read.entity_columns)).
  * **credentials** ([`Union`](https://docs.python.org/3/library/typing.html#typing.Union)[[`None`](https://docs.python.org/3/builtins/constants.html#None), `_HasHeader`, [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – `None`, a [`BasicAuth`](_autosummary/odatadol.transport.html.md#odatadol.transport.BasicAuth) or
    [`BearerToken`](_autosummary/odatadol.transport.html.md#odatadol.transport.BearerToken), or a callable returning one.
  * **fetch** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The transport, `(url, headers) -> (status, bytes)`.
  * **literal** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Writes a key as a `$filter` literal, for `store[k]` and
    `k in store`. Defaults to [`odata_literal()`](_autosummary/odatadol.store.html.md#odatadol.store.odata_literal); an `Edm.Guid` key read
    back as text by iteration needs `literal=str`. Raises `TypeError` for a
    key it cannot write, which the reader reports as missing.
  * **parse_float** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Applied to fractional JSON numbers (`decimal.Decimal` keeps
    every digit).

#### items()

Every `(key, record)`, read in bulk.

* **Return type:**
  [`ItemsView`](https://docs.python.org/3/library/collections.abc.html#collections.abc.ItemsView)

#### values()

Every record, read in bulk.

* **Return type:**
  [`ValuesView`](https://docs.python.org/3/library/collections.abc.html#collections.abc.ValuesView)

### odatadol.store.odata_literal(value)

`value` written as an OData `$filter` literal.

```pycon
>>> 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 `str` is always a quoted string literal: pass a [`uuid.UUID`](https://docs.python.org/3/library/uuid.html#uuid.UUID) for an
`Edm.Guid` and a [`datetime`](https://docs.python.org/3/library/datetime.html#module-datetime) object for a date or time.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/odatadol.testing.html.md

# odatadol.testing

A scripted OData service for tests: no network, no password.

[`FakeService`](_autosummary/odatadol.testing.html.md#odatadol.testing.FakeService) is a `fetch` (`(url, headers) -> (status, bytes)`) that answers
like a real, quirky server did: it refuses `Accept: text/plain` on `$count`, 404s a
URL past `max_url` the way an IIS front end does, writes raw CR characters inside
JSON strings, and takes double-quoted names in `$select`/`$orderby`. It records every
URL it was asked for, so a test can assert what crossed the wire.

```pycon
>>> server = FakeService([{'ProductID': 1, 'Unit Price': 2.5}])
>>> from odatadol import entity_records
>>> list(entity_records('https://example.com/odata', 'Products', fetch=server,
...                     select=['ProductID', 'Unit Price']))
[{'ProductID': 1, 'Unit Price': 2.5}]
>>> sorted(server.asked_for())
['ProductID', 'Unit Price']
```

Its `$filter` understands what odatadol itself sends: comparisons
(`eq ne gt ge lt le`) of a property with a literal, joined by `and`.

### Functions

| [`csdl`](_autosummary/odatadol.testing.html.md#odatadol.testing.csdl)(entity, properties, \*[, namespace, ...])   | A minimal CSDL `$metadata` document declaring one entity set.   |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|

### Classes

| [`FakeService`](_autosummary/odatadol.testing.html.md#odatadol.testing.FakeService)(rows, \*[, entity, metadata, ...])   | Answers OData requests over `rows` like a quirky real server, and records them.   |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|

### *class* odatadol.testing.FakeService(rows, , entity='Entities', metadata=None, max_url=10000, page_cap=None, next_links=False, require_auth=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Answers OData requests over `rows` like a quirky real server, and records them.

* **Parameters:**
  * **rows** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – The records of every entity set it serves (one set is enough for tests).
  * **entity** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The entity set the default `$metadata` declares.
  * **metadata** ([`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The `$metadata` body; defaults to one declaring `entity` with an
    `Edm.String` for every property of the first row.
  * **max_url** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – URLs longer than this get an IIS-style 404.
  * **page_cap** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Serve at most this many records per page, whatever `$top` asks,
    and say nothing about it (unless `next_links`), as some servers do.
  * **next_links** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Page by `@odata.nextLink` (with a `$skiptoken`) when a page
    is capped, as server-driven paging does.
  * **require_auth** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Answer 401 to a request without an `Authorization` header.

#### asked_for()

Every property named in a `$select` it was sent.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### odatadol.testing.csdl(entity, properties, , namespace='Demo', type_name=None, annotations=None, nullable=None)

A minimal CSDL `$metadata` document declaring one entity set.

* **Parameters:**
  * **properties** ([`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – `{name: Edm type}`.
  * **type_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The entity type’s name, if not `entity` (it often differs).
  * **annotations** ([`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – `{property: {term: Bool value}}`.
  * **nullable** ([`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – `{property: False}` for non-nullable properties.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)


# _autosummary/odatadol.transport.html.md

# odatadol.transport

The one request odatadol makes: an authenticated `GET`, through a swappable `fetch`.

Everything that touches the network goes through [`odata_get()`](_autosummary/odatadol.transport.html.md#odatadol.transport.odata_get), and
[`odata_get()`](_autosummary/odatadol.transport.html.md#odatadol.transport.odata_get) has no verb argument: odatadol cannot write, by construction.

The transport seam is `fetch=(url, headers) -> (status, body bytes)`. The default,
[`urllib_fetch()`](_autosummary/odatadol.transport.html.md#odatadol.transport.urllib_fetch), is the standard library; a test scripts a fake server instead,
and a host behind a proxy or with its own session pool passes its own.

```pycon
>>> calls = []
>>> def fake(url, headers):
...     calls.append((url, dict(headers)))
...     return 200, b'{"value": []}'
>>> odata_get('https://example.com/odata/Products', fetch=fake)
b'{"value": []}'
>>> calls[0][1]
{'Accept': 'application/json'}
```

Credentials are anything with a `header() -> str` method ([`BasicAuth`](_autosummary/odatadol.transport.html.md#odatadol.transport.BasicAuth),
[`BearerToken`](_autosummary/odatadol.transport.html.md#odatadol.transport.BearerToken)), or a zero-argument callable returning one, so a host can defer a
keychain or file lookup until a read starts (it runs once per read). Neither the password nor the
`Authorization` header ever appears in an error message, and the header never
follows a redirect or a `nextLink` to another origin.

### Module Attributes

| [`DEFAULT_TIMEOUT`](_autosummary/odatadol.transport.html.md#odatadol.transport.DEFAULT_TIMEOUT)   | Seconds before the default transport gives up on one request.                                |
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| [`Fetch`](_autosummary/odatadol.transport.html.md#odatadol.transport.Fetch)             | the transport seam.                                                                          |
| [`Credentials`](_autosummary/odatadol.transport.html.md#odatadol.transport.Credentials)       | `None` (anonymous), an object with `header()`, or a zero-argument callable returning either. |

### Functions

| [`urllib_fetch`](_autosummary/odatadol.transport.html.md#odatadol.transport.urllib_fetch)(url, headers, \*[, timeout])        | The default transport: one `GET` with [`urllib`](https://docs.python.org/3/library/urllib.html#module-urllib).   |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| [`odata_get`](_autosummary/odatadol.transport.html.md#odatadol.transport.odata_get)(url, \*[, credentials, accept, fetch]) | `GET` one OData URL and return the body.                                                                         |
| [`redact`](_autosummary/odatadol.transport.html.md#odatadol.transport.redact)(url)                                      | `url` without any `user:password@` it carries.                                                                   |
| [`resolve_credentials`](_autosummary/odatadol.transport.html.md#odatadol.transport.resolve_credentials)(credentials)                 | `credentials` as an object with `header()` (or `None`), calling a factory.                                       |
| [`same_origin`](_autosummary/odatadol.transport.html.md#odatadol.transport.same_origin)(a, b)                                | Whether two URLs share scheme, host and port: where credentials may follow.                                      |

### Classes

| [`BasicAuth`](_autosummary/odatadol.transport.html.md#odatadol.transport.BasicAuth)(account, password)   | An account and its password, for HTTP Basic.   |
|---------------------------------------------------------------------------------|------------------------------------------------|
| [`BearerToken`](_autosummary/odatadol.transport.html.md#odatadol.transport.BearerToken)(token)             | An OAuth2 or similar bearer token.             |

### Exceptions

| [`ODataError`](_autosummary/odatadol.transport.html.md#odatadol.transport.ODataError)(message, \*[, status, code])   | The service answered with an error.   |
|--------------------------------------------------------------------------------------------|---------------------------------------|

### *class* odatadol.transport.BasicAuth(account, password)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An account and its password, for HTTP Basic. The password is never shown.

```pycon
>>> BasicAuth('reader', 'secret')
BasicAuth(account='reader')
>>> BasicAuth('reader', 'secret').header()
'Basic cmVhZGVyOnNlY3JldA=='
```

### *class* odatadol.transport.BearerToken(token)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An OAuth2 or similar bearer token. Never shown.

### odatadol.transport.Credentials

`None` (anonymous), an object with `header()`,
or a zero-argument callable returning either.

* **Type:**
  What `credentials=` accepts

alias of [`None`](https://docs.python.org/3/builtins/constants.html#None) | `_HasHeader` | [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### odatadol.transport.DEFAULT_TIMEOUT *= 120.0*

Seconds before the default transport gives up on one request.

### odatadol.transport.Fetch

the transport seam. Only `GET` is ever
issued through it.

* **Type:**
  `(url, headers) -> (status, body bytes)`

alias of [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], tuple[int, bytes]]

### *exception* odatadol.transport.ODataError(message, , status=None, code='')

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The service answered with an error. Carries the HTTP status and the OData code.

### odatadol.transport.odata_get(url, , credentials=None, accept='application/json', fetch=None)

`GET` one OData URL and return the body. The only request odatadol makes.

* **Raises:**
  [**ODataError**](_autosummary/odatadol.transport.html.md#odatadol.transport.ODataError) – On any non-2xx status, with the service’s error code and message.
      The message names the URL (without userinfo) and never the
      `Authorization` header.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### odatadol.transport.redact(url)

`url` without any `user:password@` it carries.

```pycon
>>> 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:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### odatadol.transport.resolve_credentials(credentials)

`credentials` as an object with `header()` (or `None`), calling a factory.

Functions that make several requests resolve once, so a factory (a keychain lookup)
runs once per read, not once per page.

* **Return type:**
  `_HasHeader` | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### odatadol.transport.same_origin(a, b)

Whether two URLs share scheme, host and port: where credentials may follow.

```pycon
>>> same_origin('https://example.com/odata/A', 'https://example.com/odata/B?x=1')
True
>>> same_origin('https://example.com/odata', 'https://elsewhere.example/odata')
False
```

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### odatadol.transport.urllib_fetch(url, headers, , timeout=120.0)

The default transport: one `GET` with [`urllib`](https://docs.python.org/3/library/urllib.html#module-urllib). An HTTP error is returned,
not raised, so [`odata_get()`](_autosummary/odatadol.transport.html.md#odatadol.transport.odata_get) reads every status the same way.

A redirect to another origin is followed without the `Authorization` header.
Use [`functools.partial()`](https://docs.python.org/3/library/functools.html#functools.partial) to change `timeout`.

* **Raises:**
  [**ODataError**](_autosummary/odatadol.transport.html.md#odatadol.transport.ODataError) – If no HTTP answer came back (DNS, refused connection, timeout, a
      malformed URL), naming the URL without any userinfo it carries.
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-29 03:42 UTC** from commit <a href="https://github.com/i2mint/odatadol/commit/708a714dee44f032b42d06787cedb8cdfd120a4c"><code>708a714</code></a> on branch <code>main</code>, for **odatadol 0.0.1** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.0.1) is behind the latest release on PyPI (0.0.2): `pip install odatadol` gives newer code than these docs describe.

## Source

|                     |                                                                                                                                                        |
|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/i2mint/odatadol/commit/708a714dee44f032b42d06787cedb8cdfd120a4c"><code>708a714dee44f032b42d06787cedb8cdfd120a4c</code></a> |
| Branch              | <code>main</code>                                                                                                                                      |
| Tags at this commit | none                                                                                                                                                   |
| Working tree        | clean                                                                                                                                                  |
| Remote              | <code>https://github.com/i2mint/odatadol</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>i2mint/odatadol</code>                                                               |
| Run          | <a href="https://github.com/i2mint/odatadol/actions/runs/40813526298">40813526298</a>      |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>708a714dee44f032b42d06787cedb8cdfd120a4c</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.22  |
| Sphinx   | 9.1.1   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>pydata_sphinx_theme</code>) |
| accent        | <code>#006d64</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/odatadol/0.0.2/">0.0.2</a>, newer than the documented version (0.0.1).

## Reproduce

```bash
git clone https://github.com/i2mint/odatadol && cd odatadol
git checkout 708a714dee44f032b42d06787cedb8cdfd120a4c
pip install "epythet==0.2.22"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# api.html.md

# API reference

| [`odatadol`](_autosummary/odatadol.html.md#module-odatadol)   | Read an OData v4 entity set as a Mapping, with the standard library only.   |
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------|


