Skip to content

nanosdk.client

The clients, and the bound routes you get when you access a route on one.

class GitHub(nanosdk.Client):
    get_user = nanosdk.get("/users/{username}", request=GetUser, response=User)

with GitHub("https://api.github.com") as gh:
    user = gh.get_user(username="octocat")      # gh.get_user is a BoundRoute

A client's public attributes are your routes plus with_options, close / aclose and the context-manager protocol. Everything else is underscored, including the hooks you override (_prepare_request, _process_response, _raise_for_status, _retry_policy), so routes can be named anything except those few public names.

Client

Client(
    base_url: str | URL = "",
    *,
    headers: Mapping[str, str] | None = None,
    timeout: TimeoutTypes = DEFAULT_TIMEOUT,
    auth: Auth | tuple[str, str] | None = None,
    error_model: type[Any] | None = None,
    retry: Retry | None = None,
    logger: Logger | None = None,
    http_client: Client | None = None,
    **httpx_kwargs: Any
)

Synchronous API client, built on httpx.Client.

Subclass it and declare routes as class attributes; on an instance each route becomes a BoundRoute method:

class GitHub(nanosdk.Client):
    get_user = nanosdk.get("/users/{username}", request=GetUser, response=User)

with GitHub("https://api.github.com") as gh:
    user = gh.get_user(username="octocat")  # -> User

Apart from your routes, an instance only exposes with_options and close (or use it as a context manager). Customise behaviour by overriding the underscored hooks _prepare_request, _process_response, _raise_for_status and _retry_policy. A route named like one of the client's own attributes (close, with_options, a hook, ...) raises TypeError when the class is defined.

Parameters:

Name Type Description Default
base_url str | URL

Prefix for every route path, such as "https://api.example.com/v2".

''
headers Mapping[str, str] | None

Headers sent with every request.

None
timeout TimeoutTypes

Seconds, an httpx.Timeout, or None to wait forever.

DEFAULT_TIMEOUT
auth Auth | tuple[str, str] | None

Any httpx.Auth (see nanosdk.auth) or a (username, password) tuple for basic auth.

None
error_model type[Any] | None

A type to parse 4xx/5xx bodies into, exposed as APIStatusError.error.

None
retry Retry | None

Retry policy for transient failures; None (the default) never retries.

None
logger Logger | None

Where to log requests, responses and retries; defaults to the nanosdk logger. SDKs can pass their own, such as logging.getLogger("my_sdk").

None
http_client Client | None

Use this httpx.Client instead of creating one. It isn't closed for you, and can't be combined with base_url, headers, auth or httpx_kwargs: configure it directly.

None
**httpx_kwargs Any

Any other httpx.Client argument (transport, proxy, verify, limits, event_hooks, ...).

{}

Raises:

Type Description
ValueError

If http_client is combined with options that configure httpx.

Methods:

Name Description
with_options

Return a copy of the client with extra headers or a different timeout/retry.

close

Close the connection pool, unless it came from http_client=.

_prepare_request

Hook called on every outgoing request, including each retry. Override it.

_process_response

Hook called on every final response, before the status check and decoding.

_raise_for_status

Hook: raise if response is an error. Override to map errors your own way.

_retry_policy

Hook: choose the retry policy for a route. Override to vary it per route.

with_options

with_options(
    *,
    headers: Mapping[str, str] | None = None,
    timeout: TimeoutTypes | _NotGiven = NOT_GIVEN,
    retry: Retry | _NotGiven | None = NOT_GIVEN
) -> Self

Return a copy of the client with extra headers or a different timeout/retry.

The copy is the same class, so all routes are available on it, and it shares the original's connection pool. It never closes the pool: close the original.

Example
scoped = api.with_options(headers={"Authorization": f"Bearer {token}"})
slow = api.with_options(timeout=120, retry=None)

Parameters:

Name Type Description Default
headers Mapping[str, str] | None

Headers added to every request, on top of existing ones.

None
timeout TimeoutTypes | _NotGiven

Replaces the timeout; None disables it.

NOT_GIVEN
retry Retry | _NotGiven | None

Replaces the retry policy; None disables retries.

NOT_GIVEN

Returns:

Type Description
Self

The configured copy.

close

close() -> None

Close the connection pool, unless it came from http_client=.

_prepare_request

_prepare_request(
    request: Request, route: Route[Any, Any, Any]
) -> Request

Hook called on every outgoing request, including each retry. Override it.

Use it to sign requests, add per-request headers or log. Auth from auth= is applied after this hook.

Example
def _prepare_request(self, request, route):
    if route.meta.get("signed", True):
        request.headers["X-Signature"] = sign(request)
    return request

Parameters:

Name Type Description Default
request Request

The request about to be sent.

required
route Route[Any, Any, Any]

The route being called; read route.name or route.meta.

required

Returns:

Type Description
Request

The request to send: request itself (modified or not) or a replacement.

_process_response

_process_response(
    response: Response, route: Route[Any, Any, Any]
) -> Response

Hook called on every final response, before the status check and decoding.

Override it to unwrap response envelopes, record rate limits or log. Responses that are retried don't reach it.

Parameters:

Name Type Description Default
response Response

The response.

required
route Route[Any, Any, Any]

The route that was called.

required

Returns:

Type Description
Response

The response to check and decode: response or a replacement. Pass

Response

request=response.request when building a new httpx.Response.

_raise_for_status

_raise_for_status(response: Response) -> None

Hook: raise if response is an error. Override to map errors your own way.

Called after _process_response and before decoding, for every call except .raw(). The default raises APIStatusError on a 4xx or 5xx status, with the body parsed by the client's error_model when one is set.

Parameters:

Name Type Description Default
response Response

The final response for a call.

required

Raises:

Type Description
APIStatusError

If the status is 4xx or 5xx.

_retry_policy

_retry_policy(route: Route[Any, Any, Any]) -> Retry | None

Hook: choose the retry policy for a route. Override to vary it per route.

Example
def _retry_policy(self, route):
    if route.meta.get("no_retry"):
        return None
    return super()._retry_policy(route)

Parameters:

Name Type Description Default
route Route[Any, Any, Any]

The route about to be sent.

required

Returns:

Type Description
Retry | None

The policy to use, or None for no retries. By default, the client's

Retry | None

retry= setting.

AsyncClient

AsyncClient(
    base_url: str | URL = "",
    *,
    headers: Mapping[str, str] | None = None,
    timeout: TimeoutTypes = DEFAULT_TIMEOUT,
    auth: Auth | tuple[str, str] | None = None,
    error_model: type[Any] | None = None,
    retry: Retry | None = None,
    logger: Logger | None = None,
    http_client: AsyncClient | None = None,
    **httpx_kwargs: Any
)

Asynchronous API client, built on httpx.AsyncClient.

The async twin of Client, with the same constructor arguments (http_client takes an httpx.AsyncClient). Route calls return awaitables, paginated routes give async iterators, the _prepare_request / _process_response hooks are async def, and aclose replaces close.

class AsyncGitHub(Users, Repos, nanosdk.AsyncClient): ...

async with AsyncGitHub("https://api.github.com") as gh:
    user = await gh.get_user(username="octocat")
    async for repo in gh.list_repos.iter(username="octocat"): ...

Parameters:

Name Type Description Default
base_url str | URL

Prefix for every route path.

''
headers Mapping[str, str] | None

Headers sent with every request.

None
timeout TimeoutTypes

Seconds, an httpx.Timeout, or None to wait forever.

DEFAULT_TIMEOUT
auth Auth | tuple[str, str] | None

Any httpx.Auth or a (username, password) tuple.

None
error_model type[Any] | None

A type to parse 4xx/5xx bodies into.

None
retry Retry | None

Retry policy for transient failures; None never retries.

None
logger Logger | None

Where to log; defaults to the nanosdk logger.

None
http_client AsyncClient | None

Use this httpx.AsyncClient instead of creating one.

None
**httpx_kwargs Any

Any other httpx.AsyncClient argument.

{}

Raises:

Type Description
ValueError

If http_client is combined with options that configure httpx.

Methods:

Name Description
with_options

Return a copy of the client with extra headers or a different timeout/retry.

aclose

Close the connection pool, unless it came from http_client=.

_prepare_request

Hook called on every outgoing request, including each retry.

_process_response

Hook called on every final response, before the status check and decoding.

_raise_for_status

Hook: raise if response is an error. Override to map errors your own way.

_retry_policy

Hook: choose the retry policy for a route. Override to vary it per route.

with_options

with_options(
    *,
    headers: Mapping[str, str] | None = None,
    timeout: TimeoutTypes | _NotGiven = NOT_GIVEN,
    retry: Retry | _NotGiven | None = NOT_GIVEN
) -> Self

Return a copy of the client with extra headers or a different timeout/retry.

The copy is the same class, so all routes are available on it, and it shares the original's connection pool. It never closes the pool: close the original.

Example
scoped = api.with_options(headers={"Authorization": f"Bearer {token}"})
slow = api.with_options(timeout=120, retry=None)

Parameters:

Name Type Description Default
headers Mapping[str, str] | None

Headers added to every request, on top of existing ones.

None
timeout TimeoutTypes | _NotGiven

Replaces the timeout; None disables it.

NOT_GIVEN
retry Retry | _NotGiven | None

Replaces the retry policy; None disables retries.

NOT_GIVEN

Returns:

Type Description
Self

The configured copy.

aclose async

aclose() -> None

Close the connection pool, unless it came from http_client=.

_prepare_request async

_prepare_request(
    request: Request, route: Route[Any, Any, Any]
) -> Request

Hook called on every outgoing request, including each retry.

The async version of Client._prepare_request.

Parameters:

Name Type Description Default
request Request

The request about to be sent.

required
route Route[Any, Any, Any]

The route being called.

required

Returns:

Type Description
Request

The request to send.

_process_response async

_process_response(
    response: Response, route: Route[Any, Any, Any]
) -> Response

Hook called on every final response, before the status check and decoding.

The async version of Client._process_response.

Parameters:

Name Type Description Default
response Response

The response.

required
route Route[Any, Any, Any]

The route that was called.

required

Returns:

Type Description
Response

The response to check and decode.

_raise_for_status

_raise_for_status(response: Response) -> None

Hook: raise if response is an error. Override to map errors your own way.

Called after _process_response and before decoding, for every call except .raw(). The default raises APIStatusError on a 4xx or 5xx status, with the body parsed by the client's error_model when one is set.

Parameters:

Name Type Description Default
response Response

The final response for a call.

required

Raises:

Type Description
APIStatusError

If the status is 4xx or 5xx.

_retry_policy

_retry_policy(route: Route[Any, Any, Any]) -> Retry | None

Hook: choose the retry policy for a route. Override to vary it per route.

Example
def _retry_policy(self, route):
    if route.meta.get("no_retry"):
        return None
    return super()._retry_policy(route)

Parameters:

Name Type Description Default
route Route[Any, Any, Any]

The route about to be sent.

required

Returns:

Type Description
Retry | None

The policy to use, or None for no retries. By default, the client's

Retry | None

retry= setting.

http_client

http_client(client: Client) -> Client
http_client(client: AsyncClient) -> AsyncClient
http_client(
    client: Client | AsyncClient,
) -> Client | AsyncClient

The httpx client underneath a nanosdk client, for anything nanosdk doesn't wrap.

A function rather than an attribute, so it can't collide with a route name.

Example
nanosdk.http_client(api).cookies.set("session", "...")

Parameters:

Name Type Description Default
client Client | AsyncClient

A nanosdk client.

required

Returns:

Type Description
Client | AsyncClient

Its httpx.Client (or httpx.AsyncClient).

Bound routes

What you get when you access a route on a client instance (api.get_user).

BoundRoute

BoundRoute(route: Route[P, ReqT, RespT], client: Client)

A route attached to a Client; what api.some_route gives you.

Call it with the request model's arguments to send the request and get the decoded response. The other methods are variations for when you need more control.

api.get_user(user_id=1)                 # -> User
api.get_user.send(GetUser(user_id=1))   # already-built model
api.get_user.raw(user_id=1)             # httpx.Response, no status check / decoding
api.get_user.build_request(user_id=1)   # httpx.Request, not sent

Attributes:

Name Type Description
route Route[P, ReqT, RespT]

The underlying Route.

client Client

The client it's bound to.

Methods:

Name Description
__call__

Build the request model from the arguments, send it, and decode the response.

send

Send an already-built request model and decode the response.

raw

Like calling the route, but return the httpx.Response as-is.

build_request

Build the request that calling the route would send, without sending it.

__call__

__call__(*args: P.args, **kwargs: P.kwargs) -> RespT

Build the request model from the arguments, send it, and decode the response.

Returns:

Type Description
RespT

The decoded response.

Raises:

Type Description
ValidationError

If the arguments are invalid; nothing is sent.

APIStatusError

On a 4xx/5xx response.

APIConnectionError

If no response arrived, after any retries.

ResponseValidationError

If the body doesn't match the response type.

send

send(request: ReqT) -> RespT

Send an already-built request model and decode the response.

Parameters:

Name Type Description Default
request ReqT

An instance of the route's request model.

required

Returns:

Type Description
RespT

The decoded response.

raw

raw(*args: P.args, **kwargs: P.kwargs) -> Response

Like calling the route, but return the httpx.Response as-is.

Hooks and retries apply; there's no status check and no decoding.

Returns:

Type Description
Response

The response, whatever its status.

build_request

build_request(
    *args: P.args, **kwargs: P.kwargs
) -> Request

Build the request that calling the route would send, without sending it.

Auth and the _prepare_request hook aren't applied; they run at send time.

Returns:

Type Description
Request

The unsent request.

BoundPaginatedRoute

BoundPaginatedRoute(
    paginated: PaginatedRoute[P, ReqT, RespT, ItemT],
    client: Client,
)

Bases: BoundRoute[P, ReqT, RespT]

A paginated route attached to a Client.

Calling it fetches just the first page, like any BoundRoute; iter and pages walk all of them.

first = api.list_posts(limit=50)            # the first page's response
for post in api.list_posts.iter(limit=50):  # every post, page by page
    ...

Attributes:

Name Type Description
paginated PaginatedRoute[P, ReqT, RespT, ItemT]

The underlying PaginatedRoute.

Methods:

Name Description
__call__

Build the request model from the arguments, send it, and decode the response.

send

Send an already-built request model and decode the response.

raw

Like calling the route, but return the httpx.Response as-is.

build_request

Build the request that calling the route would send, without sending it.

iter

Iterate over every item on every page.

pages

Iterate over every page, with each page's request, data, items and response.

__call__

__call__(*args: P.args, **kwargs: P.kwargs) -> RespT

Build the request model from the arguments, send it, and decode the response.

Returns:

Type Description
RespT

The decoded response.

Raises:

Type Description
ValidationError

If the arguments are invalid; nothing is sent.

APIStatusError

On a 4xx/5xx response.

APIConnectionError

If no response arrived, after any retries.

ResponseValidationError

If the body doesn't match the response type.

send

send(request: ReqT) -> RespT

Send an already-built request model and decode the response.

Parameters:

Name Type Description Default
request ReqT

An instance of the route's request model.

required

Returns:

Type Description
RespT

The decoded response.

raw

raw(*args: P.args, **kwargs: P.kwargs) -> Response

Like calling the route, but return the httpx.Response as-is.

Hooks and retries apply; there's no status check and no decoding.

Returns:

Type Description
Response

The response, whatever its status.

build_request

build_request(
    *args: P.args, **kwargs: P.kwargs
) -> Request

Build the request that calling the route would send, without sending it.

Auth and the _prepare_request hook aren't applied; they run at send time.

Returns:

Type Description
Request

The unsent request.

iter

iter(
    *args: P.args, **kwargs: P.kwargs
) -> Iterator[ItemT]

Iterate over every item on every page.

Arguments are validated immediately; pages are fetched as you iterate, so itertools.islice or break stops further requests.

Returns:

Type Description
Iterator[ItemT]

A lazy iterator over the items.

pages

pages(
    *args: P.args, **kwargs: P.kwargs
) -> Iterator[Page[ReqT, RespT, ItemT]]

Iterate over every page, with each page's request, data, items and response.

Arguments are validated immediately; pages are fetched as you iterate.

Returns:

Type Description
Iterator[Page[ReqT, RespT, ItemT]]

A lazy iterator over Page objects.

AsyncBoundRoute

AsyncBoundRoute(
    route: Route[P, ReqT, RespT], client: AsyncClient
)

A route attached to an AsyncClient.

The async twin of BoundRoute: calling it, send and raw return awaitables.

Attributes:

Name Type Description
route Route[P, ReqT, RespT]

The underlying Route.

client AsyncClient

The client it's bound to.

Methods:

Name Description
__call__

Build the request model, send it, and decode the response.

send

Send an already-built request model and decode the response.

raw

Like calling the route, but return the httpx.Response as-is.

build_request

Build the request that calling the route would send, without sending it.

__call__ async

__call__(*args: P.args, **kwargs: P.kwargs) -> RespT

Build the request model, send it, and decode the response.

Returns:

Type Description
RespT

The decoded response.

send async

send(request: ReqT) -> RespT

Send an already-built request model and decode the response.

Parameters:

Name Type Description Default
request ReqT

An instance of the route's request model.

required

Returns:

Type Description
RespT

The decoded response.

raw async

raw(*args: P.args, **kwargs: P.kwargs) -> Response

Like calling the route, but return the httpx.Response as-is.

Returns:

Type Description
Response

The response, whatever its status.

build_request

build_request(
    *args: P.args, **kwargs: P.kwargs
) -> Request

Build the request that calling the route would send, without sending it.

Returns:

Type Description
Request

The unsent request.

AsyncBoundPaginatedRoute

AsyncBoundPaginatedRoute(
    paginated: PaginatedRoute[P, ReqT, RespT, ItemT],
    client: AsyncClient,
)

Bases: AsyncBoundRoute[P, ReqT, RespT]

A paginated route attached to an AsyncClient.

async for post in api.list_posts.iter(limit=50):
    ...

Attributes:

Name Type Description
paginated PaginatedRoute[P, ReqT, RespT, ItemT]

The underlying PaginatedRoute.

Methods:

Name Description
__call__

Build the request model, send it, and decode the response.

send

Send an already-built request model and decode the response.

raw

Like calling the route, but return the httpx.Response as-is.

build_request

Build the request that calling the route would send, without sending it.

iter

Iterate over every item on every page with async for.

pages

Iterate over every page with async for.

__call__ async

__call__(*args: P.args, **kwargs: P.kwargs) -> RespT

Build the request model, send it, and decode the response.

Returns:

Type Description
RespT

The decoded response.

send async

send(request: ReqT) -> RespT

Send an already-built request model and decode the response.

Parameters:

Name Type Description Default
request ReqT

An instance of the route's request model.

required

Returns:

Type Description
RespT

The decoded response.

raw async

raw(*args: P.args, **kwargs: P.kwargs) -> Response

Like calling the route, but return the httpx.Response as-is.

Returns:

Type Description
Response

The response, whatever its status.

build_request

build_request(
    *args: P.args, **kwargs: P.kwargs
) -> Request

Build the request that calling the route would send, without sending it.

Returns:

Type Description
Request

The unsent request.

iter

iter(
    *args: P.args, **kwargs: P.kwargs
) -> AsyncIterator[ItemT]

Iterate over every item on every page with async for.

Returns:

Type Description
AsyncIterator[ItemT]

A lazy async iterator over the items.

pages

pages(
    *args: P.args, **kwargs: P.kwargs
) -> AsyncIterator[Page[ReqT, RespT, ItemT]]

Iterate over every page with async for.

Returns:

Type Description
AsyncIterator[Page[ReqT, RespT, ItemT]]

A lazy async iterator over Page objects.