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 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