rapid-api-client 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/PKG-INFO +27 -12
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/README.md +26 -12
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/pyproject.toml +3 -1
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/rapid_api_client/__init__.py +4 -0
- rapid_api_client-0.3.2/rapid_api_client/client.py +218 -0
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/rapid_api_client/model.py +54 -11
- rapid_api_client-0.3.2/rapid_api_client/py.typed +0 -0
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/rapid_api_client/utils.py +10 -0
- rapid_api_client-0.3.0/rapid_api_client/client.py +0 -145
- {rapid_api_client-0.3.0 → rapid_api_client-0.3.2}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rapid-api-client
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Rapidly develop your API clients using decorators and annotations
|
|
5
5
|
Author: Sébastien MB
|
|
6
6
|
Author-email: seb@essembeh.org
|
|
@@ -38,10 +38,15 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# Rapid Api Client
|
|
40
40
|
|
|
41
|
-
Library to rapidly develop
|
|
41
|
+
Library to **rapidly** develop *API clients* in Python, based on [Pydantic](https://docs.pydantic.dev/) and [Httpx](https://www.python-httpx.org/), using almost only *decorators* and *annotations*.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
✨ Main features:
|
|
44
|
+
- ✏️ You don't write any code, you only declare the endpoints using *decorators* and *annotations*.
|
|
45
|
+
- 🚚 Support *Pydantic* to automatically parse and validate reponses (and also for posting content).
|
|
46
|
+
- 🏗️ Does not reimplement the low-level http-related logic, it simply uses `httpx.AsyncClient` like you would do and you can customize it.
|
|
47
|
+
- ⚡️ Asynchronous, because `httpx` and `asyncio` are just amazingly fast.
|
|
44
48
|
|
|
49
|
+
🙏 This project is inspired by [FastAPI](https://fastapi.tiangolo.com/), I always wanted a library to create an API client that is as simple as *FastAPI* for handling the server-side part.
|
|
45
50
|
|
|
46
51
|
# Usage
|
|
47
52
|
|
|
@@ -51,14 +56,17 @@ Install the project
|
|
|
51
56
|
pip install rapid-api-client
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
Declare your API using decorators and annotations
|
|
59
|
+
Declare your API endpoints using decorators and annotations, **the method does not need any code, it will be generated by the decorator**, just write `...` or `pass` or whatever, it won't be called anyway 🙈.
|
|
55
60
|
|
|
56
61
|
```python
|
|
57
62
|
class GithubIssuesApi(RapidApi):
|
|
58
63
|
|
|
59
|
-
@get("/repos/{owner}/{repo}/issues", response_class=
|
|
64
|
+
@get("/repos/{owner}/{repo}/issues", response_class=TypeAdapter(List[Issue]))
|
|
60
65
|
async def list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
61
66
|
|
|
67
|
+
@get("/repos/{owner}/{repo}/releases", response_class=TypeAdapter(List[Release]))
|
|
68
|
+
async def list_releases(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
69
|
+
|
|
62
70
|
```
|
|
63
71
|
|
|
64
72
|
Use it
|
|
@@ -66,7 +74,7 @@ Use it
|
|
|
66
74
|
```python
|
|
67
75
|
api = GithubIssuesApi(client)
|
|
68
76
|
issues = await api.list_issues("essembeh", "rapid-api-client", state="closed")
|
|
69
|
-
for issue in issues
|
|
77
|
+
for issue in issues:
|
|
70
78
|
print(f"Issue: {issue.title} [{issue.url}]")
|
|
71
79
|
```
|
|
72
80
|
|
|
@@ -79,13 +87,13 @@ Any HTTP method can be used with `http` decorator
|
|
|
79
87
|
```python
|
|
80
88
|
class MyApi(RapidApi)
|
|
81
89
|
|
|
82
|
-
@http("/anything")
|
|
90
|
+
@http("GET", "/anything")
|
|
83
91
|
async def get(self): ...
|
|
84
92
|
|
|
85
|
-
@http("
|
|
93
|
+
@http("POST", "/anything")
|
|
86
94
|
async def post(self): ...
|
|
87
95
|
|
|
88
|
-
@http("
|
|
96
|
+
@http("DELETE", "/anything")
|
|
89
97
|
async def delete(self): ...
|
|
90
98
|
```
|
|
91
99
|
|
|
@@ -101,7 +109,7 @@ class MyApi(RapidApi)
|
|
|
101
109
|
async def post(self): ...
|
|
102
110
|
|
|
103
111
|
@delete("/anything")
|
|
104
|
-
async
|
|
112
|
+
async def delete(self): ...
|
|
105
113
|
```
|
|
106
114
|
|
|
107
115
|
|
|
@@ -109,9 +117,15 @@ class MyApi(RapidApi)
|
|
|
109
117
|
|
|
110
118
|
By default methods return a `httpx.Response` object and the http return code is not tested (you have to call `resp.raise_for_status()` if you need to ensure the response is OK).
|
|
111
119
|
|
|
112
|
-
But you can also specify a
|
|
120
|
+
But you can also specify a class so that the response is parsed, you can use:
|
|
121
|
+
- `httpx.Response` to get the response itself, this is the default behavior
|
|
122
|
+
- `str` to get the `response.text`
|
|
123
|
+
- `bytes` to get the `response.content`
|
|
124
|
+
- Any *Pydantic* model class (subclass of `BaseModel`), the *json* will be automatically validated
|
|
125
|
+
- Any *Pydantic-xml* model class (subclass of `BaseXmlModel`), the *xml* will be automatically validated
|
|
126
|
+
- Any `TypeAdapter` to parse the *json*, see [pydantic doc](https://docs.pydantic.dev/latest/api/type_adapter/)
|
|
113
127
|
|
|
114
|
-
> Note: When
|
|
128
|
+
> Note: When `response_class` is given (and is not `httpx.Response`), the `raise_for_status()` is always called to ensure the http response is OK
|
|
115
129
|
|
|
116
130
|
```python
|
|
117
131
|
class User(BaseModel): ...
|
|
@@ -231,3 +245,4 @@ class MyApi(RapidApi)
|
|
|
231
245
|
# Examples
|
|
232
246
|
|
|
233
247
|
See [example directory](./examples/) for some examples
|
|
248
|
+
|
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
# Rapid Api Client
|
|
8
8
|
|
|
9
|
-
Library to rapidly develop
|
|
9
|
+
Library to **rapidly** develop *API clients* in Python, based on [Pydantic](https://docs.pydantic.dev/) and [Httpx](https://www.python-httpx.org/), using almost only *decorators* and *annotations*.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
✨ Main features:
|
|
12
|
+
- ✏️ You don't write any code, you only declare the endpoints using *decorators* and *annotations*.
|
|
13
|
+
- 🚚 Support *Pydantic* to automatically parse and validate reponses (and also for posting content).
|
|
14
|
+
- 🏗️ Does not reimplement the low-level http-related logic, it simply uses `httpx.AsyncClient` like you would do and you can customize it.
|
|
15
|
+
- ⚡️ Asynchronous, because `httpx` and `asyncio` are just amazingly fast.
|
|
12
16
|
|
|
17
|
+
🙏 This project is inspired by [FastAPI](https://fastapi.tiangolo.com/), I always wanted a library to create an API client that is as simple as *FastAPI* for handling the server-side part.
|
|
13
18
|
|
|
14
19
|
# Usage
|
|
15
20
|
|
|
@@ -19,14 +24,17 @@ Install the project
|
|
|
19
24
|
pip install rapid-api-client
|
|
20
25
|
```
|
|
21
26
|
|
|
22
|
-
Declare your API using decorators and annotations
|
|
27
|
+
Declare your API endpoints using decorators and annotations, **the method does not need any code, it will be generated by the decorator**, just write `...` or `pass` or whatever, it won't be called anyway 🙈.
|
|
23
28
|
|
|
24
29
|
```python
|
|
25
30
|
class GithubIssuesApi(RapidApi):
|
|
26
31
|
|
|
27
|
-
@get("/repos/{owner}/{repo}/issues", response_class=
|
|
32
|
+
@get("/repos/{owner}/{repo}/issues", response_class=TypeAdapter(List[Issue]))
|
|
28
33
|
async def list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
29
34
|
|
|
35
|
+
@get("/repos/{owner}/{repo}/releases", response_class=TypeAdapter(List[Release]))
|
|
36
|
+
async def list_releases(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
37
|
+
|
|
30
38
|
```
|
|
31
39
|
|
|
32
40
|
Use it
|
|
@@ -34,7 +42,7 @@ Use it
|
|
|
34
42
|
```python
|
|
35
43
|
api = GithubIssuesApi(client)
|
|
36
44
|
issues = await api.list_issues("essembeh", "rapid-api-client", state="closed")
|
|
37
|
-
for issue in issues
|
|
45
|
+
for issue in issues:
|
|
38
46
|
print(f"Issue: {issue.title} [{issue.url}]")
|
|
39
47
|
```
|
|
40
48
|
|
|
@@ -47,13 +55,13 @@ Any HTTP method can be used with `http` decorator
|
|
|
47
55
|
```python
|
|
48
56
|
class MyApi(RapidApi)
|
|
49
57
|
|
|
50
|
-
@http("/anything")
|
|
58
|
+
@http("GET", "/anything")
|
|
51
59
|
async def get(self): ...
|
|
52
60
|
|
|
53
|
-
@http("
|
|
61
|
+
@http("POST", "/anything")
|
|
54
62
|
async def post(self): ...
|
|
55
63
|
|
|
56
|
-
@http("
|
|
64
|
+
@http("DELETE", "/anything")
|
|
57
65
|
async def delete(self): ...
|
|
58
66
|
```
|
|
59
67
|
|
|
@@ -69,7 +77,7 @@ class MyApi(RapidApi)
|
|
|
69
77
|
async def post(self): ...
|
|
70
78
|
|
|
71
79
|
@delete("/anything")
|
|
72
|
-
async
|
|
80
|
+
async def delete(self): ...
|
|
73
81
|
```
|
|
74
82
|
|
|
75
83
|
|
|
@@ -77,9 +85,15 @@ class MyApi(RapidApi)
|
|
|
77
85
|
|
|
78
86
|
By default methods return a `httpx.Response` object and the http return code is not tested (you have to call `resp.raise_for_status()` if you need to ensure the response is OK).
|
|
79
87
|
|
|
80
|
-
But you can also specify a
|
|
88
|
+
But you can also specify a class so that the response is parsed, you can use:
|
|
89
|
+
- `httpx.Response` to get the response itself, this is the default behavior
|
|
90
|
+
- `str` to get the `response.text`
|
|
91
|
+
- `bytes` to get the `response.content`
|
|
92
|
+
- Any *Pydantic* model class (subclass of `BaseModel`), the *json* will be automatically validated
|
|
93
|
+
- Any *Pydantic-xml* model class (subclass of `BaseXmlModel`), the *xml* will be automatically validated
|
|
94
|
+
- Any `TypeAdapter` to parse the *json*, see [pydantic doc](https://docs.pydantic.dev/latest/api/type_adapter/)
|
|
81
95
|
|
|
82
|
-
> Note: When
|
|
96
|
+
> Note: When `response_class` is given (and is not `httpx.Response`), the `raise_for_status()` is always called to ensure the http response is OK
|
|
83
97
|
|
|
84
98
|
```python
|
|
85
99
|
class User(BaseModel): ...
|
|
@@ -198,4 +212,4 @@ class MyApi(RapidApi)
|
|
|
198
212
|
|
|
199
213
|
# Examples
|
|
200
214
|
|
|
201
|
-
See [example directory](./examples/) for some examples
|
|
215
|
+
See [example directory](./examples/) for some examples
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "rapid-api-client"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Rapidly develop your API clients using decorators and annotations"
|
|
5
5
|
authors = ["Sébastien MB <seb@essembeh.org>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -44,6 +44,8 @@ pytest-asyncio = "^0.24.0"
|
|
|
44
44
|
pytest-cov = "^5.0.0"
|
|
45
45
|
pytest-dotenv = {extras = ["cli"], version = "^0.5.2"}
|
|
46
46
|
pydantic-xml = "^2.13.0"
|
|
47
|
+
mypy = "^1.11.2"
|
|
48
|
+
pylint = "^3.3.1"
|
|
47
49
|
|
|
48
50
|
[build-system]
|
|
49
51
|
requires = ["poetry-core"]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorator used to build the httpx request
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import partial, wraps
|
|
7
|
+
from inspect import Signature, signature
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Awaitable,
|
|
11
|
+
Callable,
|
|
12
|
+
Dict,
|
|
13
|
+
Mapping,
|
|
14
|
+
Self,
|
|
15
|
+
Tuple,
|
|
16
|
+
Type,
|
|
17
|
+
TypeVar,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from httpx import AsyncClient, Request, Response
|
|
22
|
+
from pydantic import BaseModel, TypeAdapter
|
|
23
|
+
from pydantic_xml import BaseXmlModel
|
|
24
|
+
|
|
25
|
+
from .model import Body, CustomParameter, FileBody, Header, Path, Query, RapidApi
|
|
26
|
+
from .utils import filter_none_values, find_annotation
|
|
27
|
+
|
|
28
|
+
BM = TypeVar("BM", bound=BaseModel)
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CustomParameters:
|
|
34
|
+
"""
|
|
35
|
+
Class containing all custom parameters used to build the request.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
path: Dict[str, Path] = field(default_factory=dict)
|
|
39
|
+
query: Dict[str, Query] = field(default_factory=dict)
|
|
40
|
+
headers: Dict[str, Header] = field(default_factory=dict)
|
|
41
|
+
body: Dict[str, Body] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_sig(cls, sig: Signature) -> Self:
|
|
45
|
+
"""
|
|
46
|
+
Iterate over parameters of given function to find annotated parameters
|
|
47
|
+
"""
|
|
48
|
+
out = cls()
|
|
49
|
+
for parameter in sig.parameters.values():
|
|
50
|
+
annot: CustomParameter | None = None
|
|
51
|
+
if (annot := find_annotation(parameter, Path)) is not None:
|
|
52
|
+
out.path[parameter.name] = annot
|
|
53
|
+
if (annot := find_annotation(parameter, Query)) is not None:
|
|
54
|
+
out.query[parameter.name] = annot
|
|
55
|
+
if (annot := find_annotation(parameter, Header)) is not None:
|
|
56
|
+
out.headers[parameter.name] = annot
|
|
57
|
+
if (annot := find_annotation(parameter, Body)) is not None:
|
|
58
|
+
out.body[parameter.name] = annot
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_request(
|
|
63
|
+
client: AsyncClient,
|
|
64
|
+
sig: Signature,
|
|
65
|
+
parameters: CustomParameters,
|
|
66
|
+
method: str,
|
|
67
|
+
path: str,
|
|
68
|
+
args: Tuple[Any],
|
|
69
|
+
kwargs: Mapping[str, Any],
|
|
70
|
+
timeout: float | None,
|
|
71
|
+
) -> Request:
|
|
72
|
+
"""
|
|
73
|
+
Build the httpx request with given custom parameters.
|
|
74
|
+
"""
|
|
75
|
+
# valuate arguments with default values
|
|
76
|
+
ba = sig.bind(*args, **kwargs)
|
|
77
|
+
ba.apply_defaults()
|
|
78
|
+
|
|
79
|
+
# resolve the api path
|
|
80
|
+
path = path.format(**{k: ba.arguments[k] for k in parameters.path})
|
|
81
|
+
|
|
82
|
+
build_kwargs: Dict[str, Any] = {}
|
|
83
|
+
|
|
84
|
+
build_kwargs["headers"] = filter_none_values(
|
|
85
|
+
{
|
|
86
|
+
annot.alias or param: ba.arguments[param]
|
|
87
|
+
for param, annot in parameters.headers.items()
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
build_kwargs["params"] = filter_none_values(
|
|
91
|
+
{
|
|
92
|
+
annot.alias or param: ba.arguments[param]
|
|
93
|
+
for param, annot in parameters.query.items()
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
for param, annot in parameters.body.items():
|
|
97
|
+
if (value := ba.arguments[param]) is not None:
|
|
98
|
+
if isinstance(annot, FileBody):
|
|
99
|
+
files = build_kwargs.setdefault("files", {})
|
|
100
|
+
files[annot.alias or param] = annot.serialize(value)
|
|
101
|
+
else:
|
|
102
|
+
build_kwargs["content"] = annot.serialize(value)
|
|
103
|
+
|
|
104
|
+
# handle extra optional kwargs
|
|
105
|
+
if timeout is not None:
|
|
106
|
+
build_kwargs["timeout"] = timeout
|
|
107
|
+
|
|
108
|
+
return client.build_request(method, path, **build_kwargs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handle_response(
|
|
112
|
+
response: Response,
|
|
113
|
+
response_class: Type[Response | str | bytes | BM] | TypeAdapter[T],
|
|
114
|
+
) -> Response | str | bytes | BM | T:
|
|
115
|
+
"""
|
|
116
|
+
Parse the response given the expected class
|
|
117
|
+
"""
|
|
118
|
+
# do not check response status code if we return the Response itself
|
|
119
|
+
if response_class is Response:
|
|
120
|
+
return response
|
|
121
|
+
# before parsing the response, check its status
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
if response_class is str:
|
|
124
|
+
return response.text
|
|
125
|
+
if response_class is bytes:
|
|
126
|
+
return response.content
|
|
127
|
+
if isinstance(response_class, TypeAdapter):
|
|
128
|
+
return response_class.validate_json(response.content)
|
|
129
|
+
if issubclass(response_class, BaseXmlModel):
|
|
130
|
+
return response_class.from_xml(response.content)
|
|
131
|
+
if issubclass(response_class, BaseModel):
|
|
132
|
+
return response_class.model_validate_json(response.content)
|
|
133
|
+
raise ValueError(f"Response class not supported: {response_class}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@overload
|
|
137
|
+
def http(
|
|
138
|
+
method: str,
|
|
139
|
+
path: str,
|
|
140
|
+
response_class: Type[Response] = Response,
|
|
141
|
+
timeout: float | None = None,
|
|
142
|
+
) -> Callable[[Callable], Callable[..., Awaitable[Response]]]: ...
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@overload
|
|
146
|
+
def http(
|
|
147
|
+
method: str,
|
|
148
|
+
path: str,
|
|
149
|
+
response_class: Type[str],
|
|
150
|
+
timeout: float | None = None,
|
|
151
|
+
) -> Callable[[Callable], Callable[..., Awaitable[str]]]: ...
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@overload
|
|
155
|
+
def http(
|
|
156
|
+
method: str,
|
|
157
|
+
path: str,
|
|
158
|
+
response_class: Type[bytes],
|
|
159
|
+
timeout: float | None = None,
|
|
160
|
+
) -> Callable[[Callable], Callable[..., Awaitable[bytes]]]: ...
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@overload
|
|
164
|
+
def http(
|
|
165
|
+
method: str,
|
|
166
|
+
path: str,
|
|
167
|
+
response_class: Type[BM],
|
|
168
|
+
timeout: float | None = None,
|
|
169
|
+
) -> Callable[[Callable], Callable[..., Awaitable[BM]]]: ...
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@overload
|
|
173
|
+
def http(
|
|
174
|
+
method: str,
|
|
175
|
+
path: str,
|
|
176
|
+
response_class: TypeAdapter[T],
|
|
177
|
+
timeout: float | None = None,
|
|
178
|
+
) -> Callable[[Callable], Callable[..., Awaitable[T]]]: ...
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def http(
|
|
182
|
+
method: str,
|
|
183
|
+
path: str,
|
|
184
|
+
response_class: Type[BM | str | bytes | Response] | TypeAdapter[T] = Response,
|
|
185
|
+
timeout: float | None = None,
|
|
186
|
+
) -> Callable[[Callable], Callable[..., Awaitable[BM | str | bytes | Response | T]]]:
|
|
187
|
+
"""
|
|
188
|
+
Main decorator used to generate an http request and return its result
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def decorator(
|
|
192
|
+
func: Callable,
|
|
193
|
+
) -> Callable[..., Awaitable[BM | str | bytes | Response | T]]:
|
|
194
|
+
sig = signature(func)
|
|
195
|
+
custom_parameters = CustomParameters.from_sig(sig)
|
|
196
|
+
|
|
197
|
+
@wraps(func)
|
|
198
|
+
async def wrapper(*args, **kwargs) -> BM | str | bytes | Response | T:
|
|
199
|
+
assert isinstance(
|
|
200
|
+
args[0], RapidApi
|
|
201
|
+
), f"{args[0]} should be an instance of RapidApi"
|
|
202
|
+
client = args[0].client
|
|
203
|
+
request = build_request(
|
|
204
|
+
client, sig, custom_parameters, method, path, args, kwargs, timeout
|
|
205
|
+
)
|
|
206
|
+
response = await client.send(request)
|
|
207
|
+
return handle_response(response, response_class)
|
|
208
|
+
|
|
209
|
+
return wrapper
|
|
210
|
+
|
|
211
|
+
return decorator
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
get = partial(http, "GET")
|
|
215
|
+
post = partial(http, "POST")
|
|
216
|
+
delete = partial(http, "DELETE")
|
|
217
|
+
put = partial(http, "PUT")
|
|
218
|
+
patch = partial(http, "PATCH")
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model classes
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
3
7
|
|
|
4
8
|
from httpx import AsyncClient
|
|
5
9
|
from pydantic import BaseModel
|
|
@@ -12,58 +16,97 @@ except ImportError: # pragma: nocover
|
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
18
|
class RapidApi:
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
"""
|
|
20
|
+
Represent an API, a RapidApi subclass should have methods decorated with @http
|
|
21
|
+
which are endpoints
|
|
22
|
+
"""
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
def update_headers(self, headers: Dict[str, str]) -> Dict[str, str]: ...
|
|
20
|
-
|
|
21
|
-
def update_query(self, query_parameters: Dict[str, str]) -> Dict[str, str]: ...
|
|
22
|
-
|
|
23
|
-
def update_path(self, path: str) -> str: ...
|
|
24
|
+
client: AsyncClient = field(default_factory=AsyncClient)
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
class CustomParameter:
|
|
27
|
+
class CustomParameter:
|
|
28
|
+
"""
|
|
29
|
+
Meta class for annotations used to customize the request build
|
|
30
|
+
"""
|
|
27
31
|
|
|
28
32
|
|
|
29
|
-
class Path(CustomParameter):
|
|
33
|
+
class Path(CustomParameter):
|
|
34
|
+
"""
|
|
35
|
+
Annotation to declare an argument used to resolve the api path/url
|
|
36
|
+
"""
|
|
30
37
|
|
|
31
38
|
|
|
32
39
|
@dataclass
|
|
33
40
|
class Query(CustomParameter):
|
|
41
|
+
"""
|
|
42
|
+
Annotation to declare an argument used as a query parameter
|
|
43
|
+
"""
|
|
44
|
+
|
|
34
45
|
alias: str | None = None
|
|
35
46
|
|
|
36
47
|
|
|
37
48
|
@dataclass
|
|
38
49
|
class Header(CustomParameter):
|
|
50
|
+
"""
|
|
51
|
+
Annotation to declare an argument used as a request header
|
|
52
|
+
"""
|
|
53
|
+
|
|
39
54
|
alias: str | None = None
|
|
40
55
|
|
|
41
56
|
|
|
42
57
|
class Body(CustomParameter):
|
|
58
|
+
"""
|
|
59
|
+
Annotation to declare an argument used as http content for post/put/...
|
|
60
|
+
"""
|
|
61
|
+
|
|
43
62
|
def serialize(self, body: Any) -> str | bytes:
|
|
63
|
+
"""
|
|
64
|
+
Serialize the annotated parameter value
|
|
65
|
+
"""
|
|
44
66
|
return body
|
|
45
67
|
|
|
46
68
|
|
|
47
69
|
@dataclass
|
|
48
70
|
class FileBody(Body):
|
|
71
|
+
"""
|
|
72
|
+
Annotation to declare an argument used as file to be uploaded
|
|
73
|
+
"""
|
|
74
|
+
|
|
49
75
|
alias: str | None = None
|
|
50
76
|
|
|
51
77
|
def serialize(self, body: Any) -> str | bytes:
|
|
78
|
+
"""
|
|
79
|
+
Serialize the annotated parameter value
|
|
80
|
+
"""
|
|
52
81
|
return body
|
|
53
82
|
|
|
54
83
|
|
|
55
84
|
@dataclass
|
|
56
85
|
class PydanticBody(Body):
|
|
86
|
+
"""
|
|
87
|
+
Annotation to declare an argument to be serialized to json and used as http content
|
|
88
|
+
"""
|
|
89
|
+
|
|
57
90
|
prettyprint: bool = False
|
|
58
91
|
|
|
59
92
|
def serialize(self, body: Any) -> str | bytes:
|
|
93
|
+
"""
|
|
94
|
+
Serialize the annotated parameter value
|
|
95
|
+
"""
|
|
60
96
|
assert isinstance(body, BaseModel)
|
|
61
97
|
return body.model_dump_json(indent=2 if self.prettyprint else None)
|
|
62
98
|
|
|
63
99
|
|
|
64
100
|
@dataclass
|
|
65
101
|
class PydanticXmlBody(Body):
|
|
102
|
+
"""
|
|
103
|
+
Annotation to declare an argument to be serialized to xml and used as http content
|
|
104
|
+
"""
|
|
105
|
+
|
|
66
106
|
def serialize(self, body: Any) -> str | bytes:
|
|
107
|
+
"""
|
|
108
|
+
Serialize the annotated parameter value
|
|
109
|
+
"""
|
|
67
110
|
assert (
|
|
68
111
|
pydantic_xml is not None
|
|
69
112
|
), "pydantic-xml must be installed to use PydanticXmlBody"
|
|
File without changes
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility methods
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from inspect import Parameter
|
|
2
6
|
from typing import Any, Dict, Type, TypeVar, get_args
|
|
3
7
|
|
|
@@ -7,10 +11,16 @@ CP = TypeVar("CP", bound=CustomParameter)
|
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
def filter_none_values(values: Dict[str, Any | None]) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Return a new map with only key/value if the value is set.
|
|
16
|
+
"""
|
|
10
17
|
return {k: v for k, v in values.items() if v is not None}
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
def find_annotation(param: Parameter, cls: Type[CP]) -> CP | None:
|
|
21
|
+
"""
|
|
22
|
+
Check if the given parameter has an annotation which is or is a subclass of given type
|
|
23
|
+
"""
|
|
14
24
|
if param.annotation:
|
|
15
25
|
for an in get_args(param.annotation):
|
|
16
26
|
if isinstance(an, cls):
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
from functools import partial, wraps
|
|
3
|
-
from inspect import Signature, signature
|
|
4
|
-
from typing import Any, Awaitable, Callable, Dict, Mapping, Self, Tuple, Type, TypeVar
|
|
5
|
-
|
|
6
|
-
from httpx import AsyncClient, Request, Response
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
-
|
|
9
|
-
from .model import (
|
|
10
|
-
Body,
|
|
11
|
-
FileBody,
|
|
12
|
-
Header,
|
|
13
|
-
Path,
|
|
14
|
-
Query,
|
|
15
|
-
RapidApi,
|
|
16
|
-
pydantic_xml,
|
|
17
|
-
)
|
|
18
|
-
from .utils import filter_none_values, find_annotation
|
|
19
|
-
|
|
20
|
-
RESP = TypeVar("RESP", bound=BaseModel | Response)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class CustomParameters:
|
|
25
|
-
path: Dict[str, Path] = field(default_factory=dict)
|
|
26
|
-
query: Dict[str, Query] = field(default_factory=dict)
|
|
27
|
-
headers: Dict[str, Header] = field(default_factory=dict)
|
|
28
|
-
body: Dict[str, Body] = field(default_factory=dict)
|
|
29
|
-
|
|
30
|
-
@classmethod
|
|
31
|
-
def from_sig(cls, signature: Signature) -> Self:
|
|
32
|
-
out = cls()
|
|
33
|
-
for parameter in signature.parameters.values():
|
|
34
|
-
if (annot := find_annotation(parameter, Path)) is not None:
|
|
35
|
-
out.path[parameter.name] = annot
|
|
36
|
-
if (annot := find_annotation(parameter, Query)) is not None:
|
|
37
|
-
out.query[parameter.name] = annot
|
|
38
|
-
if (annot := find_annotation(parameter, Header)) is not None:
|
|
39
|
-
out.headers[parameter.name] = annot
|
|
40
|
-
if (annot := find_annotation(parameter, Body)) is not None:
|
|
41
|
-
out.body[parameter.name] = annot
|
|
42
|
-
return out
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
REQUEST_BUILDER = Callable[..., Request]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def create_request(
|
|
49
|
-
client: AsyncClient,
|
|
50
|
-
request_builder: Callable[..., Request],
|
|
51
|
-
signature: Signature,
|
|
52
|
-
parameters: CustomParameters,
|
|
53
|
-
method: str,
|
|
54
|
-
path: str,
|
|
55
|
-
args: Tuple[Any],
|
|
56
|
-
kwargs: Mapping[str, Any],
|
|
57
|
-
) -> Request:
|
|
58
|
-
# valuate arguments with default values
|
|
59
|
-
ba = signature.bind(*args, **kwargs)
|
|
60
|
-
ba.apply_defaults()
|
|
61
|
-
|
|
62
|
-
# resolve the api path
|
|
63
|
-
path = path.format(**{k: ba.arguments[k] for k in parameters.path})
|
|
64
|
-
|
|
65
|
-
headers = filter_none_values(
|
|
66
|
-
{
|
|
67
|
-
annot.alias or param: ba.arguments[param]
|
|
68
|
-
for param, annot in parameters.headers.items()
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
params = filter_none_values(
|
|
73
|
-
{
|
|
74
|
-
annot.alias or param: ba.arguments[param]
|
|
75
|
-
for param, annot in parameters.query.items()
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
content = None
|
|
80
|
-
files = {}
|
|
81
|
-
for param, annot in parameters.body.items():
|
|
82
|
-
if (value := ba.arguments[param]) is not None:
|
|
83
|
-
if isinstance(annot, FileBody):
|
|
84
|
-
files[annot.alias or param] = annot.serialize(value)
|
|
85
|
-
else:
|
|
86
|
-
content = annot.serialize(value)
|
|
87
|
-
|
|
88
|
-
return request_builder(
|
|
89
|
-
client,
|
|
90
|
-
method,
|
|
91
|
-
path,
|
|
92
|
-
headers=headers,
|
|
93
|
-
params=params,
|
|
94
|
-
content=content,
|
|
95
|
-
files=files,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def http(
|
|
100
|
-
path: str,
|
|
101
|
-
method: str = "GET",
|
|
102
|
-
response_class: Type[RESP] = Response,
|
|
103
|
-
request_builder: Callable[..., Request] = AsyncClient.build_request,
|
|
104
|
-
) -> Callable[[Callable], Callable[..., Awaitable[RESP]]]:
|
|
105
|
-
def decorator(func: Callable) -> Callable[..., Awaitable[RESP]]:
|
|
106
|
-
sig = signature(func)
|
|
107
|
-
custom_parameters = CustomParameters.from_sig(sig)
|
|
108
|
-
|
|
109
|
-
@wraps(func)
|
|
110
|
-
async def wrapper(*args, **kwargs) -> RESP:
|
|
111
|
-
assert isinstance(
|
|
112
|
-
args[0], RapidApi
|
|
113
|
-
), f"{args[0]} should be an instance of RapidApi"
|
|
114
|
-
client = args[0].client
|
|
115
|
-
request = create_request(
|
|
116
|
-
client,
|
|
117
|
-
request_builder,
|
|
118
|
-
sig,
|
|
119
|
-
custom_parameters,
|
|
120
|
-
method,
|
|
121
|
-
path,
|
|
122
|
-
args,
|
|
123
|
-
kwargs,
|
|
124
|
-
)
|
|
125
|
-
response = await client.send(request)
|
|
126
|
-
if pydantic_xml is not None and issubclass(
|
|
127
|
-
response_class, pydantic_xml.BaseXmlModel
|
|
128
|
-
):
|
|
129
|
-
response.raise_for_status()
|
|
130
|
-
return response_class.from_xml(response.content)
|
|
131
|
-
if issubclass(response_class, BaseModel):
|
|
132
|
-
response.raise_for_status()
|
|
133
|
-
return response_class.model_validate_json(response.content)
|
|
134
|
-
return response
|
|
135
|
-
|
|
136
|
-
return wrapper
|
|
137
|
-
|
|
138
|
-
return decorator
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
get = partial(http, method="GET")
|
|
142
|
-
post = partial(http, method="POST")
|
|
143
|
-
delete = partial(http, method="DELETE")
|
|
144
|
-
put = partial(http, method="PUT")
|
|
145
|
-
patch = partial(http, method="PATCH")
|
|
File without changes
|