Skip to content

Commit f4b87d7

Browse files
author
Raphael Krupinski
committed
📝 Update documentation.
1 parent a14b495 commit f4b87d7

File tree

4 files changed

+183
-191
lines changed

4 files changed

+183
-191
lines changed

Readme.md

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Python DSL for Web API clients.
66

77
Also check [lapidary-render](https://github.com/python-lapidary/lapidary-render/),
8-
a command line program that generates Lapidary client code from OpenAPI documents.
8+
a command line program that generates Lapidary clients from OpenAPI.
99

1010
## Features
1111

@@ -20,65 +20,57 @@ pip install lapidary
2020
```
2121

2222
or with Poetry
23+
2324
```console
2425
poetry add lapidary
2526
```
2627

2728
## Usage
2829

30+
With Lapidary, you define an API client by creating a class that mirrors the API structure, akin to OpenAPI but through
31+
decorated and annotated Python methods. Calling these method handles making HTTP requests and transforming the responses
32+
back into Python objects.
33+
2934
```python
30-
import dataclasses as dc
3135
from typing import Annotated, Self
32-
33-
import pydantic
34-
35-
from lapidary.runtime import ClientBase, ParamStyle, Path, Responses, get
36-
36+
from lapidary.runtime import *
3737

3838
# Define models
39-
# Pydantic models are recommended, but classes cannot inherit from both BaseModel and Exception.
4039

41-
@dc.dataclass
42-
class ServerError(Exception):
43-
msg: str
44-
45-
46-
class Cat(pydantic.BaseModel):
40+
class Cat(ModelBase):
4741
id: int
4842
name: str
4943

5044
# Declare the client
5145

5246
class CatClient(ClientBase):
5347
def __init__(
54-
self,
55-
base_url = 'http://localhost:8080/api',
48+
self,
49+
base_url='http://localhost:8080/api',
5650
):
5751
super().__init__(base_url=base_url)
5852

59-
# Parameters are interpreted according to their annotation.
60-
# Response is parsed according to the return type annotation.
61-
6253
@get('/cat/{id}')
6354
async def cat_get(
64-
self: Self,
65-
*,
66-
id: Annotated[int, Path(style=ParamStyle.simple)],
55+
self: Self,
56+
*,
57+
id: Annotated[int, Path(style=ParamStyle.simple)],
6758
) -> Annotated[Cat, Responses({
6859
'2XX': {
6960
'application/json': Cat
7061
},
71-
'4XX': {
72-
'application/json': ServerError
73-
},
7462
})]:
7563
pass
7664

77-
client = CatClient()
78-
cat = await client.cat_get(id=7)
65+
# User code
66+
67+
async def main():
68+
client = CatClient()
69+
cat = await client.cat_get(id=7)
7970
```
8071

81-
See [this test file](https://github.com/python-lapidary/lapidary/blob/develop/tests/test_client.py) for a working example.
72+
See [this test file](https://github.com/python-lapidary/lapidary/blob/develop/tests/test_client.py) for a working
73+
example.
8274

8375
[Full documentation](https://lapidary.dev)
8476

docs/usage/auth.md

Lines changed: 75 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,101 @@
11
# Authentication
22

3-
OpenAPI allows declaring security schemes and security requirements of operations.
3+
## Model
44

5-
Lapidary allows declaring methods that create or consume `httpx.Auth` objects.
5+
Lapidary allows API client authors to declare security requirements using maps that specify acceptable security schemes
6+
per request. OAuth2 schemes require specifying scopes, while other schemes use an empty list.
67

7-
## Basic auth
8+
For example, a call might require authentication with either two specific schemes together (auth1 and auth2) or another
9+
pair (auth3 and auth4):
810

9-
TODO
11+
```python
12+
security = [
13+
{
14+
'auth1': ['scope1'],
15+
'auth2': ['scope1', 'scope2'],
16+
},
17+
{
18+
'auth3': ['scope3'],
19+
'auth4': [],
20+
},
21+
]
22+
```
23+
24+
Lapidary also supports optional authentication, allowing for certain operations, such as a login endpoint, to forego the
25+
global security requirements specified for the API:
26+
27+
```python
28+
security = [
29+
{'auth': []},
30+
{}, # unauthenticated calls allowed
31+
]
32+
```
1033

11-
## Login endpoints
34+
You can also use this method to disable global security requirement for a particular operation (e.g. login endpoint).
1235

13-
A `/login/` or `/authenticate/` endpoint that returns the token is quite common with simpler authentication schemes like http or apiKey, yet their support is poor in OpenAPI. There's no way to connect
14-
such endpoint to a security scheme as in the case of OIDC.
36+
## Usage
1537

16-
A function that handles such an endpoint can declare that it returns an Auth object, but it's not obvious to the user of python API which security scheme the method returns.
38+
Lapidary handles security schemes through httpx.Auth instances, wrapped in `NamedAuth` tuple.
39+
You can define security requirements globally in the client `__init__()` or at the operation level with decorators, where
40+
operation-level declarations override global settings.
41+
42+
Lapidary validates security requirements at runtime, ensuring that any method call is accompanied by the necessary
43+
authentication, as specified by its security requirements. This process involves matching the provided authentication
44+
against the declared requirements before proceeding with the request. To meet these requirements, the user must have
45+
previously configured the necessary Auth instances using lapidary_authenticate.
1746

1847
```python
19-
from httpx import Auth
20-
import pydantic
21-
from typing_extensions import Annotated, Self
22-
23-
from lapidary.runtime import post, ClientBase, RequestBody, APIKeyAuth, Responses
24-
25-
26-
class LoginRequest(pydantic.BaseModel):
27-
...
28-
29-
30-
class LoginResponse(pydantic.BaseModel):
31-
token: str
32-
33-
34-
class Client(ClientBase):
35-
@post('/login')
36-
def login(
37-
self: Self,
38-
*,
39-
body: Annotated[LoginRequest, RequestBody({'application/json': LoginRequest})],
40-
) -> Annotated[
41-
Auth,
42-
Responses({
43-
'200': {
44-
'application/json': Annotated[
45-
LoginResponse,
46-
APIKeyAuth(
47-
in_='header',
48-
name='Authorization',
49-
format='Token {body.token}'
50-
),
51-
]
52-
}
53-
}),
54-
]:
55-
"""Authenticates with the "primary" security scheme"""
56-
```
48+
from lapidary.runtime import *
49+
from lapidary.runtime.auth import HeaderApiKey
50+
from typing import Self, Annotated
5751

58-
The top return Annotated declares the returned type, the inner one declares the processing steps for the actual response.
59-
First the response is parsed as LoginResponse, then that object is passed to ApiKeyAuth which is a callable object.
6052

61-
The result of the call, in this case an Auth object, is returned by the `login` function.
53+
class MyClient(ClientBase):
54+
def __init__(self):
55+
super().__init__(
56+
base_url=...,
57+
security=[{'apiKeyAuth': []}],
58+
)
6259

63-
The innermost Annotated is not necessary from the python syntax standpoint. It's done this way since it kind of matches the semantics of Annotated, but it could be replaced with a simple tuple or other type in the future.
60+
@get('/api/operation', security=[{'admin_only': []}])
61+
async def my_op(self: Self) -> ...:
62+
pass
63+
64+
@post('/api/login', security=())
65+
async def login(
66+
self: Self,
67+
user: Annotated[str, ...],
68+
password: Annotated[str, ...],
69+
) -> ...:
70+
pass
6471

65-
## Using auth tokens
72+
# User code
73+
async def main():
74+
client = MyClient()
6675

67-
OpenApi allows operations to declare a collection of alternative groups of security requirements.
76+
token = await client.login().token
77+
client.lapidary_authenticate(apiKeyAuth=HeaderApiKey(token))
78+
await client.my_op()
6879

69-
The second most trivial example (the first being no security) is a single required security scheme.
70-
```yaml
71-
security:
72-
- primary: []
80+
# optionally
81+
client.lapidary_deauthenticate('apiKeyAuth')
7382
```
74-
The name of the security scheme corresponds to a key in `components.securitySchemes` object.
7583

76-
This can be represented as a simple parameter, named for example `primary_auth` and of type `httpx.Auth`.
77-
The parameter could be annotated as `Optional` if the security requirement is optional for the operation.
84+
`lapidary_authenticate` also accepts tuples of Auth instances with names, so this is possible:
7885

79-
In case of multiple alternative groups of security requirements, it gets harder to properly describe which schemes are required and in what combination.
86+
```python
87+
def admin_auth(api_key: str) -> NamedAuth:
88+
return 'admin_only', HeaderApiKey(api_key)
8089

81-
Lapidary takes all passed `httpx.Auth` parameters and passes them to `httpx.AsyncClient.send(..., auth=auth_params)`, leaving the responsibility to select the right ones to the user.
8290

83-
If multiple `Auth` parameters are passed, they're wrapped in `lapidary.runtime.aauth.MultiAuth`, which is just reexported `_MultiAuth` from `https_auth` package.
91+
client.lapidary_authencticate(admin_auth('my token'))
92+
```
8493

85-
#### Example
94+
## De-registering Auth instances
8695

87-
Auth object returned by the login operation declared in the above example can be used by another operation.
96+
To remove an auth instance, thereby allowing for unauthenticated calls or the use of alternative security schemes,
97+
lapidary_deauthenticate is used:
8898

8999
```python
90-
from httpx import Auth
91-
from typing import Annotated, Self
92-
93-
from lapidary.runtime import ClientBase, get, post
94-
95-
96-
class Client(ClientBase):
97-
@post('/login')
98-
def login(
99-
self: Self,
100-
body: ...,
101-
) -> Annotated[
102-
Auth,
103-
...
104-
]:
105-
"""Authenticates with the "primary" security scheme"""
106-
107-
@get('/private')
108-
def private(
109-
self: Self,
110-
*,
111-
primary_auth: Auth,
112-
):
113-
pass
100+
client.lapidary_deauthenticate('apiKeyAuth')
114101
```
115-
116-
In this example the method `client.private` can be called with the auth object returned by `client.login`.

docs/usage/client.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
# Client class
22

3-
Lapidary client is represented by a single class that holds all operation methods and encapsulates a `httpx.AsyncClient` instance.
3+
The core of the Lapidary API client is a single class that contains all the methods for API operations. This class is
4+
built around an `httpx.AsyncClient` instance to manage HTTP requests and responses.
5+
6+
Example usage:
47

58
```python
6-
import lapidary.runtime
9+
from lapidary.runtime import *
710

811

9-
class CatClient(lapidary.runtime.ClientBase):
12+
class CatClient(ClientBase):
1013
...
1114
```
1215

1316
# `__init__()` method
1417

15-
Implementing `__init__()` is optional, and provides means to pass arguments, like `base_url`, to `httpx.AsyncClient.__init__`.
18+
Implementing the `__init__()` method is optional but useful for specifying default values for settings like
19+
the `base_url` of the API.
20+
21+
Example implementation:
1622

1723
```python
1824
import lapidary.runtime
@@ -21,11 +27,12 @@ import lapidary.runtime
2127
class CatClient(lapidary.runtime.ClientBase):
2228
def __init__(
2329
self,
24-
base_url='https://example.com/api'
30+
base_url='https://example.com/api',
31+
**kwargs
2532
):
2633
super().__init__(
2734
base_url=base_url,
35+
**kwargs
2836
)
29-
3037
...
3138
```

0 commit comments

Comments
 (0)