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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rapid-api-client
3
- Version: 0.3.1
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 list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
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 ef delete(self): ...
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 list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
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 ef delete(self): ...
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.1"
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
@@ -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, signature: Signature) -> Self:
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 signature.parameters.values():
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
- signature: Signature,
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 = signature.bind(*args, **kwargs)
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
- headers = filter_none_values(
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
- return client.build_request(
88
- method, path, headers=headers, params=params, content=content, files=files
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, path: str, response_class: Type[Response] = Response
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, path: str, response_class: Type[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, path: str, response_class: Type[bytes]
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, path: str, response_class: Type[BM]
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, path: str, response_class: TypeAdapter[T]
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):