rapid-api-client 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Sébastien MB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.1
2
+ Name: rapid-api-client
3
+ Version: 0.1.0
4
+ Summary: Rapidly develop your API clients using decorators and annotations
5
+ Author: Sébastien MB
6
+ Author-email: seb@essembeh.org
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Framework :: Pydantic
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Topic :: Internet
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Classifier: Typing :: Typed
29
+ Requires-Dist: httpx (>=0.27.2,<0.28.0)
30
+ Requires-Dist: pydantic (>=2.9.2,<3.0.0)
31
+ Description-Content-Type: text/markdown
32
+
33
+ ![Github](https://img.shields.io/github/tag/essembeh/rapid-api-client.svg)
34
+ ![PyPi](https://img.shields.io/pypi/v/rapid-api-client.svg)
35
+ ![Python](https://img.shields.io/pypi/pyversions/rapid-api-client.svg)
36
+ ![CI](https://github.com/essembeh/python-helloworld/actions/workflows/poetry.yml/badge.svg)
37
+
38
+
39
+ # Rapid Api Client
40
+
41
+ Library to rapidly develop API clients based on [Pydantic](https://docs.pydantic.dev/) and [Httpx](https://www.python-httpx.org/) using *decorators* and *annotations*.
42
+
43
+ This project is largely inspired by [FastAPI](https://fastapi.tiangolo.com/).
44
+
45
+
46
+ # Usage
47
+
48
+ Install the project
49
+
50
+ ```sh
51
+ pip install rapid-api-client
52
+ ```
53
+
54
+ Declare your API using decorators and annotations (the method does not need any code, it will be generated by the decorator)
55
+
56
+ ```python
57
+ class GithubIssuesApi(RapidApi):
58
+
59
+ @get("/repos/{owner}/{repo}/issues", response_class=RootModel[List[Issue]])
60
+ def list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
61
+
62
+ ```
63
+
64
+ Use it
65
+
66
+ ```python
67
+ for issue in GithubIssuesApi().list_issues("essembeh", "rapid-api-client").root:
68
+ print("Issue:", issue)
69
+ ```
70
+
71
+ # Features
72
+
73
+ ## Http method
74
+
75
+ Any HTTP method can be used with `http` decorator
76
+
77
+ ```python
78
+ class MyApi(RapidApi)
79
+
80
+ @http("/anything") # default is GET
81
+ def get(self): ...
82
+
83
+ @http("/anything", method="POST")
84
+ def post(self): ...
85
+
86
+ @http("/anything", method="DELETE)
87
+ def delete(self): ...
88
+ ```
89
+
90
+ Convenient decorators are available like `get`, `post`, `delete`, `put`, `patch`
91
+
92
+ ```python
93
+ class MyApi(RapidApi)
94
+
95
+ @get("/anything")
96
+ def get(self): ...
97
+
98
+ @post("/anything")
99
+ def post(self): ...
100
+
101
+ @delete("/anything")
102
+ def delete(self): ...
103
+ ```
104
+
105
+
106
+ ## Response class
107
+
108
+ 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).
109
+
110
+ But you can also specify a *Pydantic model class* to automatically parse the response.
111
+
112
+ > Note: When a `response_class` is given, the `raise_for_status()` is always called to ensure the http response is OK
113
+
114
+ ```python
115
+ class User(BaseModel): ...
116
+
117
+ class MyApi(RapidApi)
118
+
119
+ # this method return a httpx.Response
120
+ @get("/user/me")
121
+ def get_user_resp(self): ...
122
+
123
+ # this method returns a User class
124
+ @get("/user/me", response_class=User)
125
+ def get_user(self): ...
126
+ ```
127
+
128
+
129
+ ## Path parameters
130
+
131
+ Like `fastapi` you can use your method arguments to build the api path to call.
132
+
133
+ ```python
134
+ class MyApi(RapidApi)
135
+
136
+ @get("/user/{user_id}")
137
+ def get_user(self, user_id: Annotated[int, Path()]): ...
138
+
139
+ # Path parameters dans have a default value
140
+ @get("/user/{user_id}")
141
+ def get_user(self, user_id: Annotated[int, Path()] = 1): ...
142
+
143
+ ```
144
+
145
+ ## Query parameters
146
+
147
+ You can add `query parameters` to your request using the `Query` annotation.
148
+
149
+ ```python
150
+ class MyApi(RapidApi)
151
+
152
+ @get("/issues")
153
+ def get_issues(self, sort: Annotated[str, Query()]): ...
154
+
155
+ # Query parameters can have a default value
156
+ @get("/issues")
157
+ def get_issues_default(self, sort: Annotated[str, Query()] = "date"): ...
158
+
159
+ # Query parameters can have an alias to change the key in the http request
160
+ @get("/issues")
161
+ def get_issues_alias(self, sort: Annotated[str, Query(alias="sort-by")] = "date"): ...
162
+ ```
163
+
164
+
165
+ ## Header parameter
166
+
167
+ You can add `headers` to your request using the `Header` annotation.
168
+
169
+ ```python
170
+ class MyApi(RapidApi)
171
+
172
+ @get("/issues")
173
+ def get_issues(self, version: Annotated[str, Header()]): ...
174
+
175
+ # Headers can have a default value
176
+ @get("/issues")
177
+ def get_issues(self, version: Annotated[str, Header()] = "1"): ...
178
+
179
+ # Headers can have an alias to change the key in the http request
180
+ @get("/issues")
181
+ def get_issues(self, version: Annotated[str, Header(alias="X-API-Version")] = "1"): ...
182
+ ```
183
+
184
+ ## Body parameter
185
+
186
+
@@ -0,0 +1,153 @@
1
+ ![Github](https://img.shields.io/github/tag/essembeh/rapid-api-client.svg)
2
+ ![PyPi](https://img.shields.io/pypi/v/rapid-api-client.svg)
3
+ ![Python](https://img.shields.io/pypi/pyversions/rapid-api-client.svg)
4
+ ![CI](https://github.com/essembeh/python-helloworld/actions/workflows/poetry.yml/badge.svg)
5
+
6
+
7
+ # Rapid Api Client
8
+
9
+ Library to rapidly develop API clients based on [Pydantic](https://docs.pydantic.dev/) and [Httpx](https://www.python-httpx.org/) using *decorators* and *annotations*.
10
+
11
+ This project is largely inspired by [FastAPI](https://fastapi.tiangolo.com/).
12
+
13
+
14
+ # Usage
15
+
16
+ Install the project
17
+
18
+ ```sh
19
+ pip install rapid-api-client
20
+ ```
21
+
22
+ Declare your API using decorators and annotations (the method does not need any code, it will be generated by the decorator)
23
+
24
+ ```python
25
+ class GithubIssuesApi(RapidApi):
26
+
27
+ @get("/repos/{owner}/{repo}/issues", response_class=RootModel[List[Issue]])
28
+ def list_issues(self, owner: Annotated[str, Path()], repo: Annotated[str, Path()]): ...
29
+
30
+ ```
31
+
32
+ Use it
33
+
34
+ ```python
35
+ for issue in GithubIssuesApi().list_issues("essembeh", "rapid-api-client").root:
36
+ print("Issue:", issue)
37
+ ```
38
+
39
+ # Features
40
+
41
+ ## Http method
42
+
43
+ Any HTTP method can be used with `http` decorator
44
+
45
+ ```python
46
+ class MyApi(RapidApi)
47
+
48
+ @http("/anything") # default is GET
49
+ def get(self): ...
50
+
51
+ @http("/anything", method="POST")
52
+ def post(self): ...
53
+
54
+ @http("/anything", method="DELETE)
55
+ def delete(self): ...
56
+ ```
57
+
58
+ Convenient decorators are available like `get`, `post`, `delete`, `put`, `patch`
59
+
60
+ ```python
61
+ class MyApi(RapidApi)
62
+
63
+ @get("/anything")
64
+ def get(self): ...
65
+
66
+ @post("/anything")
67
+ def post(self): ...
68
+
69
+ @delete("/anything")
70
+ def delete(self): ...
71
+ ```
72
+
73
+
74
+ ## Response class
75
+
76
+ 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).
77
+
78
+ But you can also specify a *Pydantic model class* to automatically parse the response.
79
+
80
+ > Note: When a `response_class` is given, the `raise_for_status()` is always called to ensure the http response is OK
81
+
82
+ ```python
83
+ class User(BaseModel): ...
84
+
85
+ class MyApi(RapidApi)
86
+
87
+ # this method return a httpx.Response
88
+ @get("/user/me")
89
+ def get_user_resp(self): ...
90
+
91
+ # this method returns a User class
92
+ @get("/user/me", response_class=User)
93
+ def get_user(self): ...
94
+ ```
95
+
96
+
97
+ ## Path parameters
98
+
99
+ Like `fastapi` you can use your method arguments to build the api path to call.
100
+
101
+ ```python
102
+ class MyApi(RapidApi)
103
+
104
+ @get("/user/{user_id}")
105
+ def get_user(self, user_id: Annotated[int, Path()]): ...
106
+
107
+ # Path parameters dans have a default value
108
+ @get("/user/{user_id}")
109
+ def get_user(self, user_id: Annotated[int, Path()] = 1): ...
110
+
111
+ ```
112
+
113
+ ## Query parameters
114
+
115
+ You can add `query parameters` to your request using the `Query` annotation.
116
+
117
+ ```python
118
+ class MyApi(RapidApi)
119
+
120
+ @get("/issues")
121
+ def get_issues(self, sort: Annotated[str, Query()]): ...
122
+
123
+ # Query parameters can have a default value
124
+ @get("/issues")
125
+ def get_issues_default(self, sort: Annotated[str, Query()] = "date"): ...
126
+
127
+ # Query parameters can have an alias to change the key in the http request
128
+ @get("/issues")
129
+ def get_issues_alias(self, sort: Annotated[str, Query(alias="sort-by")] = "date"): ...
130
+ ```
131
+
132
+
133
+ ## Header parameter
134
+
135
+ You can add `headers` to your request using the `Header` annotation.
136
+
137
+ ```python
138
+ class MyApi(RapidApi)
139
+
140
+ @get("/issues")
141
+ def get_issues(self, version: Annotated[str, Header()]): ...
142
+
143
+ # Headers can have a default value
144
+ @get("/issues")
145
+ def get_issues(self, version: Annotated[str, Header()] = "1"): ...
146
+
147
+ # Headers can have an alias to change the key in the http request
148
+ @get("/issues")
149
+ def get_issues(self, version: Annotated[str, Header(alias="X-API-Version")] = "1"): ...
150
+ ```
151
+
152
+ ## Body parameter
153
+
@@ -0,0 +1,49 @@
1
+ [tool.poetry]
2
+ name = "rapid-api-client"
3
+ version = "0.1.0"
4
+ description = "Rapidly develop your API clients using decorators and annotations"
5
+ authors = ["Sébastien MB <seb@essembeh.org>"]
6
+ readme = "README.md"
7
+ classifiers = [
8
+ "Intended Audience :: Information Technology",
9
+ "Intended Audience :: System Administrators",
10
+ "Operating System :: OS Independent",
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python",
13
+ "Topic :: Internet",
14
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
15
+ "Topic :: Software Development :: Libraries :: Python Modules",
16
+ "Topic :: Software Development :: Libraries",
17
+ "Topic :: Software Development",
18
+ "Typing :: Typed",
19
+ "Development Status :: 4 - Beta",
20
+ "Environment :: Web Environment",
21
+ "Framework :: AsyncIO",
22
+ "Framework :: Pydantic",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Topic :: Internet :: WWW/HTTP",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/essembeh/rapid-api-client"
33
+ Repository = "https://github.com/essembeh/rapid-api-client"
34
+ Issues = "https://github.com/essembeh/rapid-api-client/issues"
35
+
36
+ [tool.poetry.dependencies]
37
+ python = "^3.11"
38
+ pydantic = "^2.9.2"
39
+ httpx = "^0.27.2"
40
+
41
+ [tool.poetry.group.dev.dependencies]
42
+ pytest = "^8.3.3"
43
+ pytest-asyncio = "^0.24.0"
44
+ pytest-cov = "^5.0.0"
45
+ pytest-dotenv = {extras = ["cli"], version = "^0.5.2"}
46
+
47
+ [build-system]
48
+ requires = ["poetry-core"]
49
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,21 @@
1
+ from importlib.metadata import version
2
+
3
+ from .client import delete, get, http, patch, post, put
4
+ from .model import Body, FileBody, Header, Path, PydanticBody, Query, RapidApi
5
+
6
+ __version__ = version(__name__)
7
+ __all__ = [
8
+ "Body",
9
+ "FileBody",
10
+ "Header",
11
+ "Path",
12
+ "PydanticBody",
13
+ "Query",
14
+ "RapidApi",
15
+ "get",
16
+ "post",
17
+ "put",
18
+ "delete",
19
+ "patch",
20
+ "http",
21
+ ]
@@ -0,0 +1,110 @@
1
+ from dataclasses import dataclass, field
2
+ from functools import partial, wraps
3
+ from inspect import Signature, signature
4
+ from typing import Any, Callable, Dict, Mapping, Self, Tuple, Type, TypeVar
5
+
6
+ from httpx import Client, Request, Response
7
+ from pydantic import BaseModel
8
+
9
+ from .model import Body, FileBody, Header, Path, Query, RapidApi
10
+ from .utils import filter_none_values, find_annotation
11
+
12
+ RESP = TypeVar("RESP", bound=BaseModel | Response)
13
+
14
+
15
+ @dataclass
16
+ class CustomParameters:
17
+ path: Dict[str, Path] = field(default_factory=dict)
18
+ query: Dict[str, Query] = field(default_factory=dict)
19
+ headers: Dict[str, Header] = field(default_factory=dict)
20
+ body: Dict[str, Body] = field(default_factory=dict)
21
+
22
+ @classmethod
23
+ def from_sig(cls, signature: Signature) -> Self:
24
+ out = cls()
25
+ for parameter in signature.parameters.values():
26
+ if (annot := find_annotation(parameter, Path)) is not None:
27
+ out.path[parameter.name] = annot
28
+ if (annot := find_annotation(parameter, Query)) is not None:
29
+ out.query[parameter.name] = annot
30
+ if (annot := find_annotation(parameter, Header)) is not None:
31
+ out.headers[parameter.name] = annot
32
+ if (annot := find_annotation(parameter, Body)) is not None:
33
+ out.body[parameter.name] = annot
34
+ return out
35
+
36
+
37
+ def build_request(
38
+ client: Client,
39
+ signature: Signature,
40
+ parameters: CustomParameters,
41
+ method: str,
42
+ path: str,
43
+ args: Tuple[Any],
44
+ kwargs: Mapping[str, Any],
45
+ ) -> Request:
46
+ # valuate arguments with default values
47
+ ba = signature.bind(*args, **kwargs)
48
+ ba.apply_defaults()
49
+
50
+ # resolve the api path
51
+ path = path.format(**{k: ba.arguments[k] for k in parameters.path})
52
+
53
+ headers = filter_none_values(
54
+ {
55
+ annot.alias or param: ba.arguments[param]
56
+ for param, annot in parameters.headers.items()
57
+ }
58
+ )
59
+ params = filter_none_values(
60
+ {
61
+ annot.alias or param: ba.arguments[param]
62
+ for param, annot in parameters.query.items()
63
+ }
64
+ )
65
+ content = None
66
+ files = {}
67
+ for param, annot in parameters.body.items():
68
+ if (value := ba.arguments[param]) is not None:
69
+ if isinstance(annot, FileBody):
70
+ files[annot.alias or param] = annot.serialize(value)
71
+ else:
72
+ content = annot.serialize(value)
73
+
74
+ return client.build_request(
75
+ method, path, headers=headers, params=params, content=content, files=files
76
+ )
77
+
78
+
79
+ def http(
80
+ path: str, method: str = "GET", response_class: Type[RESP] = Response
81
+ ) -> Callable[[Callable], Callable[..., RESP]]:
82
+ def decorator(func: Callable) -> Callable[..., RESP]:
83
+ sig = signature(func)
84
+ custom_parameters = CustomParameters.from_sig(sig)
85
+
86
+ @wraps(func)
87
+ def wrapper(*args, **kwargs) -> RESP:
88
+ assert isinstance(
89
+ args[0], RapidApi
90
+ ), f"{args[0]} should be an instance of RapidApi"
91
+ client = args[0].client
92
+ request = build_request(
93
+ client, sig, custom_parameters, method, path, args, kwargs
94
+ )
95
+ response = client.send(request)
96
+ if issubclass(response_class, BaseModel):
97
+ response.raise_for_status()
98
+ return response_class.model_validate_json(response.content)
99
+ return response
100
+
101
+ return wrapper
102
+
103
+ return decorator
104
+
105
+
106
+ get = partial(http, method="GET")
107
+ post = partial(http, method="POST")
108
+ delete = partial(http, method="DELETE")
109
+ put = partial(http, method="PUT")
110
+ patch = partial(http, method="PATCH")
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+ from httpx import Client
5
+ from pydantic import BaseModel
6
+
7
+
8
+ @dataclass
9
+ class RapidApi:
10
+ client: Client = field(default_factory=Client)
11
+
12
+
13
+ class CustomParameter: ...
14
+
15
+
16
+ class Path(CustomParameter): ...
17
+
18
+
19
+ @dataclass
20
+ class Query(CustomParameter):
21
+ alias: str | None = None
22
+
23
+
24
+ @dataclass
25
+ class Header(CustomParameter):
26
+ alias: str | None = None
27
+
28
+
29
+ class Body(CustomParameter):
30
+ def serialize(self, body: Any) -> str | bytes:
31
+ return body
32
+
33
+
34
+ @dataclass
35
+ class FileBody(Body):
36
+ alias: str | None = None
37
+
38
+ def serialize(self, body: Any) -> str | bytes:
39
+ return body
40
+
41
+
42
+ @dataclass
43
+ class PydanticBody(Body):
44
+ prettyprint: bool = False
45
+
46
+ def serialize(self, body: Any) -> str | bytes:
47
+ assert isinstance(body, BaseModel)
48
+ return body.model_dump_json(indent=2 if self.prettyprint else None)
@@ -0,0 +1,18 @@
1
+ from inspect import Parameter
2
+ from typing import Any, Dict, Type, TypeVar, get_args
3
+
4
+ from rapid_api_client.model import CustomParameter
5
+
6
+ CP = TypeVar("CP", bound=CustomParameter)
7
+
8
+
9
+ def filter_none_values(values: Dict[str, Any | None]) -> Dict[str, Any]:
10
+ return {k: v for k, v in values.items() if v is not None}
11
+
12
+
13
+ def find_annotation(param: Parameter, cls: Type[CP]) -> CP | None:
14
+ if param.annotation:
15
+ for an in get_args(param.annotation):
16
+ if isinstance(an, cls):
17
+ return an
18
+ return None