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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rapid-api-client
3
- Version: 0.3.0
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 asynchronous API clients based on [Pydantic](https://docs.pydantic.dev/) and [Httpx](https://www.python-httpx.org/) using *decorators* and *annotations*.
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
- This project is largely inspired by [FastAPI](https://fastapi.tiangolo.com/).
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 (the method does not need any code, it will be generated by the decorator)
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=RootModel[List[Issue]])
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.root:
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") # default is GET
90
+ @http("GET", "/anything")
83
91
  async def get(self): ...
84
92
 
85
- @http("/anything", method="POST")
93
+ @http("POST", "/anything")
86
94
  async def post(self): ...
87
95
 
88
- @http("/anything", method="DELETE")
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 ef delete(self): ...
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 *Pydantic model class* to automatically parse the response.
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 a `response_class` is given, the `raise_for_status()` is always called to ensure the http response is OK
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 asynchronous API clients based on [Pydantic](https://docs.pydantic.dev/) and [Httpx](https://www.python-httpx.org/) using *decorators* and *annotations*.
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
- This project is largely inspired by [FastAPI](https://fastapi.tiangolo.com/).
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 (the method does not need any code, it will be generated by the decorator)
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=RootModel[List[Issue]])
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.root:
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") # default is GET
58
+ @http("GET", "/anything")
51
59
  async def get(self): ...
52
60
 
53
- @http("/anything", method="POST")
61
+ @http("POST", "/anything")
54
62
  async def post(self): ...
55
63
 
56
- @http("/anything", method="DELETE")
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 ef delete(self): ...
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 *Pydantic model class* to automatically parse the response.
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 a `response_class` is given, the `raise_for_status()` is always called to ensure the http response is OK
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.0"
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
+ rapid-api-client
3
+ """
4
+
1
5
  from importlib.metadata import version
2
6
 
3
7
  from .client import delete, get, http, patch, post, put
@@ -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, Dict
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
- client: AsyncClient = field(default_factory=AsyncClient)
16
-
19
+ """
20
+ Represent an API, a RapidApi subclass should have methods decorated with @http
21
+ which are endpoints
22
+ """
17
23
 
18
- class RequestModifier:
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")