patera-apiinterface 0.4.0__py3-none-any.whl
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,25 @@
|
|
|
1
|
+
from .api_interface import (AuthType,
|
|
2
|
+
service,
|
|
3
|
+
method,
|
|
4
|
+
resource,
|
|
5
|
+
consumes,
|
|
6
|
+
produces,
|
|
7
|
+
headers,
|
|
8
|
+
timeout,
|
|
9
|
+
auth,
|
|
10
|
+
MissingApiInterfaceConfigurations,
|
|
11
|
+
ApiInterface)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'AuthType',
|
|
15
|
+
'service',
|
|
16
|
+
'method',
|
|
17
|
+
'resource',
|
|
18
|
+
'consumes',
|
|
19
|
+
'produces',
|
|
20
|
+
'headers',
|
|
21
|
+
'timeout',
|
|
22
|
+
'auth',
|
|
23
|
+
'MissingApiInterfaceConfigurations',
|
|
24
|
+
'ApiInterface'
|
|
25
|
+
]
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface for API integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import base64
|
|
7
|
+
from typing import (
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
Any,
|
|
11
|
+
Generic,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Type,
|
|
15
|
+
cast,
|
|
16
|
+
get_type_hints,
|
|
17
|
+
TypeVar,
|
|
18
|
+
TYPE_CHECKING,
|
|
19
|
+
)
|
|
20
|
+
import httpx
|
|
21
|
+
from httpx import Response
|
|
22
|
+
from functools import wraps
|
|
23
|
+
from enum import StrEnum
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
|
|
26
|
+
from .http_methods import HttpMethod
|
|
27
|
+
from .media_types import MediaType
|
|
28
|
+
from .base_extension import BaseExtension
|
|
29
|
+
from .request import UploadedFile, Request
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from .patera import Patera
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthType(StrEnum):
|
|
36
|
+
BASIC_AUTH = "basic_auth"
|
|
37
|
+
BEARER = "bearer_token"
|
|
38
|
+
API_KEY = "api_key"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def service(service_url: str) -> "Callable":
|
|
45
|
+
def decorator(cls: "Type[ApiInterface]") -> "Type[ApiInterface]":
|
|
46
|
+
setattr(cls, "__service_url__", service_url)
|
|
47
|
+
return cls
|
|
48
|
+
|
|
49
|
+
return decorator
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def method(http_method: HttpMethod) -> Callable[[F], F]:
|
|
53
|
+
def decorator(func: F) -> F:
|
|
54
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
55
|
+
api_int["method"] = http_method
|
|
56
|
+
setattr(func, "__api_interface__", api_int)
|
|
57
|
+
return func
|
|
58
|
+
|
|
59
|
+
return decorator
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resource(url: str, follow_redirects: bool = True) -> Callable[[F], F]:
|
|
63
|
+
def decorator(func: F) -> F:
|
|
64
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
65
|
+
api_int["target"] = url
|
|
66
|
+
api_int["follow_redirects"] = follow_redirects
|
|
67
|
+
setattr(func, "__api_interface__", api_int)
|
|
68
|
+
return func
|
|
69
|
+
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def consumes(media: MediaType) -> Callable[[F], F]:
|
|
74
|
+
def decorator(func: F) -> F:
|
|
75
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
76
|
+
api_int["consumes"] = media
|
|
77
|
+
setattr(func, "__api_interface__", api_int)
|
|
78
|
+
return func
|
|
79
|
+
|
|
80
|
+
return decorator
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def produces(media: MediaType) -> Callable[[F], F]:
|
|
84
|
+
def decorator(func: F) -> F:
|
|
85
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
86
|
+
api_int["produces"] = media
|
|
87
|
+
setattr(func, "__api_interface__", api_int)
|
|
88
|
+
return func
|
|
89
|
+
|
|
90
|
+
return decorator
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def headers(custom_headers: Dict[str, Any]) -> Callable[[F], F]:
|
|
94
|
+
def decorator(func: F) -> F:
|
|
95
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
96
|
+
api_int["custom_headers"] = custom_headers
|
|
97
|
+
setattr(func, "__api_interface__", api_int)
|
|
98
|
+
return func
|
|
99
|
+
|
|
100
|
+
return decorator
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def timeout(timeout: int) -> Callable[[F], F]:
|
|
104
|
+
def decorator(func: F) -> F:
|
|
105
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
106
|
+
api_int["timeout"] = timeout
|
|
107
|
+
setattr(func, "__api_interface__", api_int)
|
|
108
|
+
return func
|
|
109
|
+
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def auth(
|
|
114
|
+
auth_type: AuthType,
|
|
115
|
+
*,
|
|
116
|
+
username: str | None = None,
|
|
117
|
+
password: str | None = None,
|
|
118
|
+
header_name: str = "Authorization",
|
|
119
|
+
) -> Callable[[F], F]:
|
|
120
|
+
def decorator(func: F) -> F:
|
|
121
|
+
api_int: Dict[str, Any] = getattr(func, "__api_interface__", {}) or {}
|
|
122
|
+
api_int["auth"] = {
|
|
123
|
+
"type": auth_type,
|
|
124
|
+
"username": username,
|
|
125
|
+
"password": password,
|
|
126
|
+
"header_name": header_name,
|
|
127
|
+
}
|
|
128
|
+
setattr(func, "__api_interface__", api_int)
|
|
129
|
+
return func
|
|
130
|
+
|
|
131
|
+
return decorator
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MissingApiInterfaceConfigurations(Exception):
|
|
135
|
+
def __init__(self, msg: str) -> None:
|
|
136
|
+
super().__init__(msg)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
AppT = TypeVar("AppT", bound="Patera[Any]")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ApiInterface(BaseExtension[AppT], Generic[AppT]):
|
|
143
|
+
def init(self) -> None:
|
|
144
|
+
serv_url = getattr(self, "__service_url__", None)
|
|
145
|
+
if serv_url is None:
|
|
146
|
+
raise Exception(
|
|
147
|
+
"Missing service url for API Interface. Use @path from api_interface"
|
|
148
|
+
)
|
|
149
|
+
self._service_url = self._app.get_conf(serv_url, serv_url)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def _wrap_method(cls, func):
|
|
153
|
+
@wraps(func)
|
|
154
|
+
async def inner(self, req: Request, *args, **kwargs):
|
|
155
|
+
return await self._wrapper(func, req, *args, **kwargs)
|
|
156
|
+
|
|
157
|
+
return inner
|
|
158
|
+
|
|
159
|
+
async def _wrapper(self, func, req: Request, *args, **kwargs) -> Any:
|
|
160
|
+
raw_configs: Optional[Dict[str, Any]] = getattr(func, "__api_interface__", None)
|
|
161
|
+
if raw_configs is None:
|
|
162
|
+
raise MissingApiInterfaceConfigurations(
|
|
163
|
+
f"API Interface method {func.__name__} does not have any configurations. Please add configuration decorators."
|
|
164
|
+
)
|
|
165
|
+
configs: Dict[str, Any] = dict(raw_configs)
|
|
166
|
+
return_type: Optional[Type[BaseModel]] = get_type_hints(func).get(
|
|
167
|
+
"return", None
|
|
168
|
+
)
|
|
169
|
+
async with httpx.AsyncClient() as client:
|
|
170
|
+
url: str = cast(str, self._service_url) + cast(str, configs.get("target"))
|
|
171
|
+
url = self._format_url(req, url)
|
|
172
|
+
follow_redirects = cast(bool, configs.get("follow_redirects"))
|
|
173
|
+
method = configs.get("method", HttpMethod.GET)
|
|
174
|
+
timeout = configs.get("timeout", 10)
|
|
175
|
+
headers: Dict[str, str] = self._construct_headers(configs)
|
|
176
|
+
pydantic_data: list[BaseModel | dict] = list(
|
|
177
|
+
filter(lambda d: isinstance(d, (BaseModel, dict)), list(args))
|
|
178
|
+
)
|
|
179
|
+
data: BaseModel | dict | None = None
|
|
180
|
+
if len(pydantic_data) > 0:
|
|
181
|
+
data = pydantic_data[0]
|
|
182
|
+
res: Response
|
|
183
|
+
if method == HttpMethod.GET:
|
|
184
|
+
res = await client.request(
|
|
185
|
+
method,
|
|
186
|
+
url,
|
|
187
|
+
headers=headers,
|
|
188
|
+
timeout=timeout,
|
|
189
|
+
params=req.query_parameters,
|
|
190
|
+
follow_redirects=follow_redirects,
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
if data is None:
|
|
194
|
+
raise Exception("Missing body for request.")
|
|
195
|
+
if configs.get("consumes", MediaType.APPLICATION_JSON) in [
|
|
196
|
+
MediaType.APPLICATION_JSON,
|
|
197
|
+
MediaType.APPLICATION_PROBLEM_JSON,
|
|
198
|
+
MediaType.APPLICATION_X_NDJSON,
|
|
199
|
+
]:
|
|
200
|
+
res = await client.request(
|
|
201
|
+
method,
|
|
202
|
+
url,
|
|
203
|
+
headers=headers,
|
|
204
|
+
timeout=timeout,
|
|
205
|
+
json=cast(BaseModel, data).model_dump(),
|
|
206
|
+
params=req.query_parameters,
|
|
207
|
+
follow_redirects=follow_redirects,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
if isinstance(data, BaseModel):
|
|
211
|
+
form, files = self._get_form_and_files(data)
|
|
212
|
+
else:
|
|
213
|
+
form = data
|
|
214
|
+
files = None
|
|
215
|
+
res = await client.request(
|
|
216
|
+
method,
|
|
217
|
+
url,
|
|
218
|
+
headers=headers,
|
|
219
|
+
timeout=timeout,
|
|
220
|
+
data=form,
|
|
221
|
+
files=files,
|
|
222
|
+
params=req.query_parameters,
|
|
223
|
+
follow_redirects=follow_redirects,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
res = self._get_response_body(
|
|
227
|
+
res, configs.get("produces", MediaType.APPLICATION_JSON)
|
|
228
|
+
)
|
|
229
|
+
if return_type and issubclass(return_type, BaseModel):
|
|
230
|
+
return return_type.model_validate(res)
|
|
231
|
+
return res
|
|
232
|
+
|
|
233
|
+
def _construct_headers(self, configs: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
|
234
|
+
if configs is None:
|
|
235
|
+
configs = {}
|
|
236
|
+
|
|
237
|
+
headers: Dict[str, str] = {
|
|
238
|
+
"Content-Type": configs.get("consumes", MediaType.APPLICATION_JSON),
|
|
239
|
+
"Accept": configs.get("produces", MediaType.APPLICATION_JSON),
|
|
240
|
+
**configs.get("custom_headers", {}),
|
|
241
|
+
}
|
|
242
|
+
auth: Optional[dict[str, Any]] = configs.get("auth", None)
|
|
243
|
+
if auth is not None:
|
|
244
|
+
header_name = cast(str, auth.get("header_name"))
|
|
245
|
+
headers[header_name] = self._create_auth_headers(auth)
|
|
246
|
+
return headers
|
|
247
|
+
|
|
248
|
+
def _format_url(self, req: Request, url: str) -> str:
|
|
249
|
+
for key, value in req.route_parameters.items():
|
|
250
|
+
url = url.replace(f"<{key}>", value)
|
|
251
|
+
return url
|
|
252
|
+
|
|
253
|
+
def _get_form_and_files(
|
|
254
|
+
self, data: BaseModel
|
|
255
|
+
) -> Tuple[dict[str, Any], Optional[dict[str, tuple]]]:
|
|
256
|
+
form_data: dict[str, Any] = {}
|
|
257
|
+
files: dict[str, tuple] = {}
|
|
258
|
+
for field in dir(data):
|
|
259
|
+
if field.startswith("_"):
|
|
260
|
+
continue
|
|
261
|
+
value = getattr(data, field, None)
|
|
262
|
+
if isinstance(value, UploadedFile):
|
|
263
|
+
files[field] = (value.filename, value.get_stream(), value.content_type)
|
|
264
|
+
continue
|
|
265
|
+
form_data[field] = value
|
|
266
|
+
if len(files.keys()) == 0:
|
|
267
|
+
files = cast(dict[str, tuple], None)
|
|
268
|
+
if len(form_data.keys()) == 0:
|
|
269
|
+
form_data = cast(dict[str, Any], None)
|
|
270
|
+
return form_data, files
|
|
271
|
+
|
|
272
|
+
def _create_auth_headers(self, auth: dict[str, Any]) -> str:
|
|
273
|
+
auth_type = auth.get("type")
|
|
274
|
+
username = auth.get("username")
|
|
275
|
+
password = auth.get("password")
|
|
276
|
+
if auth_type == AuthType.BASIC_AUTH:
|
|
277
|
+
credentials = f"{username}:{password}"
|
|
278
|
+
encoded = base64.b64encode(credentials.encode()).decode()
|
|
279
|
+
return f"Basic {encoded}"
|
|
280
|
+
if auth_type == AuthType.BEARER:
|
|
281
|
+
return f"Bearer {self._app.get_conf(password, password)}" # type: ignore
|
|
282
|
+
if auth_type == AuthType.API_KEY:
|
|
283
|
+
return f"{self._app.get_conf(password, password)}" # type: ignore
|
|
284
|
+
return ""
|
|
285
|
+
|
|
286
|
+
def _get_response_body(self, res: Response, media_type: MediaType) -> Any:
|
|
287
|
+
if media_type in [
|
|
288
|
+
MediaType.APPLICATION_JSON,
|
|
289
|
+
MediaType.APPLICATION_X_NDJSON,
|
|
290
|
+
MediaType.APPLICATION_PROBLEM_JSON,
|
|
291
|
+
]:
|
|
292
|
+
return res.json()
|
|
293
|
+
if media_type in [
|
|
294
|
+
MediaType.TEXT_CSV,
|
|
295
|
+
MediaType.TEXT_HTML,
|
|
296
|
+
MediaType.TEXT_PLAIN,
|
|
297
|
+
MediaType.TEXT_XML,
|
|
298
|
+
MediaType.TEXT_YAML,
|
|
299
|
+
]:
|
|
300
|
+
return res.text
|
|
301
|
+
return res.read()
|
|
302
|
+
|
|
303
|
+
def __init_subclass__(cls, **kwargs):
|
|
304
|
+
super().__init_subclass__(**kwargs)
|
|
305
|
+
|
|
306
|
+
for name, value in list(cls.__dict__.items()):
|
|
307
|
+
if name.startswith("_"):
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
if callable(value) and hasattr(value, "__api_interface__"):
|
|
311
|
+
setattr(cls, name, ApiInterface._wrap_method(value))
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
patera/apiinterface/__init__.py,sha256=IL86nIjQ64E6tb2sFXuQC0rGN9OIl0ZHnK9zrmdapG4,673
|
|
2
|
+
patera/apiinterface/api_interface.py,sha256=1RC8f4i846ho6_HZ0ovqX5vYM3r_i7Snlv_asWqy29Y,10867
|
|
3
|
+
patera_apiinterface-0.4.0.dist-info/WHEEL,sha256=fWriCkzqm-pffF5af4gJC9iI5FMFaJTuN9UxxxzOmdY,81
|
|
4
|
+
patera_apiinterface-0.4.0.dist-info/METADATA,sha256=sjeWOS1BB43j06HFkEVGQg7IGyYRP47YrCsndfvb4Tk,109
|
|
5
|
+
patera_apiinterface-0.4.0.dist-info/RECORD,,
|