rapid-api-client 0.3.2__tar.gz → 0.3.4__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.2
3
+ Version: 0.3.4
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
@@ -134,7 +134,7 @@ class MyApi(RapidApi)
134
134
 
135
135
  # this method return a httpx.Response
136
136
  @get("/user/me")
137
- async def get_user_resp(self): ...
137
+ async def get_user_raw(self): ...
138
138
 
139
139
  # this method returns a User class
140
140
  @get("/user/me", response_class=User)
@@ -190,11 +190,11 @@ class MyApi(RapidApi)
190
190
 
191
191
  # Headers can have a default value
192
192
  @get("/issues")
193
- async def get_issues(self, version: Annotated[str, Header()] = "1"): ...
193
+ async def get_issues_default(self, version: Annotated[str, Header()] = "1"): ...
194
194
 
195
195
  # Headers can have an alias to change the key in the http request
196
196
  @get("/issues")
197
- async def get_issues(self, version: Annotated[str, Header(alias="X-API-Version")] = "1"): ...
197
+ async def get_issues_version(self, version: Annotated[str, Header(alias="X-API-Version")] = "1"): ...
198
198
  ```
199
199
 
200
200
  ## Body parameter
@@ -211,15 +211,15 @@ class MyApi(RapidApi)
211
211
 
212
212
  # send a string in request content
213
213
  @post("/string")
214
- async def message(self, body: Annotated[str, Body()]): ...
214
+ async def post_string(self, body: Annotated[str, Body()]): ...
215
215
 
216
216
  # send a string in request content
217
217
  @post("/model")
218
- async def model(self, body: Annotated[MyPydanticClass, PydanticBody()]): ...
218
+ async def post_model(self, body: Annotated[MyPydanticClass, PydanticBody()]): ...
219
219
 
220
220
  # send a multiple files
221
221
  @post("/files")
222
- async def model(self, report: Annotated[bytes, FileBody()], image: Annotated[bytes, FileBody()]): ...
222
+ async def post_files(self, report: Annotated[bytes, FileBody()], image: Annotated[bytes, FileBody()]): ...
223
223
 
224
224
  ```
225
225
 
@@ -102,7 +102,7 @@ class MyApi(RapidApi)
102
102
 
103
103
  # this method return a httpx.Response
104
104
  @get("/user/me")
105
- async def get_user_resp(self): ...
105
+ async def get_user_raw(self): ...
106
106
 
107
107
  # this method returns a User class
108
108
  @get("/user/me", response_class=User)
@@ -158,11 +158,11 @@ class MyApi(RapidApi)
158
158
 
159
159
  # Headers can have a default value
160
160
  @get("/issues")
161
- async def get_issues(self, version: Annotated[str, Header()] = "1"): ...
161
+ async def get_issues_default(self, version: Annotated[str, Header()] = "1"): ...
162
162
 
163
163
  # Headers can have an alias to change the key in the http request
164
164
  @get("/issues")
165
- async def get_issues(self, version: Annotated[str, Header(alias="X-API-Version")] = "1"): ...
165
+ async def get_issues_version(self, version: Annotated[str, Header(alias="X-API-Version")] = "1"): ...
166
166
  ```
167
167
 
168
168
  ## Body parameter
@@ -179,15 +179,15 @@ class MyApi(RapidApi)
179
179
 
180
180
  # send a string in request content
181
181
  @post("/string")
182
- async def message(self, body: Annotated[str, Body()]): ...
182
+ async def post_string(self, body: Annotated[str, Body()]): ...
183
183
 
184
184
  # send a string in request content
185
185
  @post("/model")
186
- async def model(self, body: Annotated[MyPydanticClass, PydanticBody()]): ...
186
+ async def post_model(self, body: Annotated[MyPydanticClass, PydanticBody()]): ...
187
187
 
188
188
  # send a multiple files
189
189
  @post("/files")
190
- async def model(self, report: Annotated[bytes, FileBody()], image: Annotated[bytes, FileBody()]): ...
190
+ async def post_files(self, report: Annotated[bytes, FileBody()], image: Annotated[bytes, FileBody()]): ...
191
191
 
192
192
  ```
193
193
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rapid-api-client"
3
- version = "0.3.2"
3
+ version = "0.3.4"
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"
@@ -4,8 +4,9 @@ rapid-api-client
4
4
 
5
5
  from importlib.metadata import version
6
6
 
7
- from .client import delete, get, http, patch, post, put
8
- from .model import Body, FileBody, Header, Path, PydanticBody, Query, RapidApi
7
+ from .annotations import Body, FileBody, Header, Path, PydanticBody, Query
8
+ from .client import RapidApi
9
+ from .decorator import delete, get, http, patch, post, put
9
10
 
10
11
  __version__ = version(__name__)
11
12
  __all__ = [
@@ -2,10 +2,9 @@
2
2
  Model classes
3
3
  """
4
4
 
5
- from dataclasses import dataclass, field
6
- from typing import Any
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal
7
7
 
8
- from httpx import AsyncClient
9
8
  from pydantic import BaseModel
10
9
 
11
10
  try:
@@ -14,30 +13,20 @@ except ImportError: # pragma: nocover
14
13
  pydantic_xml = None # type: ignore
15
14
 
16
15
 
17
- @dataclass
18
- class RapidApi:
19
- """
20
- Represent an API, a RapidApi subclass should have methods decorated with @http
21
- which are endpoints
22
- """
23
-
24
- client: AsyncClient = field(default_factory=AsyncClient)
25
-
26
-
27
- class CustomParameter:
16
+ class BaseAnnotation:
28
17
  """
29
18
  Meta class for annotations used to customize the request build
30
19
  """
31
20
 
32
21
 
33
- class Path(CustomParameter):
22
+ class Path(BaseAnnotation):
34
23
  """
35
24
  Annotation to declare an argument used to resolve the api path/url
36
25
  """
37
26
 
38
27
 
39
28
  @dataclass
40
- class Query(CustomParameter):
29
+ class Query(BaseAnnotation):
41
30
  """
42
31
  Annotation to declare an argument used as a query parameter
43
32
  """
@@ -46,7 +35,7 @@ class Query(CustomParameter):
46
35
 
47
36
 
48
37
  @dataclass
49
- class Header(CustomParameter):
38
+ class Header(BaseAnnotation):
50
39
  """
51
40
  Annotation to declare an argument used as a request header
52
41
  """
@@ -54,11 +43,14 @@ class Header(CustomParameter):
54
43
  alias: str | None = None
55
44
 
56
45
 
57
- class Body(CustomParameter):
46
+ @dataclass
47
+ class Body(BaseAnnotation):
58
48
  """
59
49
  Annotation to declare an argument used as http content for post/put/...
60
50
  """
61
51
 
52
+ target: Literal["content", "data", "files"] = "content"
53
+
62
54
  def serialize(self, body: Any) -> str | bytes:
63
55
  """
64
56
  Serialize the annotated parameter value
@@ -72,14 +64,9 @@ class FileBody(Body):
72
64
  Annotation to declare an argument used as file to be uploaded
73
65
  """
74
66
 
67
+ target: Literal["content", "data", "files"] = "files"
75
68
  alias: str | None = None
76
69
 
77
- def serialize(self, body: Any) -> str | bytes:
78
- """
79
- Serialize the annotated parameter value
80
- """
81
- return body
82
-
83
70
 
84
71
  @dataclass
85
72
  class PydanticBody(Body):
@@ -0,0 +1,138 @@
1
+ """
2
+ Decorator used to build the httpx request
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from inspect import Signature
7
+ from typing import (
8
+ Any,
9
+ Dict,
10
+ Mapping,
11
+ Self,
12
+ Tuple,
13
+ Type,
14
+ TypeVar,
15
+ )
16
+
17
+ from httpx import AsyncClient, Request, Response
18
+ from pydantic import BaseModel, TypeAdapter
19
+ from pydantic_xml import BaseXmlModel
20
+
21
+ from .annotations import BaseAnnotation, Body, FileBody, Header, Path, Query
22
+ from .utils import filter_none_values, find_annotation
23
+
24
+ BM = TypeVar("BM", bound=BaseModel)
25
+ T = TypeVar("T")
26
+
27
+
28
+ @dataclass
29
+ class CustomParameters:
30
+ """
31
+ Class containing all custom parameters used to build the request.
32
+ """
33
+
34
+ path: Dict[str, Path] = field(default_factory=dict)
35
+ query: Dict[str, Query] = field(default_factory=dict)
36
+ headers: Dict[str, Header] = field(default_factory=dict)
37
+ body: Dict[str, Body] = field(default_factory=dict)
38
+
39
+ @classmethod
40
+ def from_sig(cls, sig: Signature) -> Self:
41
+ """
42
+ Iterate over parameters of given function to find annotated parameters
43
+ """
44
+ out = cls()
45
+ for parameter in sig.parameters.values():
46
+ annot: BaseAnnotation | None = None
47
+ if (annot := find_annotation(parameter, Path)) is not None:
48
+ out.path[parameter.name] = annot
49
+ if (annot := find_annotation(parameter, Query)) is not None:
50
+ out.query[parameter.name] = annot
51
+ if (annot := find_annotation(parameter, Header)) is not None:
52
+ out.headers[parameter.name] = annot
53
+ if (annot := find_annotation(parameter, Body)) is not None:
54
+ out.body[parameter.name] = annot
55
+ return out
56
+
57
+
58
+ @dataclass
59
+ class RapidApi:
60
+ """
61
+ Represent an API, a RapidApi subclass should have methods decorated with @http
62
+ which are endpoints
63
+ """
64
+
65
+ client: AsyncClient = field(default_factory=AsyncClient)
66
+
67
+ def _build_request(
68
+ self,
69
+ sig: Signature,
70
+ parameters: CustomParameters,
71
+ method: str,
72
+ path: str,
73
+ args: Tuple[Any],
74
+ kwargs: Mapping[str, Any],
75
+ timeout: float | None,
76
+ ) -> Request:
77
+ """
78
+ Build the httpx request with given custom parameters.
79
+ """
80
+ # valuate arguments with default values
81
+ ba = sig.bind(*args, **kwargs)
82
+ ba.apply_defaults()
83
+
84
+ # resolve the api path
85
+ path = path.format(**{k: ba.arguments[k] for k in parameters.path})
86
+
87
+ build_kwargs: Dict[str, Any] = {}
88
+
89
+ build_kwargs["headers"] = filter_none_values(
90
+ {
91
+ annot.alias or param: ba.arguments[param]
92
+ for param, annot in parameters.headers.items()
93
+ }
94
+ )
95
+ build_kwargs["params"] = filter_none_values(
96
+ {
97
+ annot.alias or param: ba.arguments[param]
98
+ for param, annot in parameters.query.items()
99
+ }
100
+ )
101
+ for param, annot in parameters.body.items():
102
+ if (value := ba.arguments[param]) is not None:
103
+ if isinstance(annot, FileBody):
104
+ files = build_kwargs.setdefault("files", {})
105
+ files[annot.alias or param] = annot.serialize(value)
106
+ else:
107
+ build_kwargs[annot.target] = annot.serialize(value)
108
+
109
+ # handle extra optional kwargs
110
+ if timeout is not None:
111
+ build_kwargs["timeout"] = timeout
112
+
113
+ return self.client.build_request(method, path, **build_kwargs)
114
+
115
+ def _handle_response(
116
+ self,
117
+ response: Response,
118
+ response_class: Type[Response | str | bytes | BM] | TypeAdapter[T] = Response,
119
+ ) -> Response | str | bytes | BM | T:
120
+ """
121
+ Parse the response given the expected class
122
+ """
123
+ # do not check response status code if we return the Response itself
124
+ if response_class is Response:
125
+ return response
126
+ # before parsing the response, check its status
127
+ response.raise_for_status()
128
+ if response_class is str:
129
+ return response.text
130
+ if response_class is bytes:
131
+ return response.content
132
+ if isinstance(response_class, TypeAdapter):
133
+ return response_class.validate_json(response.content)
134
+ if issubclass(response_class, BaseXmlModel):
135
+ return response_class.from_xml(response.content)
136
+ if issubclass(response_class, BaseModel):
137
+ return response_class.model_validate_json(response.content)
138
+ raise ValueError(f"Response class not supported: {response_class}")
@@ -0,0 +1,103 @@
1
+ """
2
+ Decorator used to build the request and send it with httpx
3
+ """
4
+
5
+ from functools import partial, wraps
6
+ from inspect import signature
7
+ from typing import (
8
+ Any,
9
+ Callable,
10
+ Coroutine,
11
+ Type,
12
+ overload,
13
+ )
14
+
15
+ from httpx import Response
16
+ from pydantic import TypeAdapter
17
+
18
+ from .client import BM, CustomParameters, RapidApi, T
19
+
20
+
21
+ @overload
22
+ def http(
23
+ method: str,
24
+ path: str,
25
+ response_class: Type[Response] = Response,
26
+ timeout: float | None = None,
27
+ ) -> Callable[[Callable], Callable[..., Coroutine[Any, Any, Response]]]: ...
28
+
29
+
30
+ @overload
31
+ def http(
32
+ method: str,
33
+ path: str,
34
+ response_class: Type[str],
35
+ timeout: float | None = None,
36
+ ) -> Callable[[Callable], Callable[..., Coroutine[Any, Any, str]]]: ...
37
+
38
+
39
+ @overload
40
+ def http(
41
+ method: str,
42
+ path: str,
43
+ response_class: Type[bytes],
44
+ timeout: float | None = None,
45
+ ) -> Callable[[Callable], Callable[..., Coroutine[Any, Any, bytes]]]: ...
46
+
47
+
48
+ @overload
49
+ def http(
50
+ method: str,
51
+ path: str,
52
+ response_class: Type[BM],
53
+ timeout: float | None = None,
54
+ ) -> Callable[[Callable], Callable[..., Coroutine[Any, Any, BM]]]: ...
55
+
56
+
57
+ @overload
58
+ def http(
59
+ method: str,
60
+ path: str,
61
+ response_class: TypeAdapter[T],
62
+ timeout: float | None = None,
63
+ ) -> Callable[[Callable], Callable[..., Coroutine[Any, Any, T]]]: ...
64
+
65
+
66
+ def http(
67
+ method: str,
68
+ path: str,
69
+ response_class: Type[BM | str | bytes | Response] | TypeAdapter[T] = Response,
70
+ timeout: float | None = None,
71
+ ) -> Callable[
72
+ [Callable], Callable[..., Coroutine[Any, Any, BM | str | bytes | Response | T]]
73
+ ]:
74
+ """
75
+ Main decorator used to generate an http request and return its result
76
+ """
77
+
78
+ def decorator(
79
+ func: Callable,
80
+ ) -> Callable[..., Coroutine[Any, Any, BM | str | bytes | Response | T]]:
81
+ sig = signature(func)
82
+ custom_parameters = CustomParameters.from_sig(sig)
83
+
84
+ @wraps(func)
85
+ async def wrapper(*args, **kwargs) -> BM | str | bytes | Response | T:
86
+ if not isinstance((api := args[0]), RapidApi):
87
+ raise ValueError(f"{api} should be an instance of RapidApi")
88
+ request = api._build_request(
89
+ sig, custom_parameters, method, path, args, kwargs, timeout
90
+ )
91
+ response = await api.client.send(request)
92
+ return api._handle_response(response, response_class=response_class)
93
+
94
+ return wrapper
95
+
96
+ return decorator
97
+
98
+
99
+ get = partial(http, "GET")
100
+ post = partial(http, "POST")
101
+ delete = partial(http, "DELETE")
102
+ put = partial(http, "PUT")
103
+ patch = partial(http, "PATCH")
@@ -5,9 +5,9 @@ Utility methods
5
5
  from inspect import Parameter
6
6
  from typing import Any, Dict, Type, TypeVar, get_args
7
7
 
8
- from rapid_api_client.model import CustomParameter
8
+ from .annotations import BaseAnnotation
9
9
 
10
- CP = TypeVar("CP", bound=CustomParameter)
10
+ BA = TypeVar("BA", bound=BaseAnnotation)
11
11
 
12
12
 
13
13
  def filter_none_values(values: Dict[str, Any | None]) -> Dict[str, Any]:
@@ -17,7 +17,7 @@ def filter_none_values(values: Dict[str, Any | None]) -> Dict[str, Any]:
17
17
  return {k: v for k, v in values.items() if v is not None}
18
18
 
19
19
 
20
- def find_annotation(param: Parameter, cls: Type[CP]) -> CP | None:
20
+ def find_annotation(param: Parameter, cls: Type[BA]) -> BA | None:
21
21
  """
22
22
  Check if the given parameter has an annotation which is or is a subclass of given type
23
23
  """
@@ -1,218 +0,0 @@
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")