Skip to content

feat(fmp-py): add AsyncFmpClient asyncio interface to Python bindings - #423

Merged
benbenbang merged 6 commits into
mainfrom
feat/async-client
Oct 11, 2026
Merged

benbenbang merged 6 commits into
mainfrom
feat/async-client

Conversation

@benbenbang

Copy link
Copy Markdown
Member

The FMP Python bindings previously offered only a synchronous client, forcing users to block threads or wrap calls themselves to use the API from asyncio code. This branch adds a fully asynchronous AsyncFmpClient that mirrors the synchronous client so callers can await endpoints directly, run requests concurrently, and integrate with event-loop-based frameworks.

Async client:

  • Add AsyncFmpClient with coroutine endpoint methods that share the same constructor keywords, environment fallbacks, defaults, exceptions, and on_response hook as FmpClient.
  • Run requests on the process-wide runtime so awaiting never blocks the event loop and concurrent coroutines execute in parallel, with support for cancellation, close()/await aclose(), and async with.
  • Register AsyncFmpClient in the native module and re-export it from the fmp and fmp.client facades.

Async namespaces:

  • Emit an Async*Namespace twin for every endpoint group, each exposing the same methods as its synchronous counterpart as coroutines, wired into client and parent getters.
  • Add Async*Namespace type stubs and re-export the new classes through the generated __init__.py/__init__.pyi modules and their __all__ lists.

Runtime and code generation:

  • Add spawn, the async counterpart of block_on, and an AbortOnDrop guard so a cancelled Python coroutine aborts its in-flight request.
  • Extract shared constructor validation into a build_handle helper and factor the method renderer into shared paths reused by sync and async generation.

Tests and docs:

  • Add Rust unit tests for the AbortOnDrop guard and a Python async client test suite, and extend the fixture server to simulate slow and abandoned requests with an async_client fixture.
  • Replace the "no async facade" notes in the README and wiki with coverage of AsyncFmpClient, add an async_client.py example, and document an Airflow deferrable operator use case.

close #422

This change introduces `AsyncFmpClient`, an asyncio-compatible counterpart to
`FmpClient` whose endpoint methods are coroutines to `await`. It shares the
same constructor keywords, environment fallbacks, defaults, and exceptions as
the synchronous client, and runs requests on the process-wide runtime so
awaiting never blocks the event loop and concurrent coroutines run in parallel.

**New async client:**
* Added `AsyncFmpClient` with `close()`, `await aclose()`, and `async with`
  support via `__aenter__`/`__aexit__`, mirroring the synchronous client's
  connection lifecycle and `on_response` hook behaviour.
* Enabled the `experimental-async` pyo3 feature required for the async methods.

**Shared construction:**
* Extracted the constructor validation and builder assembly into a reusable
  `build_handle` helper so both `FmpClient` and `AsyncFmpClient` construct
  their client handle through the same path.

**Module registration:**
* Registered `AsyncFmpClient` in the native module and re-exported it from the
  `fmp` and `fmp.client` facades alongside `FmpClient`.
Extend the Python bindings to expose an `AsyncFmpClient` that mirrors the
existing synchronous client. Every namespace node now renders two classes:
the sync `<Path>Namespace` and its `Async<Path>Namespace` twin, each with a
getter on the respective client. The async wrappers take owned `String`
arguments (coroutine arguments must be `'static`), lend them back to the
shared query builders, and run each request as a task on the process-wide
runtime instead of blocking.

**Runtime support:**
* Add `spawn`, the asynchronous counterpart of `block_on`, which runs an
  operation against a cached client as a task on the shared runtime and
  awaits it without blocking the event loop thread.
* Introduce `AbortOnDrop` so a cancelled Python coroutine aborts its
  in-flight request rather than detaching it.

**Code generation:**
* Factor the method renderer into a shared header, call, and query-builder
  path reused by both `render_method` and the new `render_async_method`.
* Add `async_struct_name_for` and emit both class flavors per node, wiring
  the `Async` twins into registration, client getters, and parent getters.
* Register every `Async*Namespace` class alongside its sync counterpart.

Dynamic-row endpoints reattach for the `dict` conversion once the task
resolves. Tests cover the async class shape, owned-argument lending, and
registration wiring.
This change exposes an asyncio-based client across the FMP Python bindings,
giving users a fully asynchronous interface alongside the existing
synchronous one.

**New async client:**
* Added `AsyncFmpClient` to the top-level `fmp` package and `fmp.client`,
  mirroring `FmpClient` with the same constructor keywords, environment
  fallbacks, defaults, and exceptions, but with every endpoint method as an
  awaitable coroutine.
* The client shares the process-wide runtime so concurrent coroutines run
  their requests in parallel without blocking the event loop, supports
  cancellation, `close()`/`await aclose()`, and `async with` usage.

**Async namespaces:**
* Added `Async*Namespace` type stubs for every endpoint group (analyst, bulk,
  calendar, chart, company, quote, statements, and all others), each
  providing the same methods as its synchronous counterpart as coroutines.
* Re-exported the new async classes through the generated `__init__.py` and
  `__init__.pyi` modules and their `__all__` lists.
Add Rust unit tests for the `AbortOnDrop` guard, verifying that dropping
the guard aborts the spawned task and tears down its state, that the
guard resolves to the task output, and that a panicking task surfaces as
an error.

Extend the Python fixture server to simulate slow and abandoned
requests: routes now carry a `delay`, the server tracks `disconnects`
for clients that hang up before a delayed answer, and a larger listen
backlog absorbs bursts of concurrent connections. Add an `async_client`
fixture returning an `AsyncFmpClient` with the same unauthenticated
configuration as `client`, and document the new behaviour in the
fixture docstrings.
Add a new test file `crates/fmp-py/tests/test_async_client.py` to cover
the asynchronous client functionality. This ensures the async client
behaves correctly and guards against regressions in future changes.
Replace the "no async facade" notes in the fmp-py and wiki Python docs with
coverage of `AsyncFmpClient`, the asyncio twin that shares the same keywords,
namespaces, and methods as `FmpClient` with coroutine endpoints. Describe its
event loop, concurrency, cancellation, error, and lifecycle behavior, and add
an Airflow deferrable operator example for use inside a trigger. Add an
`async_client.py` example and list it in the examples README.

close #422
@benbenbang
benbenbang enabled auto-merge (rebase) October 11, 2026 17:42
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request build labels Oct 11, 2026
@benbenbang
benbenbang merged commit 20a1439 into main Oct 11, 2026
10 checks passed
@benbenbang
benbenbang deleted the feat/async-client branch October 11, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(fmp-py): AsyncFmpClient for asyncio and Airflow deferrable

1 participant