wexample-api 0.0.74__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.
- wexample_api/__init__.py +0 -0
- wexample_api/__pycache__/__init__.py +0 -0
- wexample_api/common/__init__.py +0 -0
- wexample_api/common/__pycache__/__init__.py +0 -0
- wexample_api/common/abstract_gateway.py +333 -0
- wexample_api/common/http_request_payload.py +74 -0
- wexample_api/const/__init__.py +0 -0
- wexample_api/const/__pycache__/__init__.py +0 -0
- wexample_api/const/http.py +13 -0
- wexample_api/demo/__init__.py +0 -0
- wexample_api/demo/demo_simple_gateway.py +57 -0
- wexample_api/enums/__init__.py +0 -0
- wexample_api/enums/__pycache__/__init__.py +0 -0
- wexample_api/enums/http.py +26 -0
- wexample_api/errors/__init__.py +0 -0
- wexample_api/errors/gateway_authentication_error.py +7 -0
- wexample_api/errors/gateway_connexion_error.py +7 -0
- wexample_api/py.typed +0 -0
- wexample_api-0.0.74.dist-info/METADATA +188 -0
- wexample_api-0.0.74.dist-info/RECORD +22 -0
- wexample_api-0.0.74.dist-info/WHEEL +4 -0
- wexample_api-0.0.74.dist-info/entry_points.txt +4 -0
wexample_api/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from wexample_helpers.classes.field import public_field
|
|
9
|
+
from wexample_helpers.classes.mixin.has_snake_short_class_name_class_mixin import (
|
|
10
|
+
HasSnakeShortClassNameClassMixin,
|
|
11
|
+
)
|
|
12
|
+
from wexample_helpers.classes.mixin.has_two_steps_init import HasTwoStepInit
|
|
13
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
14
|
+
from wexample_prompt.mixins.with_io_manager import WithIoManager
|
|
15
|
+
|
|
16
|
+
from wexample_api.enums.http import ContentType, HttpMethod
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
|
|
21
|
+
from wexample_helpers.const.types import StringsList
|
|
22
|
+
|
|
23
|
+
from wexample_api.common.http_request_payload import HttpRequestPayload
|
|
24
|
+
from wexample_api.enums.http import Header
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@base_class
|
|
28
|
+
class AbstractGateway(
|
|
29
|
+
HasSnakeShortClassNameClassMixin,
|
|
30
|
+
WithIoManager,
|
|
31
|
+
HasTwoStepInit,
|
|
32
|
+
):
|
|
33
|
+
# Base configuration
|
|
34
|
+
base_url: str | None = public_field(default=None, description="Base API URL")
|
|
35
|
+
# State
|
|
36
|
+
connected: bool = public_field(default=False, description="Connection state")
|
|
37
|
+
# Default request configuration
|
|
38
|
+
default_headers: dict[str, str] = public_field(
|
|
39
|
+
factory=dict, description="Default headers for requests"
|
|
40
|
+
)
|
|
41
|
+
last_exception: Any = public_field(
|
|
42
|
+
default=None, description="Last exception encountered during request"
|
|
43
|
+
)
|
|
44
|
+
last_request_time: float | None = public_field(
|
|
45
|
+
default=None, description="Timestamp of last request"
|
|
46
|
+
)
|
|
47
|
+
quiet: bool = public_field(
|
|
48
|
+
default=False, description="If True, only show errors and warnings"
|
|
49
|
+
)
|
|
50
|
+
rate_limit_delay: float = public_field(
|
|
51
|
+
default=1.0, description="Minimum delay between requests in seconds"
|
|
52
|
+
)
|
|
53
|
+
timeout: int = public_field(default=30, description="Request timeout in seconds")
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_class_name_suffix(cls) -> str | None:
|
|
57
|
+
return "GatewayService"
|
|
58
|
+
|
|
59
|
+
def check_connexion(self) -> bool:
|
|
60
|
+
return self.connected
|
|
61
|
+
|
|
62
|
+
def check_status_code(self, expected_status_codes: int | list[int] = 200) -> bool:
|
|
63
|
+
return (
|
|
64
|
+
self.make_request(
|
|
65
|
+
endpoint="", expected_status_codes=expected_status_codes, quiet=True
|
|
66
|
+
)
|
|
67
|
+
is not None
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def clear_error(self) -> None:
|
|
71
|
+
self.last_exception = None
|
|
72
|
+
|
|
73
|
+
def connect(self) -> bool:
|
|
74
|
+
self.connected = True
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def format_response_content(self, response: requests.Response | None) -> str:
|
|
78
|
+
"""Extract and format response content for logging."""
|
|
79
|
+
if response is None:
|
|
80
|
+
return "Null response"
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
return response.json()
|
|
84
|
+
except (ValueError, AttributeError):
|
|
85
|
+
return response.text
|
|
86
|
+
|
|
87
|
+
def get_base_url(self) -> str | None:
|
|
88
|
+
return self.base_url
|
|
89
|
+
|
|
90
|
+
def get_expected_env_keys(self) -> StringsList:
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
def get_last_error(self) -> Exception | None:
|
|
94
|
+
return self.last_exception
|
|
95
|
+
|
|
96
|
+
def handle_api_response(
|
|
97
|
+
self,
|
|
98
|
+
response: requests.Response | None,
|
|
99
|
+
request_context: HttpRequestPayload,
|
|
100
|
+
exception: Exception | None = None,
|
|
101
|
+
fatal_on_error: bool = False,
|
|
102
|
+
quiet: bool | None = None,
|
|
103
|
+
) -> requests.Response | None:
|
|
104
|
+
self.last_exception = exception
|
|
105
|
+
is_quiet = self.quiet if quiet is None else quiet
|
|
106
|
+
|
|
107
|
+
if response is None:
|
|
108
|
+
if not is_quiet:
|
|
109
|
+
self.io.properties(
|
|
110
|
+
self._create_request_details(request_context),
|
|
111
|
+
title="Request Details",
|
|
112
|
+
)
|
|
113
|
+
if exception:
|
|
114
|
+
self.io.error(str(exception), exception=exception, fatal=fatal_on_error)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
if not is_quiet:
|
|
118
|
+
self.io.debug(
|
|
119
|
+
message=f"{request_context.method} | {response.status_code} -> {request_context.url}",
|
|
120
|
+
symbol="🌍",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# If no filtering is configured, accept all responses
|
|
124
|
+
if request_context.expected_status_codes is None:
|
|
125
|
+
return response
|
|
126
|
+
|
|
127
|
+
if response.status_code in request_context.expected_status_codes:
|
|
128
|
+
return response
|
|
129
|
+
|
|
130
|
+
# Combine request details with response content
|
|
131
|
+
details = {
|
|
132
|
+
**self._create_request_details(request_context, response.status_code),
|
|
133
|
+
"Response Content": self.format_response_content(response),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if not is_quiet:
|
|
137
|
+
self.io.properties(details, title="Request Details")
|
|
138
|
+
|
|
139
|
+
self.io.error(
|
|
140
|
+
message=(
|
|
141
|
+
str(exception) if exception else self._extract_error_message(response)
|
|
142
|
+
),
|
|
143
|
+
exception=exception,
|
|
144
|
+
fatal=fatal_on_error,
|
|
145
|
+
)
|
|
146
|
+
return response
|
|
147
|
+
|
|
148
|
+
def has_error(self) -> bool:
|
|
149
|
+
return self.last_exception is not None
|
|
150
|
+
|
|
151
|
+
def make_request(
|
|
152
|
+
self,
|
|
153
|
+
endpoint: str,
|
|
154
|
+
method: HttpMethod = HttpMethod.GET,
|
|
155
|
+
data: dict[str, Any] | bytes | None = None,
|
|
156
|
+
query_params: dict[str, Any] | None = None,
|
|
157
|
+
headers: dict[str, str] | None = None,
|
|
158
|
+
files: dict[str, Any] | list[tuple] | None = None,
|
|
159
|
+
call_origin: str | None = None,
|
|
160
|
+
expected_status_codes: int | list[int] | None = None,
|
|
161
|
+
fatal_if_unexpected: bool = False,
|
|
162
|
+
quiet: bool = False,
|
|
163
|
+
stream: bool = False,
|
|
164
|
+
timeout: int | None = None,
|
|
165
|
+
raise_exceptions: bool = False,
|
|
166
|
+
) -> requests.Response | None:
|
|
167
|
+
from wexample_helpers.errors.gateway_error import GatewayError
|
|
168
|
+
|
|
169
|
+
from wexample_api.common.http_request_payload import HttpRequestPayload
|
|
170
|
+
from wexample_api.enums.http import Header
|
|
171
|
+
|
|
172
|
+
payload = HttpRequestPayload.from_endpoint(
|
|
173
|
+
base_url=self.get_base_url(),
|
|
174
|
+
endpoint=endpoint,
|
|
175
|
+
method=method,
|
|
176
|
+
data=data,
|
|
177
|
+
query_params=query_params,
|
|
178
|
+
headers={**self.default_headers, **(headers or {})},
|
|
179
|
+
call_origin=call_origin,
|
|
180
|
+
expected_status_codes=expected_status_codes,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not self.connected:
|
|
184
|
+
self.connect()
|
|
185
|
+
|
|
186
|
+
self._handle_rate_limiting()
|
|
187
|
+
|
|
188
|
+
# Determine how to send the data based on Content-Type header
|
|
189
|
+
content_type = self._get_header_value(payload.headers, Header.CONTENT_TYPE)
|
|
190
|
+
|
|
191
|
+
if files:
|
|
192
|
+
content_type = ContentType.MULTIPART.value
|
|
193
|
+
payload.headers.pop(Header.CONTENT_TYPE.value, None)
|
|
194
|
+
|
|
195
|
+
if payload.data is not None:
|
|
196
|
+
if isinstance(payload.data, bytes):
|
|
197
|
+
self.io.log(f"Sending binary payload ({len(payload.data)} bytes)")
|
|
198
|
+
else:
|
|
199
|
+
self.io.log(f"Sending {type(payload.data).__name__} payload")
|
|
200
|
+
|
|
201
|
+
request_kwargs: dict[str, Any] = {
|
|
202
|
+
"method": payload.method.value,
|
|
203
|
+
"url": payload.url,
|
|
204
|
+
"params": payload.query_params,
|
|
205
|
+
"headers": payload.headers,
|
|
206
|
+
"timeout": timeout or self.timeout,
|
|
207
|
+
"stream": stream,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if files:
|
|
211
|
+
request_kwargs["data"] = data or {}
|
|
212
|
+
request_kwargs["files"] = files
|
|
213
|
+
elif content_type in (
|
|
214
|
+
ContentType.FORM_URLENCODED.value,
|
|
215
|
+
ContentType.OCTET_STREAM.value,
|
|
216
|
+
ContentType.TEXT.value,
|
|
217
|
+
):
|
|
218
|
+
request_kwargs["data"] = data
|
|
219
|
+
else:
|
|
220
|
+
request_kwargs["json"] = data
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
response = requests.request(**request_kwargs)
|
|
224
|
+
except requests.exceptions.RequestException as exc:
|
|
225
|
+
gateway_error = GatewayError(f"Request failed: {exc}")
|
|
226
|
+
gateway_error.__cause__ = exc
|
|
227
|
+
|
|
228
|
+
if raise_exceptions:
|
|
229
|
+
raise gateway_error
|
|
230
|
+
|
|
231
|
+
return self.handle_api_response(
|
|
232
|
+
response=None,
|
|
233
|
+
request_context=payload,
|
|
234
|
+
exception=gateway_error,
|
|
235
|
+
fatal_on_error=fatal_if_unexpected,
|
|
236
|
+
quiet=quiet,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Only check status code if expected_status_codes is explicitly provided
|
|
240
|
+
exception = None
|
|
241
|
+
if expected_status_codes is not None:
|
|
242
|
+
expected = (
|
|
243
|
+
{expected_status_codes}
|
|
244
|
+
if isinstance(expected_status_codes, int)
|
|
245
|
+
else set(expected_status_codes)
|
|
246
|
+
)
|
|
247
|
+
if response.status_code not in expected:
|
|
248
|
+
exception = GatewayError(self._extract_error_message(response))
|
|
249
|
+
exception.response = (
|
|
250
|
+
response # Attach response to exception for debugging
|
|
251
|
+
)
|
|
252
|
+
if raise_exceptions and exception:
|
|
253
|
+
raise exception
|
|
254
|
+
|
|
255
|
+
return self.handle_api_response(
|
|
256
|
+
response=response,
|
|
257
|
+
request_context=payload,
|
|
258
|
+
exception=exception,
|
|
259
|
+
fatal_on_error=fatal_if_unexpected,
|
|
260
|
+
quiet=quiet,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def setup(self) -> AbstractGateway:
|
|
264
|
+
if self.default_headers is None:
|
|
265
|
+
self.default_headers = {}
|
|
266
|
+
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
def _create_request_details(
|
|
270
|
+
self, request_context: HttpRequestPayload, status_code: int | None = None
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
"""Create request details dictionary for logging."""
|
|
273
|
+
from wexample_helpers.helpers.cli import cli_make_clickable_path
|
|
274
|
+
|
|
275
|
+
details: dict[str, Any] = {
|
|
276
|
+
"URL": request_context.url,
|
|
277
|
+
"Method": request_context.method,
|
|
278
|
+
}
|
|
279
|
+
if request_context.call_origin:
|
|
280
|
+
details["Call Origin"] = cli_make_clickable_path(
|
|
281
|
+
request_context.call_origin
|
|
282
|
+
)
|
|
283
|
+
if request_context.data:
|
|
284
|
+
if isinstance(request_context.data, bytes):
|
|
285
|
+
details["Data"] = f"<Binary data: {len(request_context.data)} bytes>"
|
|
286
|
+
else:
|
|
287
|
+
details["Data"] = request_context.data
|
|
288
|
+
if request_context.query_params:
|
|
289
|
+
details["Query Parameters"] = request_context.query_params
|
|
290
|
+
if status_code is not None:
|
|
291
|
+
details["Status"] = status_code
|
|
292
|
+
return details
|
|
293
|
+
|
|
294
|
+
def _extract_error_message(self, response: requests.Response) -> str:
|
|
295
|
+
"""Extract error message from response."""
|
|
296
|
+
message = f"HTTP {response.status_code}"
|
|
297
|
+
try:
|
|
298
|
+
data = response.json()
|
|
299
|
+
if isinstance(data, dict):
|
|
300
|
+
message = data.get("message", data.get("error", message))
|
|
301
|
+
except (ValueError, AttributeError):
|
|
302
|
+
if response.text:
|
|
303
|
+
message = response.text
|
|
304
|
+
return message
|
|
305
|
+
|
|
306
|
+
def _get_header_value(
|
|
307
|
+
self,
|
|
308
|
+
headers: Mapping[str, str] | None,
|
|
309
|
+
name: Header,
|
|
310
|
+
) -> str | None:
|
|
311
|
+
"""
|
|
312
|
+
Case-insensitive lookup of a header followed by normalisation:
|
|
313
|
+
- keep only the part before the first ';'
|
|
314
|
+
- trim whitespace
|
|
315
|
+
- convert to lower-case
|
|
316
|
+
"""
|
|
317
|
+
if not headers:
|
|
318
|
+
return None
|
|
319
|
+
raw = next(
|
|
320
|
+
(v for k, v in headers.items() if k.lower() == name.value.lower()),
|
|
321
|
+
None,
|
|
322
|
+
)
|
|
323
|
+
if raw is None:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
return raw.split(";", 1)[0].strip().lower() or None
|
|
327
|
+
|
|
328
|
+
def _handle_rate_limiting(self) -> None:
|
|
329
|
+
if self.last_request_time is not None:
|
|
330
|
+
elapsed = time.time() - self.last_request_time
|
|
331
|
+
if elapsed < self.rate_limit_delay:
|
|
332
|
+
time.sleep(self.rate_limit_delay - elapsed)
|
|
333
|
+
self.last_request_time = time.time()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
from wexample_api.enums.http import HttpMethod
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@base_class
|
|
13
|
+
class HttpRequestPayload(BaseClass):
|
|
14
|
+
call_origin: str | None = public_field(
|
|
15
|
+
default=None,
|
|
16
|
+
description="Optional identifier of the request origin",
|
|
17
|
+
)
|
|
18
|
+
data: dict[str, Any] | bytes | None = public_field(
|
|
19
|
+
default=None,
|
|
20
|
+
description="Request body as a dictionary, raw bytes, or None",
|
|
21
|
+
)
|
|
22
|
+
expected_status_codes: list[int] | None = public_field(
|
|
23
|
+
default=None,
|
|
24
|
+
description="Optional list of expected HTTP status codes. If None, all responses are accepted.",
|
|
25
|
+
)
|
|
26
|
+
headers: dict[str, str] | None = public_field(
|
|
27
|
+
default=None,
|
|
28
|
+
description="Optional HTTP headers for the request",
|
|
29
|
+
)
|
|
30
|
+
method: HttpMethod = public_field(
|
|
31
|
+
default=HttpMethod.GET,
|
|
32
|
+
description="HTTP method to use for the request",
|
|
33
|
+
)
|
|
34
|
+
query_params: dict[str, Any] | None = public_field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Optional query parameters to append to the URL",
|
|
37
|
+
)
|
|
38
|
+
url: str = public_field(
|
|
39
|
+
description="Target URL for the HTTP request",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_endpoint(
|
|
44
|
+
cls,
|
|
45
|
+
base_url: str | None,
|
|
46
|
+
endpoint: str,
|
|
47
|
+
method: HttpMethod = HttpMethod.GET,
|
|
48
|
+
data: dict[str, Any] | bytes | None = None,
|
|
49
|
+
query_params: dict[str, Any] | None = None,
|
|
50
|
+
headers: dict[str, str] | None = None,
|
|
51
|
+
call_origin: str | None = None,
|
|
52
|
+
expected_status_codes: int | list[int] | None = None,
|
|
53
|
+
) -> HttpRequestPayload:
|
|
54
|
+
if base_url:
|
|
55
|
+
url = f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
56
|
+
else:
|
|
57
|
+
url = endpoint
|
|
58
|
+
|
|
59
|
+
if isinstance(expected_status_codes, int):
|
|
60
|
+
expected_status_codes = [expected_status_codes]
|
|
61
|
+
|
|
62
|
+
return cls(
|
|
63
|
+
url=url,
|
|
64
|
+
method=method,
|
|
65
|
+
data=data,
|
|
66
|
+
query_params=query_params,
|
|
67
|
+
headers=headers,
|
|
68
|
+
call_origin=call_origin,
|
|
69
|
+
expected_status_codes=expected_status_codes,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_url(cls, url: str, call_origin: str | None = None) -> HttpRequestPayload:
|
|
74
|
+
return cls(url=url, call_origin=call_origin)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from wexample_api.enums.http import HttpMethod
|
|
4
|
+
|
|
5
|
+
HTTP_METHOD_MAP = {
|
|
6
|
+
"GET": HttpMethod.GET,
|
|
7
|
+
"POST": HttpMethod.POST,
|
|
8
|
+
"PUT": HttpMethod.PUT,
|
|
9
|
+
"DELETE": HttpMethod.DELETE,
|
|
10
|
+
"PATCH": HttpMethod.PATCH,
|
|
11
|
+
"OPTIONS": HttpMethod.OPTIONS,
|
|
12
|
+
"HEAD": HttpMethod.HEAD,
|
|
13
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from wexample_api.common.abstract_gateway import AbstractGateway
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@base_class
|
|
11
|
+
class DemoSimpleGateway(AbstractGateway):
|
|
12
|
+
"""A simple implementation of AbstractGateway for demonstration purposes."""
|
|
13
|
+
|
|
14
|
+
def check_connection(self) -> bool:
|
|
15
|
+
# Always return True for demo purposes
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
def create_item(self, item_data: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
"""Demo method to create an item."""
|
|
20
|
+
from wexample_api.enums.http import HttpMethod
|
|
21
|
+
|
|
22
|
+
response = self.make_request(
|
|
23
|
+
method=HttpMethod.POST,
|
|
24
|
+
endpoint="/items",
|
|
25
|
+
data=item_data,
|
|
26
|
+
call_origin=__file__,
|
|
27
|
+
)
|
|
28
|
+
return response.json()
|
|
29
|
+
|
|
30
|
+
def delete_item(self, item_id: str) -> None:
|
|
31
|
+
"""Demo method to delete an item."""
|
|
32
|
+
from wexample_api.enums.http import HttpMethod
|
|
33
|
+
|
|
34
|
+
self.make_request(
|
|
35
|
+
method=HttpMethod.DELETE, endpoint=f"/items/{item_id}", call_origin=__file__
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def get_user_info(self) -> dict[str, Any]:
|
|
39
|
+
"""Demo method to get user information."""
|
|
40
|
+
from wexample_api.enums.http import HttpMethod
|
|
41
|
+
|
|
42
|
+
response = self.make_request(
|
|
43
|
+
method=HttpMethod.GET, endpoint="/user", call_origin=__file__
|
|
44
|
+
)
|
|
45
|
+
return response.json()
|
|
46
|
+
|
|
47
|
+
def update_item(self, item_id: str, item_data: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
"""Demo method to update an item."""
|
|
49
|
+
from wexample_api.enums.http import HttpMethod
|
|
50
|
+
|
|
51
|
+
response = self.make_request(
|
|
52
|
+
method=HttpMethod.PUT,
|
|
53
|
+
endpoint=f"/items/{item_id}",
|
|
54
|
+
data=item_data,
|
|
55
|
+
call_origin=__file__,
|
|
56
|
+
)
|
|
57
|
+
return response.json()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HttpMethod(Enum):
|
|
7
|
+
GET: str = "GET"
|
|
8
|
+
POST: str = "POST"
|
|
9
|
+
PUT: str = "PUT"
|
|
10
|
+
DELETE: str = "DELETE"
|
|
11
|
+
PATCH: str = "PATCH"
|
|
12
|
+
OPTIONS: str = "OPTIONS"
|
|
13
|
+
HEAD: str = "HEAD"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContentType(Enum):
|
|
17
|
+
JSON: str = "application/json"
|
|
18
|
+
FORM_URLENCODED: str = "application/x-www-form-urlencoded"
|
|
19
|
+
MULTIPART: str = "multipart/form-data"
|
|
20
|
+
TEXT: str = "text/plain"
|
|
21
|
+
OCTET_STREAM: str = "application/octet-stream"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Header(Enum):
|
|
25
|
+
CONTENT_TYPE: str = "Content-Type"
|
|
26
|
+
AUTHORIZATION: str = "Authorization"
|
|
File without changes
|
wexample_api/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: wexample-api
|
|
3
|
+
Version: 0.0.74
|
|
4
|
+
Summary: Some python basic helpers for apis.
|
|
5
|
+
Author-Email: weeger <contact@wexample.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Project-URL: homepage, https://github.com/wexample/python-api
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: attrs>=23.1.0
|
|
13
|
+
Requires-Dist: cattrs>=23.1.0
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: wexample-helpers==0.0.78
|
|
16
|
+
Requires-Dist: wexample-prompt==0.0.90
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# wexample-api
|
|
23
|
+
|
|
24
|
+
Version: 0.0.74
|
|
25
|
+
|
|
26
|
+
Some python basic helpers for apis.
|
|
27
|
+
|
|
28
|
+
## Tests
|
|
29
|
+
|
|
30
|
+
This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
First, install the required testing dependencies:
|
|
35
|
+
```bash
|
|
36
|
+
.venv/bin/python -m pip install pytest pytest-cov
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Basic Usage
|
|
40
|
+
|
|
41
|
+
Run all tests with coverage:
|
|
42
|
+
```bash
|
|
43
|
+
.venv/bin/python -m pytest --cov --cov-report=html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Common Commands
|
|
47
|
+
```bash
|
|
48
|
+
# Run tests with coverage for a specific module
|
|
49
|
+
.venv/bin/python -m pytest --cov=your_module
|
|
50
|
+
|
|
51
|
+
# Show which lines are not covered
|
|
52
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
|
|
53
|
+
|
|
54
|
+
# Generate an HTML coverage report
|
|
55
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=html
|
|
56
|
+
|
|
57
|
+
# Combine terminal and HTML reports
|
|
58
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
|
|
59
|
+
|
|
60
|
+
# Run specific test file with coverage
|
|
61
|
+
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Viewing HTML Reports
|
|
65
|
+
|
|
66
|
+
After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
|
|
67
|
+
|
|
68
|
+
### Coverage Threshold
|
|
69
|
+
|
|
70
|
+
To enforce a minimum coverage percentage:
|
|
71
|
+
```bash
|
|
72
|
+
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This will cause the test suite to fail if coverage drops below 80%.
|
|
76
|
+
|
|
77
|
+
## Code Quality & Typing
|
|
78
|
+
|
|
79
|
+
All the suite packages follow strict quality standards:
|
|
80
|
+
|
|
81
|
+
- **Type hints**: Full type coverage with mypy validation
|
|
82
|
+
- **Code formatting**: Enforced with black and isort
|
|
83
|
+
- **Linting**: Comprehensive checks with custom scripts and tools
|
|
84
|
+
- **Testing**: High test coverage requirements
|
|
85
|
+
|
|
86
|
+
These standards ensure reliability and maintainability across the suite.
|
|
87
|
+
|
|
88
|
+
## Versioning & Compatibility Policy
|
|
89
|
+
|
|
90
|
+
Wexample packages follow **Semantic Versioning** (SemVer):
|
|
91
|
+
|
|
92
|
+
- **MAJOR**: Breaking changes
|
|
93
|
+
- **MINOR**: New features, backward compatible
|
|
94
|
+
- **PATCH**: Bug fixes, backward compatible
|
|
95
|
+
|
|
96
|
+
We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
|
|
97
|
+
|
|
98
|
+
## Changelog
|
|
99
|
+
|
|
100
|
+
See [CHANGELOG.md](CHANGELOG.md) for detailed version history and release notes.
|
|
101
|
+
|
|
102
|
+
Major changes are documented with migration guides when applicable.
|
|
103
|
+
|
|
104
|
+
## Migration Notes
|
|
105
|
+
|
|
106
|
+
When upgrading between major versions, refer to the migration guides in the documentation.
|
|
107
|
+
|
|
108
|
+
Breaking changes are clearly documented with upgrade paths and examples.
|
|
109
|
+
|
|
110
|
+
## Known Limitations & Roadmap
|
|
111
|
+
|
|
112
|
+
Current limitations and planned features are tracked in the GitHub issues.
|
|
113
|
+
|
|
114
|
+
See the [project roadmap](https://github.com/wexample/python-api/issues) for upcoming features and improvements.
|
|
115
|
+
|
|
116
|
+
## Security Policy
|
|
117
|
+
|
|
118
|
+
### Reporting Vulnerabilities
|
|
119
|
+
|
|
120
|
+
If you discover a security vulnerability, please email security@wexample.com.
|
|
121
|
+
|
|
122
|
+
**Do not** open public issues for security vulnerabilities.
|
|
123
|
+
|
|
124
|
+
We take security seriously and will respond promptly to verified reports.
|
|
125
|
+
|
|
126
|
+
## Privacy & Telemetry
|
|
127
|
+
|
|
128
|
+
This package does **not** collect any telemetry or usage data.
|
|
129
|
+
|
|
130
|
+
Your privacy is respected — no data is transmitted to external services.
|
|
131
|
+
|
|
132
|
+
## Support Channels
|
|
133
|
+
|
|
134
|
+
- **GitHub Issues**: Bug reports and feature requests
|
|
135
|
+
- **GitHub Discussions**: Questions and community support
|
|
136
|
+
- **Documentation**: Comprehensive guides and API reference
|
|
137
|
+
- **Email**: contact@wexample.com for general inquiries
|
|
138
|
+
|
|
139
|
+
Community support is available through GitHub Discussions.
|
|
140
|
+
|
|
141
|
+
## Contribution Guidelines
|
|
142
|
+
|
|
143
|
+
We welcome contributions to the Wexample suite!
|
|
144
|
+
|
|
145
|
+
### How to Contribute
|
|
146
|
+
|
|
147
|
+
1. **Fork** the repository
|
|
148
|
+
2. **Create** a feature branch
|
|
149
|
+
3. **Make** your changes
|
|
150
|
+
4. **Test** thoroughly
|
|
151
|
+
5. **Submit** a pull request
|
|
152
|
+
|
|
153
|
+
## Maintainers & Authors
|
|
154
|
+
|
|
155
|
+
Maintained by the Wexample team and community contributors.
|
|
156
|
+
|
|
157
|
+
See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list of contributors.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
162
|
+
|
|
163
|
+
## Useful Links
|
|
164
|
+
|
|
165
|
+
- **Homepage**: https://github.com/wexample/python-api
|
|
166
|
+
- **Documentation**: [docs.wexample.com](https://docs.wexample.com)
|
|
167
|
+
- **Issue Tracker**: https://github.com/wexample/python-api/issues
|
|
168
|
+
- **Discussions**: https://github.com/wexample/python-api/discussions
|
|
169
|
+
- **PyPI**: [pypi.org/project/wexample-api](https://pypi.org/project/wexample-api/)
|
|
170
|
+
|
|
171
|
+
## Integration in the Suite
|
|
172
|
+
|
|
173
|
+
This package is part of the **Wexample Suite** — a collection of high-quality Python packages designed to work seamlessly together.
|
|
174
|
+
|
|
175
|
+
### Related Packages
|
|
176
|
+
|
|
177
|
+
The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
|
|
178
|
+
|
|
179
|
+
Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.
|
|
180
|
+
|
|
181
|
+
# About us
|
|
182
|
+
|
|
183
|
+
Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
|
|
184
|
+
|
|
185
|
+
This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
|
|
186
|
+
|
|
187
|
+
Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
|
|
188
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
wexample_api-0.0.74.dist-info/METADATA,sha256=xL8OD4dLANc7ahqesbasbn-bd1xLIEf7bxYzNPsGTBI,6334
|
|
2
|
+
wexample_api-0.0.74.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
wexample_api-0.0.74.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
wexample_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
wexample_api/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
wexample_api/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
wexample_api/common/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
wexample_api/common/abstract_gateway.py,sha256=iNl4c0T-v0FxQpJCT4gJjTtxSlON6I5Y434Xsknt5B0,11484
|
|
9
|
+
wexample_api/common/http_request_payload.py,sha256=qqIlOBgvE-8tiXx8W7VWtcDCEgX_xphTrUwmxNcYum8,2436
|
|
10
|
+
wexample_api/const/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
wexample_api/const/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
wexample_api/const/http.py,sha256=S7S28kqVr_Cm6bXExDDAl9Q1tK2Nipb1gFPVkvtiKT8,317
|
|
13
|
+
wexample_api/demo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
wexample_api/demo/demo_simple_gateway.py,sha256=8U1TqVJso77uhL2lumh92uBboYFVAw1bwWVmbGewG34,1805
|
|
15
|
+
wexample_api/enums/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
wexample_api/enums/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
wexample_api/enums/http.py,sha256=1r2tV2EtIAxrwSBYR2X8V66jSLeTtPWoPC7Hb9w9tzc,603
|
|
18
|
+
wexample_api/errors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
wexample_api/errors/gateway_authentication_error.py,sha256=M2nG1CDSY1Fwa3RKbXMaaNjCFkJTkeHlsDl6JLUWg7Q,204
|
|
20
|
+
wexample_api/errors/gateway_connexion_error.py,sha256=ym5DtLcH_hDABPUuGzzBipfKbRp2fiI44rZ2U7usEHY,196
|
|
21
|
+
wexample_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
wexample_api-0.0.74.dist-info/RECORD,,
|