# odatadol.transport

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

Everything that touches the network goes through [`odata_get()`](#odatadol.transport.odata_get), and
[`odata_get()`](#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()`](#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`](#odatadol.transport.BasicAuth),
[`BearerToken`](#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`](#odatadol.transport.DEFAULT_TIMEOUT)   | Seconds before the default transport gives up on one request.                                |
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| [`Fetch`](#odatadol.transport.Fetch)             | the transport seam.                                                                          |
| [`Credentials`](#odatadol.transport.Credentials)       | `None` (anonymous), an object with `header()`, or a zero-argument callable returning either. |

### Functions

| [`urllib_fetch`](#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`](#odatadol.transport.odata_get)(url, \*[, credentials, accept, fetch]) | `GET` one OData URL and return the body.                                                                         |
| [`redact`](#odatadol.transport.redact)(url)                                      | `url` without any `user:password@` it carries.                                                                   |
| [`resolve_credentials`](#odatadol.transport.resolve_credentials)(credentials)                 | `credentials` as an object with `header()` (or `None`), calling a factory.                                       |
| [`same_origin`](#odatadol.transport.same_origin)(a, b)                                | Whether two URLs share scheme, host and port: where credentials may follow.                                      |

### Classes

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

### Exceptions

| [`ODataError`](#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**](#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()`](#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**](#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)]
