Skip to content

nanosdk.routing

Declaring routes: get/post/..., Route, PaginatedRoute and Routes groups.

A route ties an HTTP method and path to a request model and a response type:

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

On a client instance, api.get_user is a bound route you call like the request model's constructor; the result is the decoded response. See Where fields go for how request fields are placed.

Declaring routes

get module-attribute

get: Verb = Verb('GET')

Declare a GET route. Unmarked request fields go in the query string.

post module-attribute

post: Verb = Verb('POST')

Declare a POST route. Unmarked request fields become keys of a JSON body.

put module-attribute

put: Verb = Verb('PUT')

Declare a PUT route. Unmarked request fields become keys of a JSON body.

patch module-attribute

patch: Verb = Verb('PATCH')

Declare a PATCH route. Only fields the caller sets are sent (exclude_unset).

delete module-attribute

delete: Verb = Verb('DELETE')

Declare a DELETE route. Unmarked request fields go in the query string.

head module-attribute

head: Verb = Verb('HEAD')

Declare a HEAD route.

options module-attribute

options: Verb = Verb('OPTIONS')

Declare an OPTIONS route.

route

route(method: str) -> Verb

Get a Verb for any HTTP method.

Example
propfind = nanosdk.route("PROPFIND")("/dav/{path}", request=DavPath, response=str)

Parameters:

Name Type Description Default
method str

The HTTP method; any case.

required

Returns:

Type Description
Verb

A verb that declares routes with that method.

Verb

Verb(method: str)

Declares routes for one HTTP method; nanosdk.get, nanosdk.post and so on.

Call it with a path and the request/response types to get a Route. Overloads give the route precise types whether or not request and response are given. Use route for methods without a ready-made verb.

Parameters:

Name Type Description Default
method str

The HTTP method this verb declares.

required

Methods:

Name Description
__call__

Declare a route for this verb's method.

__call__

__call__(
    path: str,
    *,
    request: Callable[P, ReqT],
    response: type[RespT],
    headers: Mapping[str, str] | None = ...,
    exclude_unset: bool | None = ...,
    meta: Mapping[str, Any] | None = ...
) -> Route[P, ReqT, RespT]
__call__(
    path: str,
    *,
    request: Callable[P, ReqT],
    response: None = ...,
    headers: Mapping[str, str] | None = ...,
    exclude_unset: bool | None = ...,
    meta: Mapping[str, Any] | None = ...
) -> Route[P, ReqT, None]
__call__(
    path: str,
    *,
    request: None = ...,
    response: type[RespT],
    headers: Mapping[str, str] | None = ...,
    exclude_unset: bool | None = ...,
    meta: Mapping[str, Any] | None = ...
) -> Route[[], None, RespT]
__call__(
    path: str,
    *,
    request: None = ...,
    response: None = ...,
    headers: Mapping[str, str] | None = ...,
    exclude_unset: bool | None = ...,
    meta: Mapping[str, Any] | None = ...
) -> Route[[], None, None]
__call__(
    path: str,
    *,
    request: Any = None,
    response: Any = None,
    headers: Mapping[str, str] | None = None,
    exclude_unset: bool | None = None,
    meta: Mapping[str, Any] | None = None
) -> Route[Any, Any, Any]

Declare a route for this verb's method.

Example
get_user = nanosdk.get("/users/{user_id}", request=GetUser, response=User)
health = nanosdk.get("/health", response=str, headers={"X-Probe": "1"})

Parameters:

Name Type Description Default
path str

The path, relative to the client's base_url, with {name} placeholders filled from request-model fields.

required
request Any

The request model class; None for routes without arguments.

None
response Any

The response type (see Route); None ignores the body.

None
headers Mapping[str, str] | None

Static headers sent with every call.

None
exclude_unset bool | None

Only send fields the caller set; defaults to True for PATCH.

None
meta Mapping[str, Any] | None

Free-form data for client hooks.

None

Returns:

Type Description
Route[Any, Any, Any]

The route, typed from request and response.

Route objects

Route

Route(
    method: str,
    path: str,
    *,
    request: Callable[P, ReqT] | None = None,
    response: Any = None,
    headers: Mapping[str, str] | None = None,
    exclude_unset: bool | None = None,
    name: str | None = None,
    meta: Mapping[str, Any] | None = None
)

One HTTP endpoint: method, path, request model and response type.

Usually made with get, post and friends rather than directly. As a class attribute of a Client it's a descriptor: on an instance it becomes a BoundRoute you call with the request model's arguments.

The type parameters carry the typing end to end: P is the request model's constructor signature (so calls are checked like constructing the model), ReqT the request model and RespT the decoded response type.

Parameters:

Name Type Description Default
method str

The HTTP method; any case.

required
path str

The path, relative to the client's base_url, with {name} placeholders filled from request-model fields.

required
request Callable[P, ReqT] | None

The request model: a pydantic BaseModel (or RootModel) subclass. None for routes that take no arguments.

None
response Any

The response type: anything pydantic's TypeAdapter accepts (a model, list[Model], dict[str, int], ...), or str, bytes, httpx.Response, or None to ignore the body.

None
headers Mapping[str, str] | None

Static headers sent with every call of this route.

None
exclude_unset bool | None

Only send fields the caller actually set. Defaults to True for PATCH and False otherwise.

None
name str | None

The route's name; defaults to the attribute name it's assigned to.

None
meta Mapping[str, Any] | None

Free-form data for your client hooks, such as {"auth": False}.

None

Attributes:

Name Type Description
method str

The upper-cased HTTP method.

path

The path template.

request

The request model class, or None.

response

The declared response type.

headers dict[str, str]

Static headers.

exclude_unset bool

Whether only explicitly set fields are sent.

name

The route's name (the attribute name on its class, once assigned).

meta dict[str, Any]

Free-form data for client hooks.

Methods:

Name Description
bind

Attach the route to a client, as accessing it on a client class does.

with_prefix

Return a copy with prefix prepended to the path.

map

Return a copy whose decoded response is passed through fn.

paginate

Make the route iterable across pages.

build_model

Construct the request model from call arguments.

encode

Work out the URL, query, headers and body for a request model.

parse

Decode a successful response into the declared type, then apply any maps.

bind

bind(client: Client) -> BoundRoute[P, ReqT, RespT]
bind(
    client: AsyncClient,
) -> AsyncBoundRoute[P, ReqT, RespT]
bind(client: Client | AsyncClient) -> Any

Attach the route to a client, as accessing it on a client class does.

For routes that aren't declared on the client's class:

get_user = nanosdk.get("/users/{user_id}", request=GetUser, response=User)
user = get_user.bind(api)(user_id=1)

Parameters:

Name Type Description Default
client Client | AsyncClient

The client to send through.

required

Returns:

Type Description
Any

A bound route: call it, or use .send(), .raw(), .build_request().

with_prefix

with_prefix(prefix: str) -> Self

Return a copy with prefix prepended to the path.

Routes groups call this for their prefix=.

Parameters:

Name Type Description Default
prefix str

Path prefix such as "/users"; slashes are normalised.

required

Returns:

Type Description
Self

A new route; this one is unchanged.

map

map(fn: Callable[[RespT], U]) -> Route[P, ReqT, U]

Return a copy whose decoded response is passed through fn.

The return type follows fn, so type checkers see the mapped type. Maps chain, and run after validation, so fn receives the declared response type.

Example
count = nanosdk.get("/posts", response=list[Post]).map(len)       # -> int
frame = nanosdk.get("/posts", response=list[Post]).map(frames.polars)

Parameters:

Name Type Description Default
fn Callable[[RespT], U]

Function of the decoded response.

required

Returns:

Type Description
Route[P, ReqT, U]

A new route; this one is unchanged.

paginate

paginate(
    next: NextPage[ReqT, list[I], I],
) -> PaginatedRoute[P, ReqT, list[I], I]
paginate(
    next: NextPage[ReqT, RespT, Any],
    *,
    items: Callable[[RespT], Iterable[I]]
) -> PaginatedRoute[P, ReqT, RespT, I]
paginate(
    next: Any, *, items: Any = None
) -> PaginatedRoute[Any, Any, Any, Any]

Make the route iterable across pages.

Bound to a client, the result keeps the route's normal call (the first page) and adds .iter() (every item, lazily) and .pages() (every Page).

Example
# The response is a list: its elements are the items.
by_offset = nanosdk.get("/posts", request=ListPosts, response=list[Post]).paginate(
    nanosdk.Offset("offset", limit="limit")
)
# The response wraps the list: say where the items are.
feed = nanosdk.get("/feed", request=Feed, response=FeedPage).paginate(
    nanosdk.Cursor("next_cursor"), items=lambda page: page.data
)

Parameters:

Name Type Description Default
next Any

The pagination strategy: Cursor, Offset, PageNumber, LinkHeader, or any function of the current Page returning the next request model, an httpx.URL, or None to stop.

required
items Any

Extracts the item list from a decoded page. Optional when the response type is already a list.

None

Returns:

Type Description
PaginatedRoute[Any, Any, Any, Any]

A paginated route wrapping this one.

build_model

build_model(*args: P.args, **kwargs: P.kwargs) -> ReqT

Construct the request model from call arguments.

Raises:

Type Description
ValidationError

If the arguments don't validate.

TypeError

If the route has no request model but arguments were given.

encode

encode(model: ReqT) -> EncodedRequest

Work out the URL, query, headers and body for a request model.

Parameters:

Name Type Description Default
model ReqT

An instance of the route's request model (None if it has none).

required

Returns:

Type Description
EncodedRequest

The encoded request, before client-level headers and auth are added.

Raises:

Type Description
TypeError

If the model's fields can't fill the route's path placeholders, or the Body() / Json() markers conflict.

parse

parse(response: Response) -> RespT

Decode a successful response into the declared type, then apply any maps.

Parameters:

Name Type Description Default
response Response

A 2xx response.

required

Returns:

Type Description
RespT

The decoded (and mapped) value.

Raises:

Type Description
ResponseValidationError

If the body doesn't match the response type.

PaginatedRoute

PaginatedRoute(
    route: Route[P, ReqT, RespT],
    *,
    items: Callable[[RespT], Iterable[ItemT]],
    next: NextPage[ReqT, RespT, ItemT]
)

A route plus the rules for walking its pages.

Made by Route.paginate.

Bound to a client it becomes a BoundPaginatedRoute (or the async twin): calling it still fetches just the first page, and .iter(...) / .pages(...) walk all of them. ItemT is the type of each item.

Parameters:

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

The underlying route, which fetches one page.

required
items Callable[[RespT], Iterable[ItemT]]

Extracts the item list from a decoded page.

required
next NextPage[ReqT, RespT, ItemT]

The strategy that picks the next page's request.

required

Attributes:

Name Type Description
route

The underlying route.

items

Extracts the item list from a decoded page.

next_page

The strategy that picks the next page's request.

Methods:

Name Description
bind

Attach the paginated route to a client: route.bind(api).iter(...).

with_prefix

Return a copy whose underlying route has prefix prepended to its path.

make_page

Decode a response into a Page.

bind

bind(
    client: Client,
) -> BoundPaginatedRoute[P, ReqT, RespT, ItemT]
bind(
    client: AsyncClient,
) -> AsyncBoundPaginatedRoute[P, ReqT, RespT, ItemT]
bind(client: Client | AsyncClient) -> Any

Attach the paginated route to a client: route.bind(api).iter(...).

Parameters:

Name Type Description Default
client Client | AsyncClient

The client to send through.

required

Returns:

Type Description
Any

A bound paginated route.

with_prefix

with_prefix(prefix: str) -> Self

Return a copy whose underlying route has prefix prepended to its path.

make_page

make_page(
    request: ReqT, response: Response
) -> Page[ReqT, RespT, ItemT]

Decode a response into a Page.

Parameters:

Name Type Description Default
request ReqT

The request model that produced response.

required
response Response

A successful response.

required

Returns:

Type Description
Page[ReqT, RespT, ItemT]

The page, with its items extracted.

Routes

Base for a reusable group of routes, optionally under a common path prefix.

Mix groups into a Client and an AsyncClient to share one set of route declarations between a sync and an async SDK.

Example
class Users(nanosdk.Routes, prefix="/users"):
    get = nanosdk.get("/{user_id}", request=GetUser, response=User)  # /users/{user_id}

class Api(Users, Billing, nanosdk.Client): ...
class AsyncApi(Users, Billing, nanosdk.AsyncClient): ...

Pass prefix= as a class keyword to prepend it to every route declared in the class body.