Skip to content

Extensibility

Subclass BaseResource and pass a CallContext

Import the supported extension types from stonepy.extensions: BaseResource, CallContext, EndpointSpec, Param, AuthPolicy, and StatusDomain. Construct your resource explicitly with MyResource(client.call_context) or a CallContext you assemble yourself. Both clients expose call_context; resources access the same context through self.call_context.

from pydantic import BaseModel

from stonepy import ClientConfig, StoneXClient
from stonepy.extensions import AuthPolicy, BaseResource, EndpointSpec, StatusDomain


class HealthResponse(BaseModel):
    healthy: bool


_HEALTH = EndpointSpec(
    name="Health",
    method="GET",
    path="/health",
    idempotent=True,
    auth_policy=AuthPolicy.NONE,
    rate_limit_bucket="health",
    response_model=HealthResponse,
    status_domain=StatusDomain.NONE,
)


class MyResource(BaseResource):
    """Resource for an application-specific health endpoint."""

    def health(self) -> HealthResponse:
        """Read the application's health endpoint."""
        return self.call_context.invoke(_HEALTH)


with StoneXClient(ClientConfig(base_url="https://api.example")) as client:
    resource = MyResource(client.call_context)
    # For an API that implements /health:
    response = resource.health()

For an async resource, define async def health and use return await self.call_context.ainvoke(_HEALTH). Prefer reusing AsyncStoneXClient.call_context, which already supplies the async components and logon callback. If you assemble the context yourself, async invocation requires a transport with asend() and an AsyncClock-capable clock with asleep(). For a context using AsyncSessionManager, also provide an alogon callable that returns an awaitable: when refresh needs that callback, alogon=None raises TypeError instead of falling back to synchronous logon. Synchronous-only clocks or transports also raise TypeError; ainvoke() no longer falls back to synchronous invocation.

Session mutations on an AsyncSessionManager must also be awaited. Reach it through manager = client.call_context.session, then use await manager.aset_token(token, username), await manager.aclear(expected_token=...), or await manager.arefresh(seen_generation, do_logon) with an awaitable logon callback. The synchronous set_token(), clear(), and refresh() methods raise TypeError without changing state or running a logon callback. Synchronous read accessors remain available. Use ainvoke() with an async session manager: a hand-built context passed to invoke() raises TypeError if it tries to refresh that manager. Synchronous contexts should use SessionManager.

The callback takes no arguments and resolves to either a token string (retaining the current username) or a (token, username) pair: Callable[[], Awaitable[str | tuple[str, str]]]. arefresh() returns None; it updates the manager or reuses a peer's newer generation. Capture seen_generation before deciding to refresh so concurrent callers can share that result. client.call_context.session is typed as SessionManager | AsyncSessionManager, even on an async client. Neither AsyncSessionManager nor the SessionRefreshResult alias is re-exported by stonepy or stonepy.extensions; the example explicitly depends on the internal stonepy._core.session module to narrow the union:

from collections.abc import Awaitable, Callable

from stonepy import AsyncStoneXClient
from stonepy._core.session import AsyncSessionManager


async def refresh_session(
    client: AsyncStoneXClient,
    fetch_session: Callable[[], Awaitable[tuple[str, str]]],
) -> None:
    manager = client.call_context.session
    assert isinstance(manager, AsyncSessionManager)
    seen_generation = await manager.ageneration()

    async def do_logon() -> str | tuple[str, str]:
        return await fetch_session()

    await manager.arefresh(seen_generation, do_logon)  # Returns None.

Supply fetch_session as your async credential-acquisition function. It runs while the manager's lock is held, so it must obtain credentials independently of this manager; calling this client's session.log_on() or another manager mutator inside the callback would reacquire that lock. To avoid an internal import, define a runtime-checkable Protocol with the ageneration() and arefresh() signatures above and narrow with isinstance(manager, YourProtocol) instead.

When seeding a manual token that needs its own callback for a 401 refresh/replay, use await manager.acommit(token, username, alogon) to install both together; aset_token() leaves any existing manual callback unchanged and does not install one on a fresh manager. An installed manual callback takes precedence over the callback passed to arefresh().

The call_context properties are read-only references to mutable, shared call state. Reusing a client's context shares its authentication, rate limiter, retry policy, and transport. Complete resource calls before closing that client; constructing a resource does not transfer transport ownership. The stonepy.extensions module docstring also shows explicit CallContext construction.

Entry-point plugin discovery and registration support have been removed. To migrate, remove the stonepy.resources entry-point registration, enable_plugins, allow_overrides, requires_stonepy, and ABI_VERSION, and replace client.plugin("name") with MyResource(client.call_context).

For business-status customization, see custom status decoders.