rapid-api-client 0.3.1__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.1 → rapid_api_client-0.3.2}/PKG-INFO +4 -3
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/README.md +3 -3
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/pyproject.toml +3 -1
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/rapid_api_client/__init__.py +4 -0
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/rapid_api_client/client.py +61 -19
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/rapid_api_client/model.py +53 -2
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/rapid_api_client/utils.py +10 -0
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/LICENSE +0 -0
- {rapid_api_client-0.3.1 → rapid_api_client-0.3.2}/rapid_api_client/py.typed +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
|
|
@@ -65,7 +65,7 @@ class GithubIssuesApi(RapidApi):
|
|
|
65
65
|
async def list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
66
66
|
|
|
67
67
|
@get("/repos/{owner}/{repo}/releases", response_class=TypeAdapter(List[Release]))
|
|
68
|
-
async def
|
|
68
|
+
async def list_releases(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
69
69
|
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -109,7 +109,7 @@ class MyApi(RapidApi)
|
|
|
109
109
|
async def post(self): ...
|
|
110
110
|
|
|
111
111
|
@delete("/anything")
|
|
112
|
-
async
|
|
112
|
+
async def delete(self): ...
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
|
|
@@ -245,3 +245,4 @@ class MyApi(RapidApi)
|
|
|
245
245
|
# Examples
|
|
246
246
|
|
|
247
247
|
See [example directory](./examples/) for some examples
|
|
248
|
+
|
|
@@ -33,7 +33,7 @@ class GithubIssuesApi(RapidApi):
|
|
|
33
33
|
async def list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
34
34
|
|
|
35
35
|
@get("/repos/{owner}/{repo}/releases", response_class=TypeAdapter(List[Release]))
|
|
36
|
-
async def
|
|
36
|
+
async def list_releases(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
|
|
37
37
|
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -77,7 +77,7 @@ class MyApi(RapidApi)
|
|
|
77
77
|
async def post(self): ...
|
|
78
78
|
|
|
79
79
|
@delete("/anything")
|
|
80
|
-
async
|
|
80
|
+
async def delete(self): ...
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
|
|
@@ -212,4 +212,4 @@ class MyApi(RapidApi)
|
|
|
212
212
|
|
|
213
213
|
# Examples
|
|
214
214
|
|
|
215
|
-
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"]
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorator used to build the httpx request
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from dataclasses import dataclass, field
|
|
2
6
|
from functools import partial, wraps
|
|
3
7
|
from inspect import Signature, signature
|
|
@@ -18,7 +22,7 @@ from httpx import AsyncClient, Request, Response
|
|
|
18
22
|
from pydantic import BaseModel, TypeAdapter
|
|
19
23
|
from pydantic_xml import BaseXmlModel
|
|
20
24
|
|
|
21
|
-
from .model import Body, FileBody, Header, Path, Query, RapidApi
|
|
25
|
+
from .model import Body, CustomParameter, FileBody, Header, Path, Query, RapidApi
|
|
22
26
|
from .utils import filter_none_values, find_annotation
|
|
23
27
|
|
|
24
28
|
BM = TypeVar("BM", bound=BaseModel)
|
|
@@ -27,15 +31,23 @@ T = TypeVar("T")
|
|
|
27
31
|
|
|
28
32
|
@dataclass
|
|
29
33
|
class CustomParameters:
|
|
34
|
+
"""
|
|
35
|
+
Class containing all custom parameters used to build the request.
|
|
36
|
+
"""
|
|
37
|
+
|
|
30
38
|
path: Dict[str, Path] = field(default_factory=dict)
|
|
31
39
|
query: Dict[str, Query] = field(default_factory=dict)
|
|
32
40
|
headers: Dict[str, Header] = field(default_factory=dict)
|
|
33
41
|
body: Dict[str, Body] = field(default_factory=dict)
|
|
34
42
|
|
|
35
43
|
@classmethod
|
|
36
|
-
def from_sig(cls,
|
|
44
|
+
def from_sig(cls, sig: Signature) -> Self:
|
|
45
|
+
"""
|
|
46
|
+
Iterate over parameters of given function to find annotated parameters
|
|
47
|
+
"""
|
|
37
48
|
out = cls()
|
|
38
|
-
for parameter in
|
|
49
|
+
for parameter in sig.parameters.values():
|
|
50
|
+
annot: CustomParameter | None = None
|
|
39
51
|
if (annot := find_annotation(parameter, Path)) is not None:
|
|
40
52
|
out.path[parameter.name] = annot
|
|
41
53
|
if (annot := find_annotation(parameter, Query)) is not None:
|
|
@@ -49,50 +61,60 @@ class CustomParameters:
|
|
|
49
61
|
|
|
50
62
|
def build_request(
|
|
51
63
|
client: AsyncClient,
|
|
52
|
-
|
|
64
|
+
sig: Signature,
|
|
53
65
|
parameters: CustomParameters,
|
|
54
66
|
method: str,
|
|
55
67
|
path: str,
|
|
56
68
|
args: Tuple[Any],
|
|
57
69
|
kwargs: Mapping[str, Any],
|
|
70
|
+
timeout: float | None,
|
|
58
71
|
) -> Request:
|
|
72
|
+
"""
|
|
73
|
+
Build the httpx request with given custom parameters.
|
|
74
|
+
"""
|
|
59
75
|
# valuate arguments with default values
|
|
60
|
-
ba =
|
|
76
|
+
ba = sig.bind(*args, **kwargs)
|
|
61
77
|
ba.apply_defaults()
|
|
62
78
|
|
|
63
79
|
# resolve the api path
|
|
64
80
|
path = path.format(**{k: ba.arguments[k] for k in parameters.path})
|
|
65
81
|
|
|
66
|
-
|
|
82
|
+
build_kwargs: Dict[str, Any] = {}
|
|
83
|
+
|
|
84
|
+
build_kwargs["headers"] = filter_none_values(
|
|
67
85
|
{
|
|
68
86
|
annot.alias or param: ba.arguments[param]
|
|
69
87
|
for param, annot in parameters.headers.items()
|
|
70
88
|
}
|
|
71
89
|
)
|
|
72
|
-
params = filter_none_values(
|
|
90
|
+
build_kwargs["params"] = filter_none_values(
|
|
73
91
|
{
|
|
74
92
|
annot.alias or param: ba.arguments[param]
|
|
75
93
|
for param, annot in parameters.query.items()
|
|
76
94
|
}
|
|
77
95
|
)
|
|
78
|
-
content = None
|
|
79
|
-
files = {}
|
|
80
96
|
for param, annot in parameters.body.items():
|
|
81
97
|
if (value := ba.arguments[param]) is not None:
|
|
82
98
|
if isinstance(annot, FileBody):
|
|
99
|
+
files = build_kwargs.setdefault("files", {})
|
|
83
100
|
files[annot.alias or param] = annot.serialize(value)
|
|
84
101
|
else:
|
|
85
|
-
content = annot.serialize(value)
|
|
102
|
+
build_kwargs["content"] = annot.serialize(value)
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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)
|
|
90
109
|
|
|
91
110
|
|
|
92
111
|
def handle_response(
|
|
93
112
|
response: Response,
|
|
94
113
|
response_class: Type[Response | str | bytes | BM] | TypeAdapter[T],
|
|
95
114
|
) -> Response | str | bytes | BM | T:
|
|
115
|
+
"""
|
|
116
|
+
Parse the response given the expected class
|
|
117
|
+
"""
|
|
96
118
|
# do not check response status code if we return the Response itself
|
|
97
119
|
if response_class is Response:
|
|
98
120
|
return response
|
|
@@ -113,31 +135,46 @@ def handle_response(
|
|
|
113
135
|
|
|
114
136
|
@overload
|
|
115
137
|
def http(
|
|
116
|
-
method: str,
|
|
138
|
+
method: str,
|
|
139
|
+
path: str,
|
|
140
|
+
response_class: Type[Response] = Response,
|
|
141
|
+
timeout: float | None = None,
|
|
117
142
|
) -> Callable[[Callable], Callable[..., Awaitable[Response]]]: ...
|
|
118
143
|
|
|
119
144
|
|
|
120
145
|
@overload
|
|
121
146
|
def http(
|
|
122
|
-
method: str,
|
|
147
|
+
method: str,
|
|
148
|
+
path: str,
|
|
149
|
+
response_class: Type[str],
|
|
150
|
+
timeout: float | None = None,
|
|
123
151
|
) -> Callable[[Callable], Callable[..., Awaitable[str]]]: ...
|
|
124
152
|
|
|
125
153
|
|
|
126
154
|
@overload
|
|
127
155
|
def http(
|
|
128
|
-
method: str,
|
|
156
|
+
method: str,
|
|
157
|
+
path: str,
|
|
158
|
+
response_class: Type[bytes],
|
|
159
|
+
timeout: float | None = None,
|
|
129
160
|
) -> Callable[[Callable], Callable[..., Awaitable[bytes]]]: ...
|
|
130
161
|
|
|
131
162
|
|
|
132
163
|
@overload
|
|
133
164
|
def http(
|
|
134
|
-
method: str,
|
|
165
|
+
method: str,
|
|
166
|
+
path: str,
|
|
167
|
+
response_class: Type[BM],
|
|
168
|
+
timeout: float | None = None,
|
|
135
169
|
) -> Callable[[Callable], Callable[..., Awaitable[BM]]]: ...
|
|
136
170
|
|
|
137
171
|
|
|
138
172
|
@overload
|
|
139
173
|
def http(
|
|
140
|
-
method: str,
|
|
174
|
+
method: str,
|
|
175
|
+
path: str,
|
|
176
|
+
response_class: TypeAdapter[T],
|
|
177
|
+
timeout: float | None = None,
|
|
141
178
|
) -> Callable[[Callable], Callable[..., Awaitable[T]]]: ...
|
|
142
179
|
|
|
143
180
|
|
|
@@ -145,7 +182,12 @@ def http(
|
|
|
145
182
|
method: str,
|
|
146
183
|
path: str,
|
|
147
184
|
response_class: Type[BM | str | bytes | Response] | TypeAdapter[T] = Response,
|
|
185
|
+
timeout: float | None = None,
|
|
148
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
|
+
|
|
149
191
|
def decorator(
|
|
150
192
|
func: Callable,
|
|
151
193
|
) -> Callable[..., Awaitable[BM | str | bytes | Response | T]]:
|
|
@@ -159,7 +201,7 @@ def http(
|
|
|
159
201
|
), f"{args[0]} should be an instance of RapidApi"
|
|
160
202
|
client = args[0].client
|
|
161
203
|
request = build_request(
|
|
162
|
-
client, sig, custom_parameters, method, path, args, kwargs
|
|
204
|
+
client, sig, custom_parameters, method, path, args, kwargs, timeout
|
|
163
205
|
)
|
|
164
206
|
response = await client.send(request)
|
|
165
207
|
return handle_response(response, response_class)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model classes
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from dataclasses import dataclass, field
|
|
2
6
|
from typing import Any
|
|
3
7
|
|
|
@@ -12,50 +16,97 @@ except ImportError: # pragma: nocover
|
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
18
|
class RapidApi:
|
|
19
|
+
"""
|
|
20
|
+
Represent an API, a RapidApi subclass should have methods decorated with @http
|
|
21
|
+
which are endpoints
|
|
22
|
+
"""
|
|
23
|
+
|
|
15
24
|
client: AsyncClient = field(default_factory=AsyncClient)
|
|
16
25
|
|
|
17
26
|
|
|
18
|
-
class CustomParameter:
|
|
27
|
+
class CustomParameter:
|
|
28
|
+
"""
|
|
29
|
+
Meta class for annotations used to customize the request build
|
|
30
|
+
"""
|
|
19
31
|
|
|
20
32
|
|
|
21
|
-
class Path(CustomParameter):
|
|
33
|
+
class Path(CustomParameter):
|
|
34
|
+
"""
|
|
35
|
+
Annotation to declare an argument used to resolve the api path/url
|
|
36
|
+
"""
|
|
22
37
|
|
|
23
38
|
|
|
24
39
|
@dataclass
|
|
25
40
|
class Query(CustomParameter):
|
|
41
|
+
"""
|
|
42
|
+
Annotation to declare an argument used as a query parameter
|
|
43
|
+
"""
|
|
44
|
+
|
|
26
45
|
alias: str | None = None
|
|
27
46
|
|
|
28
47
|
|
|
29
48
|
@dataclass
|
|
30
49
|
class Header(CustomParameter):
|
|
50
|
+
"""
|
|
51
|
+
Annotation to declare an argument used as a request header
|
|
52
|
+
"""
|
|
53
|
+
|
|
31
54
|
alias: str | None = None
|
|
32
55
|
|
|
33
56
|
|
|
34
57
|
class Body(CustomParameter):
|
|
58
|
+
"""
|
|
59
|
+
Annotation to declare an argument used as http content for post/put/...
|
|
60
|
+
"""
|
|
61
|
+
|
|
35
62
|
def serialize(self, body: Any) -> str | bytes:
|
|
63
|
+
"""
|
|
64
|
+
Serialize the annotated parameter value
|
|
65
|
+
"""
|
|
36
66
|
return body
|
|
37
67
|
|
|
38
68
|
|
|
39
69
|
@dataclass
|
|
40
70
|
class FileBody(Body):
|
|
71
|
+
"""
|
|
72
|
+
Annotation to declare an argument used as file to be uploaded
|
|
73
|
+
"""
|
|
74
|
+
|
|
41
75
|
alias: str | None = None
|
|
42
76
|
|
|
43
77
|
def serialize(self, body: Any) -> str | bytes:
|
|
78
|
+
"""
|
|
79
|
+
Serialize the annotated parameter value
|
|
80
|
+
"""
|
|
44
81
|
return body
|
|
45
82
|
|
|
46
83
|
|
|
47
84
|
@dataclass
|
|
48
85
|
class PydanticBody(Body):
|
|
86
|
+
"""
|
|
87
|
+
Annotation to declare an argument to be serialized to json and used as http content
|
|
88
|
+
"""
|
|
89
|
+
|
|
49
90
|
prettyprint: bool = False
|
|
50
91
|
|
|
51
92
|
def serialize(self, body: Any) -> str | bytes:
|
|
93
|
+
"""
|
|
94
|
+
Serialize the annotated parameter value
|
|
95
|
+
"""
|
|
52
96
|
assert isinstance(body, BaseModel)
|
|
53
97
|
return body.model_dump_json(indent=2 if self.prettyprint else None)
|
|
54
98
|
|
|
55
99
|
|
|
56
100
|
@dataclass
|
|
57
101
|
class PydanticXmlBody(Body):
|
|
102
|
+
"""
|
|
103
|
+
Annotation to declare an argument to be serialized to xml and used as http content
|
|
104
|
+
"""
|
|
105
|
+
|
|
58
106
|
def serialize(self, body: Any) -> str | bytes:
|
|
107
|
+
"""
|
|
108
|
+
Serialize the annotated parameter value
|
|
109
|
+
"""
|
|
59
110
|
assert (
|
|
60
111
|
pydantic_xml is not None
|
|
61
112
|
), "pydantic-xml must be installed to use PydanticXmlBody"
|
|
@@ -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):
|
|
File without changes
|
|
File without changes
|