From 2897dabffb784d33d313e16d6db4b42e99a3238a Mon Sep 17 00:00:00 2001 From: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com> Date: Sun, 14 Jan 2024 21:58:30 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Describe=20operation=20methods.?= =?UTF-8?q?=20Reorganize=20documentation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 12 + docs/operation.md | 214 ++++++++++++++++++ ...on_representation.md => representation.md} | 15 +- 3 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 docs/index.md create mode 100644 docs/operation.md rename docs/{python_representation.md => representation.md} (77%) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..f8dd14d --- /dev/null +++ b/docs/index.md @@ -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. diff --git a/docs/operation.md b/docs/operation.md new file mode 100644 index 0000000..3dfb894 --- /dev/null +++ b/docs/operation.md @@ -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. diff --git a/docs/python_representation.md b/docs/representation.md similarity index 77% rename from docs/python_representation.md rename to docs/representation.md index 19b79b3..d10ce7a 100644 --- a/docs/python_representation.md +++ b/docs/representation.md @@ -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. @@ -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, @@ -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 ```