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/__init__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BustAPI - High-performance Flask-compatible web framework
|
|
3
|
+
|
|
4
|
+
BustAPI is a Flask-compatible Python web framework built with a Rust backend
|
|
5
|
+
using PyO3. It provides high performance while maintaining Flask's ease of use.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from bustapi import BustAPI
|
|
9
|
+
|
|
10
|
+
app = BustAPI()
|
|
11
|
+
|
|
12
|
+
@app.route('/')
|
|
13
|
+
def hello():
|
|
14
|
+
return {'message': 'Hello, World!'}
|
|
15
|
+
|
|
16
|
+
if __name__ == '__main__':
|
|
17
|
+
app.run(debug=True)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import platform
|
|
22
|
+
import sys
|
|
23
|
+
from http import HTTPStatus
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
__author__ = "BustAPI Team"
|
|
27
|
+
__email__ = "hello@bustapi.dev"
|
|
28
|
+
|
|
29
|
+
# Import core classes and functions
|
|
30
|
+
from .app import BustAPI
|
|
31
|
+
from .blueprints import Blueprint
|
|
32
|
+
from .flask_compat import Flask
|
|
33
|
+
from .helpers import abort, redirect, url_for
|
|
34
|
+
from .request import Request, request
|
|
35
|
+
from .response import Response, jsonify, make_response
|
|
36
|
+
|
|
37
|
+
# Import testing utilities
|
|
38
|
+
from .testing import TestClient
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Core classes
|
|
42
|
+
"BustAPI",
|
|
43
|
+
"Request",
|
|
44
|
+
"Response",
|
|
45
|
+
"Blueprint",
|
|
46
|
+
"TestClient",
|
|
47
|
+
# Global objects
|
|
48
|
+
"request",
|
|
49
|
+
# Helper functions
|
|
50
|
+
"jsonify",
|
|
51
|
+
"make_response",
|
|
52
|
+
"abort",
|
|
53
|
+
"redirect",
|
|
54
|
+
"url_for",
|
|
55
|
+
# Flask compatibility
|
|
56
|
+
"Flask",
|
|
57
|
+
# HTTP status codes
|
|
58
|
+
"HTTPStatus",
|
|
59
|
+
# Version info
|
|
60
|
+
"__version__",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# Convenience imports for common use cases
|
|
64
|
+
try:
|
|
65
|
+
from .extensions.cors import CORS # noqa: F401
|
|
66
|
+
|
|
67
|
+
__all__.append("CORS")
|
|
68
|
+
except ImportError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_version():
|
|
73
|
+
"""Get the current version of BustAPI."""
|
|
74
|
+
return __version__
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_debug_info():
|
|
78
|
+
"""Get debug information about the current BustAPI installation."""
|
|
79
|
+
try:
|
|
80
|
+
from . import bustapi_core
|
|
81
|
+
|
|
82
|
+
rust_version = getattr(bustapi_core, "__version__", "unknown")
|
|
83
|
+
except ImportError:
|
|
84
|
+
rust_version = "not available"
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"bustapi_version": __version__,
|
|
88
|
+
"rust_core_version": rust_version,
|
|
89
|
+
"python_version": sys.version,
|
|
90
|
+
"platform": platform.platform(),
|
|
91
|
+
"architecture": platform.architecture(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Set up default logging
|
|
96
|
+
logging.getLogger("bustapi").addHandler(logging.NullHandler())
|
bustapi/app.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BustAPI Application class - Flask-compatible web framework
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
8
|
+
|
|
9
|
+
from .blueprints import Blueprint
|
|
10
|
+
from .request import Request, _request_ctx
|
|
11
|
+
from .response import Response, make_response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BustAPI:
|
|
15
|
+
"""
|
|
16
|
+
Flask-compatible application class built on Rust backend.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
app = BustAPI()
|
|
20
|
+
|
|
21
|
+
@app.route('/')
|
|
22
|
+
def hello():
|
|
23
|
+
return 'Hello, World!'
|
|
24
|
+
|
|
25
|
+
app.run()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
import_name: str = None,
|
|
31
|
+
static_url_path: Optional[str] = None,
|
|
32
|
+
static_folder: Optional[str] = None,
|
|
33
|
+
template_folder: Optional[str] = None,
|
|
34
|
+
instance_relative_config: bool = False,
|
|
35
|
+
root_path: Optional[str] = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize BustAPI application.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
import_name: Name of the application package
|
|
42
|
+
static_url_path: URL path for static files
|
|
43
|
+
static_folder: Filesystem path to static files
|
|
44
|
+
template_folder: Filesystem path to templates
|
|
45
|
+
instance_relative_config: Enable instance relative config
|
|
46
|
+
root_path: Root path for the application
|
|
47
|
+
"""
|
|
48
|
+
self.import_name = import_name or self.__class__.__module__
|
|
49
|
+
self.static_url_path = static_url_path
|
|
50
|
+
self.static_folder = static_folder
|
|
51
|
+
self.template_folder = template_folder
|
|
52
|
+
self.instance_relative_config = instance_relative_config
|
|
53
|
+
self.root_path = root_path
|
|
54
|
+
|
|
55
|
+
# Configuration dictionary
|
|
56
|
+
self.config: Dict[str, Any] = {}
|
|
57
|
+
|
|
58
|
+
# Extension registry
|
|
59
|
+
self.extensions: Dict[str, Any] = {}
|
|
60
|
+
|
|
61
|
+
# Route handlers
|
|
62
|
+
self._view_functions: Dict[str, Callable] = {}
|
|
63
|
+
|
|
64
|
+
# Error handlers
|
|
65
|
+
self.error_handler_spec: Dict[Union[int, Type[Exception]], Callable] = {}
|
|
66
|
+
|
|
67
|
+
# Before/after request handlers
|
|
68
|
+
self.before_request_funcs: List[Callable] = []
|
|
69
|
+
self.after_request_funcs: List[Callable] = []
|
|
70
|
+
self.teardown_request_funcs: List[Callable] = []
|
|
71
|
+
self.teardown_appcontext_funcs: List[Callable] = []
|
|
72
|
+
|
|
73
|
+
# Blueprint registry
|
|
74
|
+
self.blueprints: Dict[str, Blueprint] = {}
|
|
75
|
+
|
|
76
|
+
# URL map and rules
|
|
77
|
+
self.url_map = {}
|
|
78
|
+
|
|
79
|
+
# Jinja environment (placeholder for template support)
|
|
80
|
+
self.jinja_env = None
|
|
81
|
+
|
|
82
|
+
# Initialize Rust backend
|
|
83
|
+
self._rust_app = None
|
|
84
|
+
self._init_rust_backend()
|
|
85
|
+
|
|
86
|
+
def _init_rust_backend(self):
|
|
87
|
+
"""Initialize the Rust backend application."""
|
|
88
|
+
try:
|
|
89
|
+
from . import bustapi_core
|
|
90
|
+
|
|
91
|
+
self._rust_app = bustapi_core.PyBustApp()
|
|
92
|
+
except ImportError as e:
|
|
93
|
+
raise RuntimeError(f"Failed to import Rust backend: {e}")
|
|
94
|
+
|
|
95
|
+
def route(self, rule: str, **options) -> Callable:
|
|
96
|
+
"""
|
|
97
|
+
Flask-compatible route decorator.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
rule: URL rule as string
|
|
101
|
+
**options: Additional options including methods, defaults, etc.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Decorator function
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
@app.route('/users/<int:id>', methods=['GET', 'POST'])
|
|
108
|
+
def user(id):
|
|
109
|
+
return f'User {id}'
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def decorator(f: Callable) -> Callable:
|
|
113
|
+
endpoint = options.pop("endpoint", f.__name__)
|
|
114
|
+
methods = options.pop("methods", ["GET"])
|
|
115
|
+
|
|
116
|
+
# Store view function
|
|
117
|
+
self._view_functions[endpoint] = f
|
|
118
|
+
|
|
119
|
+
# Register with Rust backend
|
|
120
|
+
for method in methods:
|
|
121
|
+
if inspect.iscoroutinefunction(f):
|
|
122
|
+
# Async handler executed synchronously via asyncio.run inside wrapper
|
|
123
|
+
self._rust_app.add_route(
|
|
124
|
+
method, rule, self._wrap_async_handler(f, rule)
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
# Sync handler
|
|
128
|
+
self._rust_app.add_route(
|
|
129
|
+
method, rule, self._wrap_sync_handler(f, rule)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return f
|
|
133
|
+
|
|
134
|
+
return decorator
|
|
135
|
+
|
|
136
|
+
def get(self, rule: str, **options) -> Callable:
|
|
137
|
+
"""Convenience decorator for GET routes."""
|
|
138
|
+
return self.route(rule, methods=["GET"], **options)
|
|
139
|
+
|
|
140
|
+
def post(self, rule: str, **options) -> Callable:
|
|
141
|
+
"""Convenience decorator for POST routes."""
|
|
142
|
+
return self.route(rule, methods=["POST"], **options)
|
|
143
|
+
|
|
144
|
+
def put(self, rule: str, **options) -> Callable:
|
|
145
|
+
"""Convenience decorator for PUT routes."""
|
|
146
|
+
return self.route(rule, methods=["PUT"], **options)
|
|
147
|
+
|
|
148
|
+
def delete(self, rule: str, **options) -> Callable:
|
|
149
|
+
"""Convenience decorator for DELETE routes."""
|
|
150
|
+
return self.route(rule, methods=["DELETE"], **options)
|
|
151
|
+
|
|
152
|
+
def patch(self, rule: str, **options) -> Callable:
|
|
153
|
+
"""Convenience decorator for PATCH routes."""
|
|
154
|
+
return self.route(rule, methods=["PATCH"], **options)
|
|
155
|
+
|
|
156
|
+
def head(self, rule: str, **options) -> Callable:
|
|
157
|
+
"""Convenience decorator for HEAD routes."""
|
|
158
|
+
return self.route(rule, methods=["HEAD"], **options)
|
|
159
|
+
|
|
160
|
+
def options(self, rule: str, **options) -> Callable:
|
|
161
|
+
"""Convenience decorator for OPTIONS routes."""
|
|
162
|
+
return self.route(rule, methods=["OPTIONS"], **options)
|
|
163
|
+
|
|
164
|
+
def _extract_path_params(self, rule: str, path: str):
|
|
165
|
+
"""Extract path params from a Flask-style rule like '/greet/<name>' or '/users/<int:id>'."""
|
|
166
|
+
rule_parts = rule.strip("/").split("/")
|
|
167
|
+
path_parts = path.strip("/").split("/")
|
|
168
|
+
args = []
|
|
169
|
+
kwargs = {}
|
|
170
|
+
if len(rule_parts) != len(path_parts):
|
|
171
|
+
return args, kwargs
|
|
172
|
+
for rp, pp in zip(rule_parts, path_parts):
|
|
173
|
+
if rp.startswith("<") and rp.endswith(">"):
|
|
174
|
+
inner = rp[1:-1] # strip < >
|
|
175
|
+
if ":" in inner:
|
|
176
|
+
typ, name = inner.split(":", 1)
|
|
177
|
+
typ = typ.strip()
|
|
178
|
+
name = name.strip()
|
|
179
|
+
else:
|
|
180
|
+
typ = "str"
|
|
181
|
+
name = inner.strip()
|
|
182
|
+
val = pp
|
|
183
|
+
if typ == "int":
|
|
184
|
+
try:
|
|
185
|
+
val = int(pp)
|
|
186
|
+
except ValueError:
|
|
187
|
+
val = pp
|
|
188
|
+
# Only populate kwargs to avoid duplicate positional+keyword arguments
|
|
189
|
+
kwargs[name] = val
|
|
190
|
+
return args, kwargs
|
|
191
|
+
|
|
192
|
+
def before_request(self, f: Callable) -> Callable:
|
|
193
|
+
"""
|
|
194
|
+
Register function to run before each request.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
f: Function to run before request
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The original function
|
|
201
|
+
"""
|
|
202
|
+
self.before_request_funcs.append(f)
|
|
203
|
+
return f
|
|
204
|
+
|
|
205
|
+
def after_request(self, f: Callable) -> Callable:
|
|
206
|
+
"""
|
|
207
|
+
Register function to run after each request.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
f: Function to run after request
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The original function
|
|
214
|
+
"""
|
|
215
|
+
self.after_request_funcs.append(f)
|
|
216
|
+
return f
|
|
217
|
+
|
|
218
|
+
def teardown_request(self, f: Callable) -> Callable:
|
|
219
|
+
"""
|
|
220
|
+
Register function to run after each request, even if an exception occurred.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
f: Function to run on teardown
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The original function
|
|
227
|
+
"""
|
|
228
|
+
self.teardown_request_funcs.append(f)
|
|
229
|
+
return f
|
|
230
|
+
|
|
231
|
+
def teardown_appcontext(self, f: Callable) -> Callable:
|
|
232
|
+
"""
|
|
233
|
+
Register function to run when application context is torn down.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
f: Function to run on app context teardown
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The original function
|
|
240
|
+
"""
|
|
241
|
+
self.teardown_appcontext_funcs.append(f)
|
|
242
|
+
return f
|
|
243
|
+
|
|
244
|
+
def errorhandler(self, code_or_exception: Union[int, Type[Exception]]) -> Callable:
|
|
245
|
+
"""
|
|
246
|
+
Register error handler for HTTP status codes or exceptions.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
code_or_exception: HTTP status code or exception class
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Decorator function
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def decorator(f: Callable) -> Callable:
|
|
256
|
+
self.error_handler_spec[code_or_exception] = f
|
|
257
|
+
return f
|
|
258
|
+
|
|
259
|
+
return decorator
|
|
260
|
+
|
|
261
|
+
def register_blueprint(self, blueprint: Blueprint, **options) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Register a blueprint with the application.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
blueprint: Blueprint instance to register
|
|
267
|
+
**options: Additional options for blueprint registration
|
|
268
|
+
"""
|
|
269
|
+
url_prefix = options.get("url_prefix", blueprint.url_prefix)
|
|
270
|
+
|
|
271
|
+
# Store blueprint
|
|
272
|
+
self.blueprints[blueprint.name] = blueprint
|
|
273
|
+
|
|
274
|
+
# Register blueprint routes with the application
|
|
275
|
+
for rule, endpoint, view_func, methods in blueprint.deferred_functions:
|
|
276
|
+
if url_prefix:
|
|
277
|
+
rule = url_prefix.rstrip("/") + "/" + rule.lstrip("/")
|
|
278
|
+
|
|
279
|
+
# Create route with blueprint endpoint
|
|
280
|
+
full_endpoint = f"{blueprint.name}.{endpoint}"
|
|
281
|
+
self._view_functions[full_endpoint] = view_func
|
|
282
|
+
|
|
283
|
+
# Register with Rust backend
|
|
284
|
+
for method in methods:
|
|
285
|
+
if inspect.iscoroutinefunction(view_func):
|
|
286
|
+
# Async handler executed synchronously via asyncio.run inside wrapper
|
|
287
|
+
self._rust_app.add_route(
|
|
288
|
+
method, rule, self._wrap_async_handler(view_func, rule)
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
self._rust_app.add_route(
|
|
292
|
+
method, rule, self._wrap_sync_handler(view_func, rule)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _wrap_sync_handler(self, handler: Callable, rule: str) -> Callable:
|
|
296
|
+
"""Wrap handler with request context, middleware, and path param support."""
|
|
297
|
+
|
|
298
|
+
@wraps(handler)
|
|
299
|
+
def wrapper(rust_request):
|
|
300
|
+
try:
|
|
301
|
+
# Convert Rust request to Python Request object
|
|
302
|
+
request = Request._from_rust_request(rust_request)
|
|
303
|
+
|
|
304
|
+
# Set request context
|
|
305
|
+
_request_ctx.set(request)
|
|
306
|
+
|
|
307
|
+
# Run before request handlers
|
|
308
|
+
for before_func in self.before_request_funcs:
|
|
309
|
+
result = before_func()
|
|
310
|
+
if result is not None:
|
|
311
|
+
return self._make_response(result)
|
|
312
|
+
|
|
313
|
+
# Extract path params from rule and path
|
|
314
|
+
args, kwargs = self._extract_path_params(rule, request.path)
|
|
315
|
+
|
|
316
|
+
# Call the actual handler (Flask-style handlers take path params)
|
|
317
|
+
if inspect.iscoroutinefunction(handler):
|
|
318
|
+
import asyncio # Import locally where needed
|
|
319
|
+
|
|
320
|
+
result = asyncio.run(handler(**kwargs))
|
|
321
|
+
else:
|
|
322
|
+
result = handler(**kwargs)
|
|
323
|
+
response = self._make_response(result)
|
|
324
|
+
|
|
325
|
+
# Run after request handlers
|
|
326
|
+
for after_func in self.after_request_funcs:
|
|
327
|
+
response = after_func(response) or response
|
|
328
|
+
|
|
329
|
+
# Convert Python Response to dict/tuple for Rust
|
|
330
|
+
return self._response_to_rust_format(response)
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
# Handle errors
|
|
334
|
+
error_response = self._handle_exception(e)
|
|
335
|
+
return self._response_to_rust_format(error_response)
|
|
336
|
+
finally:
|
|
337
|
+
# Teardown handlers
|
|
338
|
+
for teardown_func in self.teardown_request_funcs:
|
|
339
|
+
try:
|
|
340
|
+
teardown_func(None)
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
# Clear request context
|
|
345
|
+
_request_ctx.set(None)
|
|
346
|
+
|
|
347
|
+
return wrapper
|
|
348
|
+
|
|
349
|
+
def _wrap_async_handler(self, handler: Callable, rule: str) -> Callable:
|
|
350
|
+
"""Wrap asynchronous handler; executed synchronously via asyncio.run for now."""
|
|
351
|
+
|
|
352
|
+
@wraps(handler)
|
|
353
|
+
def wrapper(rust_request):
|
|
354
|
+
try:
|
|
355
|
+
# Convert Rust request to Python Request object
|
|
356
|
+
request = Request._from_rust_request(rust_request)
|
|
357
|
+
|
|
358
|
+
# Set request context
|
|
359
|
+
_request_ctx.set(request)
|
|
360
|
+
|
|
361
|
+
# Run before request handlers
|
|
362
|
+
for before_func in self.before_request_funcs:
|
|
363
|
+
result = before_func()
|
|
364
|
+
if result is not None:
|
|
365
|
+
return self._make_response(result)
|
|
366
|
+
|
|
367
|
+
# Extract path params
|
|
368
|
+
args, kwargs = self._extract_path_params(rule, request.path)
|
|
369
|
+
|
|
370
|
+
# Call the handler (await if coroutine)
|
|
371
|
+
if inspect.iscoroutinefunction(handler):
|
|
372
|
+
import asyncio # Import locally where needed
|
|
373
|
+
|
|
374
|
+
result = asyncio.run(handler(**kwargs))
|
|
375
|
+
else:
|
|
376
|
+
result = handler(**kwargs)
|
|
377
|
+
response = self._make_response(result)
|
|
378
|
+
|
|
379
|
+
# Run after request handlers
|
|
380
|
+
for after_func in self.after_request_funcs:
|
|
381
|
+
response = after_func(response) or response
|
|
382
|
+
|
|
383
|
+
# Convert Python Response to dict/tuple for Rust
|
|
384
|
+
return self._response_to_rust_format(response)
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
# Handle errors
|
|
388
|
+
error_response = self._handle_exception(e)
|
|
389
|
+
return self._response_to_rust_format(error_response)
|
|
390
|
+
finally:
|
|
391
|
+
# Teardown handlers
|
|
392
|
+
for teardown_func in self.teardown_request_funcs:
|
|
393
|
+
try:
|
|
394
|
+
teardown_func(None)
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
# Clear request context
|
|
399
|
+
_request_ctx.set(None)
|
|
400
|
+
|
|
401
|
+
return wrapper
|
|
402
|
+
|
|
403
|
+
def _make_response(self, result: Any) -> Response:
|
|
404
|
+
"""Convert various return types to Response objects."""
|
|
405
|
+
return make_response(result)
|
|
406
|
+
|
|
407
|
+
def _handle_exception(self, exception: Exception) -> Response:
|
|
408
|
+
"""Handle exceptions and return appropriate error responses."""
|
|
409
|
+
# Check for registered error handlers
|
|
410
|
+
for exc_class_or_code, handler in self.error_handler_spec.items():
|
|
411
|
+
if isinstance(exc_class_or_code, type) and isinstance(
|
|
412
|
+
exception, exc_class_or_code
|
|
413
|
+
):
|
|
414
|
+
return self._make_response(handler(exception))
|
|
415
|
+
elif isinstance(exc_class_or_code, int):
|
|
416
|
+
# For HTTP status code handlers, need to check if it matches
|
|
417
|
+
# This is a simplified implementation
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
# Default error response
|
|
421
|
+
if hasattr(exception, "code"):
|
|
422
|
+
status = getattr(exception, "code", 500)
|
|
423
|
+
else:
|
|
424
|
+
status = 500
|
|
425
|
+
|
|
426
|
+
return Response(f"Internal Server Error: {str(exception)}", status=status)
|
|
427
|
+
|
|
428
|
+
def _response_to_rust_format(self, response: Response) -> tuple:
|
|
429
|
+
"""Convert Python Response object to format expected by Rust."""
|
|
430
|
+
# Return (body, status_code, headers) tuple
|
|
431
|
+
headers_dict = {}
|
|
432
|
+
if hasattr(response, "headers") and response.headers:
|
|
433
|
+
headers_dict = dict(response.headers)
|
|
434
|
+
|
|
435
|
+
body = (
|
|
436
|
+
response.get_data(as_text=False)
|
|
437
|
+
if hasattr(response, "get_data")
|
|
438
|
+
else str(response).encode("utf-8")
|
|
439
|
+
)
|
|
440
|
+
status_code = response.status_code if hasattr(response, "status_code") else 200
|
|
441
|
+
|
|
442
|
+
return (body.decode("utf-8", errors="replace"), status_code, headers_dict)
|
|
443
|
+
|
|
444
|
+
def run(
|
|
445
|
+
self,
|
|
446
|
+
host: str = "127.0.0.1",
|
|
447
|
+
port: int = 5000,
|
|
448
|
+
debug: bool = False,
|
|
449
|
+
load_dotenv: bool = True,
|
|
450
|
+
**options,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""
|
|
453
|
+
Run the application server (Flask-compatible).
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
host: Hostname to bind to
|
|
457
|
+
port: Port to bind to
|
|
458
|
+
debug: Enable debug mode
|
|
459
|
+
load_dotenv: Load environment variables from .env file
|
|
460
|
+
**options: Additional server options
|
|
461
|
+
"""
|
|
462
|
+
if debug:
|
|
463
|
+
self.config["DEBUG"] = True
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
self._rust_app.run(host, port)
|
|
467
|
+
except KeyboardInterrupt:
|
|
468
|
+
print("\nShutting down server...")
|
|
469
|
+
except Exception as e:
|
|
470
|
+
print(f"Server error: {e}")
|
|
471
|
+
|
|
472
|
+
async def run_async(
|
|
473
|
+
self, host: str = "127.0.0.1", port: int = 5000, debug: bool = False, **options
|
|
474
|
+
) -> None:
|
|
475
|
+
"""
|
|
476
|
+
Run the application server asynchronously.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
host: Hostname to bind to
|
|
480
|
+
port: Port to bind to
|
|
481
|
+
debug: Enable debug mode
|
|
482
|
+
**options: Additional server options
|
|
483
|
+
"""
|
|
484
|
+
if debug:
|
|
485
|
+
self.config["DEBUG"] = True
|
|
486
|
+
|
|
487
|
+
await self._rust_app.run_async(host, port)
|
|
488
|
+
|
|
489
|
+
def test_client(self, use_cookies: bool = True, **kwargs):
|
|
490
|
+
"""
|
|
491
|
+
Create a test client for the application.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
use_cookies: Enable cookie support in test client
|
|
495
|
+
**kwargs: Additional test client options
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
TestClient instance
|
|
499
|
+
"""
|
|
500
|
+
from .testing import TestClient
|
|
501
|
+
|
|
502
|
+
return TestClient(self, use_cookies=use_cookies, **kwargs)
|
|
503
|
+
|
|
504
|
+
def app_context(self):
|
|
505
|
+
"""
|
|
506
|
+
Create an application context.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Application context manager
|
|
510
|
+
"""
|
|
511
|
+
# Placeholder for application context implementation
|
|
512
|
+
return _AppContext(self)
|
|
513
|
+
|
|
514
|
+
def request_context(self, environ_or_request):
|
|
515
|
+
"""
|
|
516
|
+
Create a request context.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
environ_or_request: WSGI environ dict or Request object
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Request context manager
|
|
523
|
+
"""
|
|
524
|
+
# Placeholder for request context implementation
|
|
525
|
+
return _RequestContext(self, environ_or_request)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class _AppContext:
|
|
529
|
+
"""Application context manager."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, app: BustAPI):
|
|
532
|
+
self.app = app
|
|
533
|
+
|
|
534
|
+
def __enter__(self):
|
|
535
|
+
return self
|
|
536
|
+
|
|
537
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class _RequestContext:
|
|
542
|
+
"""Request context manager."""
|
|
543
|
+
|
|
544
|
+
def __init__(self, app: BustAPI, environ_or_request):
|
|
545
|
+
self.app = app
|
|
546
|
+
self.request = environ_or_request
|
|
547
|
+
|
|
548
|
+
def __enter__(self):
|
|
549
|
+
return self
|
|
550
|
+
|
|
551
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
552
|
+
pass
|