Skip to content

Operation

Methods decorated with one of @get, @post, @put, etc. are transformed into operation methods. Invoking these methods initiates an HTTP request-response cycle. Lapidary is designed to be compatible with the HTTP methods defined in OpenAPI 3.x, which include all methods defined in RFC 9110, with the exception of CONNECT. Methods in your client that aren't decorated with these operation decorators are simply ignored.

Note

Python methods and HTTP methods represent two distinct concepts.

Throughout this documentation, the term method in a programming context always refers to a Python method (defined with def), whereas HTTP methods (GET, POST, etc.) are specified as such.

from lapidary.runtime import ClientBase, get


class CatClient(ClientBase):

    @get('/cats')
    async def list_cats(...):
        pass

Note

In the examples below, methods are depicted as standalone functions, with the encapsulating class structure omitted.

@get('/cats')  # method and path
async def list_cats(...):
    pass

Parameters

Parameters within Lapidary are designed to represent different components of an HTTP request, including headers, cookies, query parameters, path parameters, and the body of the request.

It's essential that every parameter, including self, is annotated to define its role and type explicitly. Note that * args and **kwargs are not supported in this structure to maintain clarity and specificity in request definition.

Query parameters

Query parameters are elements added to the URL following the '?' character, serving to modify or refine the request. An example format is https://example.com/path?param=value.

To declare a query parameter in Lapidary, use the Query() annotation:

@get('/cats')
async def list_cats(
        self: Self,
        color: Annotated[str, Query],
):
    pass

Calling a method like this:

await client.list_cats(color='black')

results in a GET request being sent to the following URL: https://example.com/cats?color=black.

This illustrates how arguments passed to the method are directly mapped to query parameters in the URL, forming a complete HTTP request based on the method's decoration and the parameters' annotations.

Path parameters

Path parameters are variables embedded within the path of a request URL, such as http://example.com/cat/{cat_id}. These parameters are essential for accessing specific resources or performing operations on them.

To define a path parameter in Lapidary, you use the path variable inside the decorator URL path and annotate the method parameter with Path(). Here is an example of how to define and use a path parameter:

@get('/cat/{cat_id}')
async def get_cat(
        self: Self,
        cat_id: Annotated[str, Path],
):
    pass

When you call this method like so:

await client.get_cat(cat_id=1)

it constructs and sends a GET request to https://example.com/cat/1. This demonstrates the method's ability to dynamically incorporate the provided argument (cat_id=1) into the request URL as a path parameter.

Headers

Header parameters are utilized to add HTTP headers to a request. These can be defined using the Header annotation in a method declaration, specifying the header name and the expected value type.

Example:

@get('/cats')
async def list_cats(
        self: Self,
        version: Annotated[str, Header],
):
    pass

Invoking this method with:

await client.list_cats(version='2')

results in the execution of a GET request that includes the header version: 2.

Note: The Cookie, Header, Param, and Query annotations all accept parameters such as name, style, and explode as defined by OpenAPI.

To add a cookie to the request, you use the Cookie parameter. This adds a name=value pair to the Cookie header of the HTTP request.

Example:

@get('/cats')
async def list_cats(
        self: Self,
        cookie_key: Annotated[str, Cookie('key')],
):
    pass

Calling this method as

await client.list_cats(cookie_key='value')

will send a GET request that includes the header Cookie: key=value.

Request body

To mark a parameter for serialization into the HTTP body, annotate it with RequestBody. Each method can include only one such parameter.

Example:

@POST('/cat')
async def add_cat(
        self: Self,
        cat: Annotated[Cat, Body({
            'application/json': Cat,
        })],
):
    pass

The parameter type, such as Cat in this example, should be a basic scalar (e.g., str, int, float, date, datetime, UUID) or a Pydantic model, to facilitate proper serialization.

Invoking this method constructs a POST request with Content-Type: application/json header. The cat object is serialized to JSON using Pydantic's BaseModel.model_dump_json() and included in the body of the request.

Return type

The Responses annotation plays a crucial role in mapping HTTP status codes and Content-Type headers to specific return types. This mechanism allows developers to define how responses are parsed and returned.

The return type is specified in two places:

  1. At the method signature level - The declared return type here should reflect the expected successful response structure. It can be a single type or a Union of types, accommodating various potential non-error response bodies.

  2. Within the Responses annotation - This details the specific type or types to be used for parsing the response body, depending on the response's HTTP status code and content type matching those specified.

Note

The type hint in the method's annotation must match the response types specified within the Responses() annotation, excluding exception types. For details on how to handle exception types, see the next section.

Example:

@get('/cat')
async def list_cats(self: Self) -> Annotated[
    tuple[List[Cat], None],
    Responses({
        '2XX': Response(Body({
            'application/json': List[Cat],
        })),
    })
]:
    pass

In this setup, the Responses dictionary specifies that for responses with a 2XX status code and a Content-Type of application/json, the response body will be parsed as a list of Cat objects. This explicit declaration ensures that the method's return type is tightly coupled with the anticipated successful response structure, providing clarity and type safety for API interactions.

Mapping headers and response status code

Lapidary operation methods always return a tuple. The first element is the response body, the second is the response metadata (headers and/or status code), each of them being optional.

Example:


class CatListMeta(ModelBase):
   total_count: Annotated[int, Header('Total-Count')]
   status_code: Annotated[int, StatusCode]


class CatClient(ClientBase):
   @get('/cat')
   async def list_cats(self: Self) -> Annotated[
       tuple[list[Cat], CatListMeta],
       Responses({
           '2XX': Response(
                Body({
                    'application/json': list[Cat],
                }),
                CatListMeta
           ),
       })
   ]:
       pass

client = CatClient()
cats_body, cats_meta = await client.list_cats()
assert cats_body.body == [Cat(...)]
assert cats_meta.count == 1
assert cats_meta.status_code == 200

Handling error responses

Lapidary maps HTTP error responses to exceptions.

class ErrorModel(ModelBase):
    error_code: int
    error_message: str


@get('/cat')
async def list_cats(
        self: Self,
) -> Annotated[
    tuple[List[Cat], None],
    Responses({
        '2XX': Response(...),
        '4XX': Response(Body({
            'application/json': ErrorModel,
        }))
    }),
]:
    pass

Responses with status code 400 and up will cause HttpErrorResponse to be risen as long as they're declared in the response map.

try:
    await client.list_cats()
except HttpErrorResponse as e:
    assert e.status_code == 400
    assert e.headers is None
    assert isinstance(e.body, ErrorModel)

Any responses not declared in the response map, regardless of their status code, raise UnexpectedResponse.

try:
    await client.list_cats()
except UnexpectedResponse as e:
    assert isinstance(e.response, httpx.response)

Note

Exception types mapped to responses in the Responses annotation should not be included in the method's return type hint. They are exclusively declared within the Responses framework for appropriate processing.