bustapi 0.1.0__cp311-cp311-manylinux_2_34_x86_64.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.
Potentially problematic release.
This version of bustapi might be problematic. Click here for more details.
- bustapi/__init__.py +96 -0
- bustapi/app.py +552 -0
- bustapi/blueprints.py +500 -0
- bustapi/bustapi_core.cpython-311-x86_64-linux-gnu.so +0 -0
- bustapi/exceptions.py +438 -0
- bustapi/flask_compat.py +62 -0
- bustapi/helpers.py +370 -0
- bustapi/py.typed +0 -0
- bustapi/request.py +378 -0
- bustapi/response.py +406 -0
- bustapi/testing.py +375 -0
- bustapi-0.1.0.dist-info/METADATA +233 -0
- bustapi-0.1.0.dist-info/RECORD +16 -0
- bustapi-0.1.0.dist-info/WHEEL +4 -0
- bustapi-0.1.0.dist-info/entry_points.txt +2 -0
- bustapi-0.1.0.dist-info/licenses/LICENSE +21 -0
bustapi/exceptions.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception classes for BustAPI - Flask-compatible exceptions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BustAPIException(Exception):
|
|
10
|
+
"""Base exception class for BustAPI."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTTPException(BustAPIException):
|
|
16
|
+
"""
|
|
17
|
+
HTTP exception for error responses (Flask-compatible).
|
|
18
|
+
|
|
19
|
+
This exception is raised when an HTTP error response should be returned.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, code: int, description: Optional[str] = None, response=None):
|
|
23
|
+
"""
|
|
24
|
+
Initialize HTTP exception.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
code: HTTP status code
|
|
28
|
+
description: Error description
|
|
29
|
+
response: Response object
|
|
30
|
+
"""
|
|
31
|
+
self.code = code
|
|
32
|
+
self.name = self._get_name(code)
|
|
33
|
+
self.description = description or self._get_default_description(code)
|
|
34
|
+
self.response = response
|
|
35
|
+
|
|
36
|
+
super().__init__(f"{self.code} {self.name}: {self.description}")
|
|
37
|
+
|
|
38
|
+
def _get_name(self, code: int) -> str:
|
|
39
|
+
"""Get name for HTTP status code."""
|
|
40
|
+
try:
|
|
41
|
+
return HTTPStatus(code).name.replace("_", " ").title()
|
|
42
|
+
except ValueError:
|
|
43
|
+
return f"HTTP {code}"
|
|
44
|
+
|
|
45
|
+
def _get_default_description(self, code: int) -> str:
|
|
46
|
+
"""Get default description for HTTP status code."""
|
|
47
|
+
try:
|
|
48
|
+
return HTTPStatus(code).phrase
|
|
49
|
+
except ValueError:
|
|
50
|
+
return f"HTTP Error {code}"
|
|
51
|
+
|
|
52
|
+
def get_response(self):
|
|
53
|
+
"""
|
|
54
|
+
Get response object for this exception.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Response object
|
|
58
|
+
"""
|
|
59
|
+
if self.response is not None:
|
|
60
|
+
return self.response
|
|
61
|
+
|
|
62
|
+
from .response import Response
|
|
63
|
+
|
|
64
|
+
return Response(self.description, status=self.code)
|
|
65
|
+
|
|
66
|
+
def get_body(self) -> str:
|
|
67
|
+
"""Get response body."""
|
|
68
|
+
return self.description
|
|
69
|
+
|
|
70
|
+
def get_headers(self) -> dict:
|
|
71
|
+
"""Get response headers."""
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Common HTTP exceptions (Flask-compatible)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BadRequest(HTTPException):
|
|
79
|
+
"""400 Bad Request exception."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
82
|
+
super().__init__(400, description, response)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Unauthorized(HTTPException):
|
|
86
|
+
"""401 Unauthorized exception."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
89
|
+
super().__init__(401, description, response)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Forbidden(HTTPException):
|
|
93
|
+
"""403 Forbidden exception."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
96
|
+
super().__init__(403, description, response)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class NotFound(HTTPException):
|
|
100
|
+
"""404 Not Found exception."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
103
|
+
super().__init__(404, description, response)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class MethodNotAllowed(HTTPException):
|
|
107
|
+
"""405 Method Not Allowed exception."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self, description: Optional[str] = None, response=None, valid_methods=None
|
|
111
|
+
):
|
|
112
|
+
super().__init__(405, description, response)
|
|
113
|
+
self.valid_methods = valid_methods or []
|
|
114
|
+
|
|
115
|
+
def get_headers(self) -> dict:
|
|
116
|
+
"""Get response headers including Allow header."""
|
|
117
|
+
headers = super().get_headers()
|
|
118
|
+
if self.valid_methods:
|
|
119
|
+
headers["Allow"] = ", ".join(self.valid_methods)
|
|
120
|
+
return headers
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NotAcceptable(HTTPException):
|
|
124
|
+
"""406 Not Acceptable exception."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
127
|
+
super().__init__(406, description, response)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RequestTimeout(HTTPException):
|
|
131
|
+
"""408 Request Timeout exception."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
134
|
+
super().__init__(408, description, response)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Conflict(HTTPException):
|
|
138
|
+
"""409 Conflict exception."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
141
|
+
super().__init__(409, description, response)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Gone(HTTPException):
|
|
145
|
+
"""410 Gone exception."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
148
|
+
super().__init__(410, description, response)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class LengthRequired(HTTPException):
|
|
152
|
+
"""411 Length Required exception."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
155
|
+
super().__init__(411, description, response)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class PreconditionFailed(HTTPException):
|
|
159
|
+
"""412 Precondition Failed exception."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
162
|
+
super().__init__(412, description, response)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class RequestEntityTooLarge(HTTPException):
|
|
166
|
+
"""413 Request Entity Too Large exception."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
169
|
+
super().__init__(413, description, response)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class RequestURITooLarge(HTTPException):
|
|
173
|
+
"""414 Request-URI Too Large exception."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
176
|
+
super().__init__(414, description, response)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class UnsupportedMediaType(HTTPException):
|
|
180
|
+
"""415 Unsupported Media Type exception."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
183
|
+
super().__init__(415, description, response)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class RequestedRangeNotSatisfiable(HTTPException):
|
|
187
|
+
"""416 Requested Range Not Satisfiable exception."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
190
|
+
super().__init__(416, description, response)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ExpectationFailed(HTTPException):
|
|
194
|
+
"""417 Expectation Failed exception."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
197
|
+
super().__init__(417, description, response)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ImATeapot(HTTPException):
|
|
201
|
+
"""418 I'm a teapot exception (RFC 2324)."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
204
|
+
super().__init__(418, description, response)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class UnprocessableEntity(HTTPException):
|
|
208
|
+
"""422 Unprocessable Entity exception."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
211
|
+
super().__init__(422, description, response)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Locked(HTTPException):
|
|
215
|
+
"""423 Locked exception."""
|
|
216
|
+
|
|
217
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
218
|
+
super().__init__(423, description, response)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class FailedDependency(HTTPException):
|
|
222
|
+
"""424 Failed Dependency exception."""
|
|
223
|
+
|
|
224
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
225
|
+
super().__init__(424, description, response)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class PreconditionRequired(HTTPException):
|
|
229
|
+
"""428 Precondition Required exception."""
|
|
230
|
+
|
|
231
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
232
|
+
super().__init__(428, description, response)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TooManyRequests(HTTPException):
|
|
236
|
+
"""429 Too Many Requests exception."""
|
|
237
|
+
|
|
238
|
+
def __init__(
|
|
239
|
+
self, description: Optional[str] = None, response=None, retry_after=None
|
|
240
|
+
):
|
|
241
|
+
super().__init__(429, description, response)
|
|
242
|
+
self.retry_after = retry_after
|
|
243
|
+
|
|
244
|
+
def get_headers(self) -> dict:
|
|
245
|
+
"""Get response headers including Retry-After header."""
|
|
246
|
+
headers = super().get_headers()
|
|
247
|
+
if self.retry_after:
|
|
248
|
+
headers["Retry-After"] = str(self.retry_after)
|
|
249
|
+
return headers
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class RequestHeaderFieldsTooLarge(HTTPException):
|
|
253
|
+
"""431 Request Header Fields Too Large exception."""
|
|
254
|
+
|
|
255
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
256
|
+
super().__init__(431, description, response)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class UnavailableForLegalReasons(HTTPException):
|
|
260
|
+
"""451 Unavailable For Legal Reasons exception."""
|
|
261
|
+
|
|
262
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
263
|
+
super().__init__(451, description, response)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# 5xx Server Error exceptions
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class InternalServerError(HTTPException):
|
|
270
|
+
"""500 Internal Server Error exception."""
|
|
271
|
+
|
|
272
|
+
def __init__(
|
|
273
|
+
self, description: Optional[str] = None, response=None, original_exception=None
|
|
274
|
+
):
|
|
275
|
+
super().__init__(500, description, response)
|
|
276
|
+
self.original_exception = original_exception
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class NotImplemented(HTTPException):
|
|
280
|
+
"""501 Not Implemented exception."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
283
|
+
super().__init__(501, description, response)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class BadGateway(HTTPException):
|
|
287
|
+
"""502 Bad Gateway exception."""
|
|
288
|
+
|
|
289
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
290
|
+
super().__init__(502, description, response)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ServiceUnavailable(HTTPException):
|
|
294
|
+
"""503 Service Unavailable exception."""
|
|
295
|
+
|
|
296
|
+
def __init__(
|
|
297
|
+
self, description: Optional[str] = None, response=None, retry_after=None
|
|
298
|
+
):
|
|
299
|
+
super().__init__(503, description, response)
|
|
300
|
+
self.retry_after = retry_after
|
|
301
|
+
|
|
302
|
+
def get_headers(self) -> dict:
|
|
303
|
+
"""Get response headers including Retry-After header."""
|
|
304
|
+
headers = super().get_headers()
|
|
305
|
+
if self.retry_after:
|
|
306
|
+
headers["Retry-After"] = str(self.retry_after)
|
|
307
|
+
return headers
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class GatewayTimeout(HTTPException):
|
|
311
|
+
"""504 Gateway Timeout exception."""
|
|
312
|
+
|
|
313
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
314
|
+
super().__init__(504, description, response)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class HTTPVersionNotSupported(HTTPException):
|
|
318
|
+
"""505 HTTP Version Not Supported exception."""
|
|
319
|
+
|
|
320
|
+
def __init__(self, description: Optional[str] = None, response=None):
|
|
321
|
+
super().__init__(505, description, response)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Application-specific exceptions
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class ConfigurationError(BustAPIException):
|
|
328
|
+
"""Configuration error exception."""
|
|
329
|
+
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class TemplateNotFound(BustAPIException):
|
|
334
|
+
"""Template not found exception."""
|
|
335
|
+
|
|
336
|
+
def __init__(self, template_name: str):
|
|
337
|
+
self.template_name = template_name
|
|
338
|
+
super().__init__(f"Template '{template_name}' not found")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class BlueprintSetupError(BustAPIException):
|
|
342
|
+
"""Blueprint setup error exception."""
|
|
343
|
+
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class SecurityError(BustAPIException):
|
|
348
|
+
"""Security-related error exception."""
|
|
349
|
+
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# Utility functions
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def abort(code: int, description: Optional[str] = None, **kwargs):
|
|
357
|
+
"""
|
|
358
|
+
Abort request with HTTP error code.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
code: HTTP status code
|
|
362
|
+
description: Error description
|
|
363
|
+
**kwargs: Additional arguments
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
HTTPException: HTTP exception with specified code
|
|
367
|
+
"""
|
|
368
|
+
# Map common status codes to specific exception classes
|
|
369
|
+
exception_map = {
|
|
370
|
+
400: BadRequest,
|
|
371
|
+
401: Unauthorized,
|
|
372
|
+
403: Forbidden,
|
|
373
|
+
404: NotFound,
|
|
374
|
+
405: MethodNotAllowed,
|
|
375
|
+
406: NotAcceptable,
|
|
376
|
+
408: RequestTimeout,
|
|
377
|
+
409: Conflict,
|
|
378
|
+
410: Gone,
|
|
379
|
+
411: LengthRequired,
|
|
380
|
+
412: PreconditionFailed,
|
|
381
|
+
413: RequestEntityTooLarge,
|
|
382
|
+
414: RequestURITooLarge,
|
|
383
|
+
415: UnsupportedMediaType,
|
|
384
|
+
416: RequestedRangeNotSatisfiable,
|
|
385
|
+
417: ExpectationFailed,
|
|
386
|
+
418: ImATeapot,
|
|
387
|
+
422: UnprocessableEntity,
|
|
388
|
+
423: Locked,
|
|
389
|
+
424: FailedDependency,
|
|
390
|
+
428: PreconditionRequired,
|
|
391
|
+
429: TooManyRequests,
|
|
392
|
+
431: RequestHeaderFieldsTooLarge,
|
|
393
|
+
451: UnavailableForLegalReasons,
|
|
394
|
+
500: InternalServerError,
|
|
395
|
+
501: NotImplemented,
|
|
396
|
+
502: BadGateway,
|
|
397
|
+
503: ServiceUnavailable,
|
|
398
|
+
504: GatewayTimeout,
|
|
399
|
+
505: HTTPVersionNotSupported,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
exception_class = exception_map.get(code, HTTPException)
|
|
403
|
+
if exception_class == HTTPException:
|
|
404
|
+
raise exception_class(code, description, **kwargs)
|
|
405
|
+
else:
|
|
406
|
+
# Filter kwargs for specific exception classes
|
|
407
|
+
if code in (405, 429, 503) and "valid_methods" in kwargs:
|
|
408
|
+
raise exception_class(description, **kwargs)
|
|
409
|
+
else:
|
|
410
|
+
raise exception_class(description)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# Exception handler registry
|
|
414
|
+
_exception_handlers = {}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def register_error_handler(code_or_exception, handler):
|
|
418
|
+
"""
|
|
419
|
+
Register error handler for HTTP status code or exception.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
code_or_exception: HTTP status code or exception class
|
|
423
|
+
handler: Handler function
|
|
424
|
+
"""
|
|
425
|
+
_exception_handlers[code_or_exception] = handler
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def get_error_handler(code_or_exception):
|
|
429
|
+
"""
|
|
430
|
+
Get error handler for HTTP status code or exception.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
code_or_exception: HTTP status code or exception class
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Handler function or None
|
|
437
|
+
"""
|
|
438
|
+
return _exception_handlers.get(code_or_exception)
|
bustapi/flask_compat.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask compatibility layer for BustAPI
|
|
3
|
+
|
|
4
|
+
This module provides Flask class as an alias for BustAPI to enable drop-in replacement.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .app import BustAPI
|
|
8
|
+
from .blueprints import Blueprint
|
|
9
|
+
from .exceptions import HTTPException
|
|
10
|
+
from .helpers import (
|
|
11
|
+
abort,
|
|
12
|
+
flash,
|
|
13
|
+
get_flashed_messages,
|
|
14
|
+
redirect,
|
|
15
|
+
render_template,
|
|
16
|
+
render_template_string,
|
|
17
|
+
send_file,
|
|
18
|
+
send_from_directory,
|
|
19
|
+
url_for,
|
|
20
|
+
)
|
|
21
|
+
from .request import request
|
|
22
|
+
from .response import Response, jsonify, make_response
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Flask(BustAPI):
|
|
26
|
+
"""
|
|
27
|
+
Flask compatibility class - direct alias for BustAPI.
|
|
28
|
+
|
|
29
|
+
This allows BustAPI to be used as a drop-in replacement for Flask:
|
|
30
|
+
|
|
31
|
+
Instead of:
|
|
32
|
+
from flask import Flask
|
|
33
|
+
app = Flask(__name__)
|
|
34
|
+
|
|
35
|
+
Use:
|
|
36
|
+
from bustapi import Flask # or: from bustapi.flask_compat import Flask
|
|
37
|
+
app = Flask(__name__)
|
|
38
|
+
|
|
39
|
+
All Flask functionality is available through BustAPI's implementation.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Flask",
|
|
47
|
+
"request",
|
|
48
|
+
"jsonify",
|
|
49
|
+
"make_response",
|
|
50
|
+
"Response",
|
|
51
|
+
"abort",
|
|
52
|
+
"redirect",
|
|
53
|
+
"url_for",
|
|
54
|
+
"render_template",
|
|
55
|
+
"render_template_string",
|
|
56
|
+
"flash",
|
|
57
|
+
"get_flashed_messages",
|
|
58
|
+
"send_file",
|
|
59
|
+
"send_from_directory",
|
|
60
|
+
"HTTPException",
|
|
61
|
+
"Blueprint",
|
|
62
|
+
]
|