Metadata-Version: 2.4
Name: truewire-core
Version: 0.1.1
Summary: Runtime for Truewire-generated API clients: async HTTP and WebSocket transport, response validation, paging, timestamps, errors.
Author: Truewire contributors
License-Expression: MIT
Project-URL: Repository, https://github.com/truewire-dev/truewire
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typing-extensions
Requires-Dist: httpx
Requires-Dist: websockets
Requires-Dist: orjson
Requires-Dist: pydantic
Requires-Dist: lazy-loader
Provides-Extra: grpc
Requires-Dist: grpclib; extra == "grpc"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: grpclib; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# Truewire Core

> Runtime for Truewire-generated API clients

[![PyPI version](https://img.shields.io/pypi/v/truewire-core.svg)](https://pypi.org/project/truewire-core/)
[![License](https://img.shields.io/pypi/l/truewire-core.svg)](LICENSE)

Every client generated by [Truewire](https://github.com/truewire-dev/truewire) depends on this
package. It holds the parts of a client that are the same for every API — async HTTP and
WebSocket transport, response validation, paging, timestamp conversion and the error
hierarchy — so that generated code only has to carry what is genuinely specific to one API:
envelope extraction, error mapping, request signing, wire quirks.

You do not normally install it directly; a generated client lists it as a dependency. You
will import from it when catching errors, converting timestamps, or driving a paginated walk.

## Installation

```bash
pip install truewire-core
```

Optional extras:

```bash
pip install 'truewire-core[grpc]'   # gRPC transport (grpclib)
```

## What it provides

| module | contents |
| --- | --- |
| `truewire_core.exceptions` | `Error`, `NetworkError`, `ValidationError`, `ApiError` (`BadRequest`, `AuthError`, `RateLimited`), `LogicError` |
| `truewire_core.http` | `HttpClient`, an async HTTP client wrapping `httpx` with lazy connection and `NetworkError` mapping |
| `truewire_core.ws` | WebSocket base classes: `Socket`, `Streams`, `Rpc`, `StreamsRpc`, `SerialReplies` |
| `truewire_core.grpc` | `GrpcClient`, `GrpcEndpoint`, `wrap_exceptions` (requires the `grpc` extra) |
| `truewire_core.validation` | `validator[T]`, a cached pydantic adapter that validates and dumps wire shapes, and a base `TypedDict` that tolerates undocumented fields |
| `truewire_core.times` | `TimeConverter`, `EpochConverter`, `IsoConverter`, `DateConverter` — parse/dump between a wire timestamp and a real `datetime`/`date` |
| `truewire_core.util` | `PaginatedResponse`/`Page`, `Stream`/`StreamManager`, `RateLimit`, and small numeric/path helpers |

### Errors

Every failure a client raises derives from `truewire_core.exceptions.Error`:

```python
from truewire_core.exceptions import ApiError, AuthError, NetworkError, RateLimited

try:
  order = await client.orders.get(id='ord_123')
except AuthError:
  ...          # bad or missing credentials
except RateLimited:
  ...          # the API told us to slow down
except ApiError as e:
  ...          # any other error the API itself returned (BadRequest, ...)
except NetworkError:
  ...          # couldn't reach the server, or the connection dropped
```

Generated clients re-export these from their own package root, so `from my_client import
AuthError` works too. Route such re-exports through `lazy_loader.attach_stub` (with a matching
`__init__.pyi`), not a plain `from truewire_core.exceptions import ...` in an `__init__.py` —
the latter breaks type-checking for every downstream consumer of a `py.typed` package.

### Timestamps

APIs put timestamps on the wire in many shapes: epoch seconds, milliseconds, microseconds or
nanoseconds; RFC 3339 strings with or without a `Z`; plain calendar dates. Each converter
turns one such shape into a real `datetime` (or `date`) and back:

```python
from datetime import timezone
from truewire_core.times import DateConverter, EpochConverter, IsoConverter

timestamp_millis = EpochConverter.milliseconds(tz=timezone.utc)   # 1717072496123 <-> datetime
timestamp_seconds = EpochConverter.seconds(tz=timezone.utc)       # 1717072496 <-> datetime
timestamp_iso = IsoConverter()                                    # '2024-05-30T12:34:56Z' <-> datetime
calendar_date = DateConverter()                                   # '2024-05-30' <-> date

dt = timestamp_iso.parse('2024-05-30T12:34:56.123456789Z')       # any fraction length, any Python >= 3.10
timestamp_millis.dump(dt)                                         # 1717072496123
```

A generated client wires these into its field types through pydantic, so a response field
declared as a millisecond epoch arrives as a `datetime` and a request parameter typed as one
is serialized back to the wire format the API expects.

### Paging

Every generated `<method>_paged` returns a `PaginatedResponse`: awaitable (every row,
flattened) and async-iterable (one page of rows at a time). Each page is one pure
`next(state)` call, so a caller can retry or resume a single page rather than the whole walk.

```python
from truewire_core import PaginatedResponse

paging = client.orders.list_paged(status='open')
orders = await paging                        # every row, flattened
async for rows in paging: ...                # one page at a time
async for page in paging.pages():            # Page(rows, state, next), for checkpointing
  checkpoint(page.next)
paging.resume(saved_state)                   # restart from a checkpointed state
paging.via(retried)                          # route every page fetch through a middleware
```

`via(call)` hands each page fetch to `call` as one zero-argument coroutine function, so a
retry or logging layer wraps a page without unrolling the loop by hand.

### Validation

Generated response types are `TypedDict`s. `validator` wraps a cached pydantic `TypeAdapter`
around one and raises `truewire_core.exceptions.ValidationError` (not pydantic's own) when
the wire body doesn't match:

```python
from truewire_core.validation import TypedDict, validator

class Order(TypedDict):
  id: str
  amount: str

order = validator(Order)(b'{"id": "ord_123", "amount": "10.5", "extra": true}')
# {'id': 'ord_123', 'amount': '10.5', 'extra': True} -- undocumented fields are kept, not rejected
validator(Order).dump(order)   # b'{"id":"ord_123","amount":"10.5","extra":true}'
```

## License

MIT — see [LICENSE](LICENSE).
