Skip to content

Commit

Permalink
πŸ“ Describe operation methods. Reorganize documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Krupinski committed Jan 14, 2024
1 parent 5660389 commit 2897dab
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 7 deletions.
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Lapidary

Lapidary consists of a pair of packages:

- lapidary (this project), a library that helps you write Web API clients,
- [lapidary-render](https://github.com/python-lapidary/lapidary-render/), a generator that consumes OpenAPI documents and produces API clients (that use Lapidary).

Both projects are coupled, in the sense that
lapidary-render produces output compatible with Lapidary, and
Lapidary supports the same features of OpenAPI Specification as lapidary-render.

See [this test](https://github.com/python-lapidary/lapidary/blob/develop/tests/test_client.py) for a small showcase.
214 changes: 214 additions & 0 deletions docs/operation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
In Lapidary, every method in the client class represents a single API operation on a path.

Compared to OpenAPI Operation, it has the same features, except more than one method can invoke the same method and path, for example to bind input parameters with output type.

# HTTP method and path

Every method in the client class, decorated with one of the decorators named after standard HTTP methods (GET, PUT, POST, etc., except CONNECT) becomes an operation method.

Other methods are ignored by Lapidary.

The decorator accepts path as the only parameter.

Note: in the examples below all functions are actually methods, with the encapsulating class omitted.

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

Other methods can be used with decorator `@Operation(method, path)`

# Parameters

Each parameter can either represent a single part of a HTTP request: header, cookie, query parameter, path parameter or body; or can be an instance of `httpx.Auth`.

Every parameter must be annotated, including `self`.

`*args` and `**kwargs` are not supported.

## Query parameters

Query parameters are appended to the URL after '?' character, e.g. `https://example.com/path?param=value`.

Query parameters are declared using `Query()` annotation:

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

Calling this method as

```python
client.list_cats(color='black')
```

will results in making a GET request to `https://example.com/cats?color=black`

## Path parameters

Path parameters are variables in the requests path: `http://example.com/cat/{cat_id}`.

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

Calling this method as

```python
client.get_cat(cat_id=1)
```

will result in making a GET request to 'https://example.com/cat/1'.

## Header parameters

Header parameter will simply add a HTTP header to the request.

```python
@GET('/cats')
async def list_cats(
self: Self,
accept: Annotated[str, Header('Accept')],
):
pass
```

Calling this method as

```python
client.list_cats(accept='application/json')
```

will result in making a GET request with added header `Accept: application/json`.

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

## Cookie parameters

Cookie parameter will add a `name=value` to the `Cookie` header of the request.

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

Calling this method as

```python
client.list_cats(cookie_key='value')
```

will result in making a GET request with added header `Cookie: key=value`.

## Request body

A Parameter annotated with `RequestBody` will be serialized as the HTTP requests body. At most one parameter can be a request body.

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

The type (here `Cat`) must be either a supported scalar value (str, int, float, date, datetime, or UUID) or a Pydantic model.

Calling this method will result in making a HTTP request with header `Content-Type: application/json` and the `cat` object serialized with Pydantics `BaseModel.model_dump_json()` as the request body.

# Return type

The `Responses` annotation maps `status code` template and `Content-Type` response header to a type,
and optionally allows converting the response body to an instance of `httpx.Auth`
class.

```python
@GET('/cat')
async def list_cats(
self: Self,
cat: Annotated[Cat, RequestBody({
'application/json': Cat,
})],
) -> Annotated[
List[Cat],
Responses({
'2XX': {
'application/json': List[Cat],
},
})
]:
pass
```

After a HTTP response is received, Lapidary checks the status code templates (here `2XX`) and the `Content-Type` header to find the expected python type, and use it to parse the response body.

## Login operations

Operation methods can accept one or more auth parameters.

```python
@GET('/cat')
async def list_cats(
self: Self,
cat: Annotated[Cat, RequestBody({
'application/json': Cat,
})],
auth: Auth,
) -> Annotated[
List[Cat],
Responses({
'2XX': {
'application/json': List[Cat],
},
})
]:
pass
```

Operation methods can also return Auth objects.

```python
@POST('/login')
async def login(
self: Self,
body: Annotated[LoginRequest, RequestBody()],
) -> Annotated[
Auth,
Responses({
'2XX': {
'application/json': Annotated[
LoginResponse,
APIKeyAuth(
'header',
'Authorization',
'Token {body.api_key}'
),
],
},
})
]:
pass
```

In this case, an additional `APIKeyAuth` annotation is used. it parses the response and returns `httpx.Auth` instance, which can be used as a parameter value for a call to `list_cats()`.

If any type in the Responses map is a subclass of `Exception`, it will be raised instead of being returned.
15 changes: 8 additions & 7 deletions docs/python_representation.md β†’ docs/representation.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Python code representation of OpenAPI document
# Python representation of a remote Web API

The goal of python representation is to allow python code to fully replace OpanAPI document, allow Lapidary library to prepare a HTTP request and parse the response, in a way compatible with the
server describe by that OpenAPI document.
server described by that representation.

The python representation must be fully sufficient, must not require the original document to function, and usage must execute a valid exchange with a server.
The python representation is fully self-sufficient, and doesn't require an OpenAPI document at runtime.

## Client

A client to a remote service may be represented as a collection of functions, each representing an operation, and a collection of data classes, each representing a schema.
The functions could be module-level or grouped in one or more classes, optionally forming an object hierarchy.

Lapidary implements a single class approach, but supporting all three styles at later stage should be simple.
Lapidary currently implements a single class approach.

## Paths and Operations

OpenAPI paths can be explained as a mapping of HTTP method and path to an Operation declaration.
OpenAPI `paths` object can be explained as a mapping of HTTP method and path to an Operation declaration.

OpenAPI Operation specifies the request body, parameters and responses, so it's the closest thing to a python function.

Expand All @@ -24,7 +24,7 @@ OpenAPI Operation specifies the request body, parameters and responses, so it's

Each of cookie, header, path and query parameters have their own namespace, so they can't be directly mapped to names of parameters of a python function.

The possible work-arounds are:
The possible workarounds are:

- grouping parameters in mappings, named tuples or objects,
- name mangling,
Expand All @@ -34,13 +34,14 @@ Lapidary uses annotations, which declares parameter location and may be used to

```python
from typing import Annotated, Self
from lapidary.runtime import Cookie, ClientBase
from lapidary.runtime import Cookie, ClientBase, Header

class Client(ClientBase):
async def operation(
self: Self,
*,
param1_c: Annotated[str, Cookie('param1')],
param1_h: Annotated[str, Header('param1')],
):
pass
```
Expand Down

0 comments on commit 2897dab

Please sign in to comment.