bustapi 0.1.0__cp311-cp311-win_amd64.whl → 0.1.5__cp311-cp311-win_amd64.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 +10 -5
- bustapi/app.py +338 -41
- bustapi/bustapi_core.cp311-win_amd64.pyd +0 -0
- bustapi/helpers.py +28 -8
- bustapi/logging.py +468 -0
- bustapi/openapi/__init__.py +33 -0
- bustapi/openapi/const.py +3 -0
- bustapi/openapi/docs.py +269 -0
- bustapi/openapi/models.py +128 -0
- bustapi/openapi/utils.py +158 -0
- bustapi/templating.py +30 -0
- bustapi/testing.py +2 -1
- bustapi-0.1.5.dist-info/METADATA +324 -0
- bustapi-0.1.5.dist-info/RECORD +23 -0
- {bustapi-0.1.0.dist-info → bustapi-0.1.5.dist-info}/licenses/LICENSE +1 -1
- bustapi-0.1.0.dist-info/METADATA +0 -233
- bustapi-0.1.0.dist-info/RECORD +0 -16
- {bustapi-0.1.0.dist-info → bustapi-0.1.5.dist-info}/WHEEL +0 -0
- {bustapi-0.1.0.dist-info → bustapi-0.1.5.dist-info}/entry_points.txt +0 -0
bustapi/__init__.py
CHANGED
|
@@ -17,20 +17,22 @@ Example:
|
|
|
17
17
|
app.run(debug=True)
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
import logging
|
|
20
|
+
import logging as _logging
|
|
21
21
|
import platform
|
|
22
22
|
import sys
|
|
23
23
|
from http import HTTPStatus
|
|
24
24
|
|
|
25
25
|
__version__ = "0.1.0"
|
|
26
|
-
__author__ = "BustAPI
|
|
27
|
-
__email__ = "
|
|
26
|
+
__author__ = "BustAPI"
|
|
27
|
+
__email__ = "grandpaabir@gmail.com"
|
|
28
|
+
|
|
29
|
+
from . import logging
|
|
28
30
|
|
|
29
31
|
# Import core classes and functions
|
|
30
32
|
from .app import BustAPI
|
|
31
33
|
from .blueprints import Blueprint
|
|
32
34
|
from .flask_compat import Flask
|
|
33
|
-
from .helpers import abort, redirect, url_for
|
|
35
|
+
from .helpers import abort, redirect, render_template, url_for
|
|
34
36
|
from .request import Request, request
|
|
35
37
|
from .response import Response, jsonify, make_response
|
|
36
38
|
|
|
@@ -52,6 +54,9 @@ __all__ = [
|
|
|
52
54
|
"abort",
|
|
53
55
|
"redirect",
|
|
54
56
|
"url_for",
|
|
57
|
+
"render_template",
|
|
58
|
+
# Logging
|
|
59
|
+
"logging",
|
|
55
60
|
# Flask compatibility
|
|
56
61
|
"Flask",
|
|
57
62
|
# HTTP status codes
|
|
@@ -93,4 +98,4 @@ def get_debug_info():
|
|
|
93
98
|
|
|
94
99
|
|
|
95
100
|
# Set up default logging
|
|
96
|
-
|
|
101
|
+
_logging.getLogger("bustapi").addHandler(_logging.NullHandler())
|
bustapi/app.py
CHANGED
|
@@ -7,6 +7,7 @@ from functools import wraps
|
|
|
7
7
|
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
8
8
|
|
|
9
9
|
from .blueprints import Blueprint
|
|
10
|
+
from .logging import get_logger
|
|
10
11
|
from .request import Request, _request_ctx
|
|
11
12
|
from .response import Response, make_response
|
|
12
13
|
|
|
@@ -33,6 +34,13 @@ class BustAPI:
|
|
|
33
34
|
template_folder: Optional[str] = None,
|
|
34
35
|
instance_relative_config: bool = False,
|
|
35
36
|
root_path: Optional[str] = None,
|
|
37
|
+
# FastAPI-style parameters
|
|
38
|
+
title: Optional[str] = None,
|
|
39
|
+
description: Optional[str] = None,
|
|
40
|
+
version: Optional[str] = None,
|
|
41
|
+
docs_url: Optional[str] = None,
|
|
42
|
+
redoc_url: Optional[str] = None,
|
|
43
|
+
openapi_url: Optional[str] = None,
|
|
36
44
|
):
|
|
37
45
|
"""
|
|
38
46
|
Initialize BustAPI application.
|
|
@@ -74,14 +82,71 @@ class BustAPI:
|
|
|
74
82
|
self.blueprints: Dict[str, Blueprint] = {}
|
|
75
83
|
|
|
76
84
|
# URL map and rules
|
|
77
|
-
|
|
85
|
+
# url_map maps rule -> {endpoint, methods}
|
|
86
|
+
self.url_map: Dict[str, Dict] = {}
|
|
78
87
|
|
|
79
88
|
# Jinja environment (placeholder for template support)
|
|
80
89
|
self.jinja_env = None
|
|
81
90
|
|
|
91
|
+
# FastAPI-style configuration
|
|
92
|
+
self.title = title or "BustAPI"
|
|
93
|
+
self.description = description or "A high-performance Python web framework"
|
|
94
|
+
self.version = version or "1.0.0"
|
|
95
|
+
self.docs_url = docs_url
|
|
96
|
+
self.redoc_url = redoc_url
|
|
97
|
+
self.openapi_url = openapi_url
|
|
98
|
+
|
|
99
|
+
# Initialize colorful logger
|
|
100
|
+
try:
|
|
101
|
+
self.logger = get_logger("bustapi.app")
|
|
102
|
+
except Exception:
|
|
103
|
+
# Fallback if logging module has issues
|
|
104
|
+
self.logger = None
|
|
105
|
+
|
|
106
|
+
# Flask compatibility attributes
|
|
107
|
+
self.debug = False
|
|
108
|
+
self.testing = False
|
|
109
|
+
self.secret_key = None
|
|
110
|
+
self.permanent_session_lifetime = None
|
|
111
|
+
self.use_x_sendfile = False
|
|
112
|
+
self.logger = None
|
|
113
|
+
self.json_encoder = None
|
|
114
|
+
self.json_decoder = None
|
|
115
|
+
self.jinja_options = {}
|
|
116
|
+
self.got_first_request = False
|
|
117
|
+
self.shell_context_processors = []
|
|
118
|
+
self.cli = None
|
|
119
|
+
self.instance_path = None
|
|
120
|
+
self.open_session = None
|
|
121
|
+
self.save_session = None
|
|
122
|
+
self.session_interface = None
|
|
123
|
+
self.wsgi_app = None
|
|
124
|
+
self.response_class = None
|
|
125
|
+
self.request_class = None
|
|
126
|
+
self.test_client_class = None
|
|
127
|
+
self.test_cli_runner_class = None
|
|
128
|
+
self.url_rule_class = None
|
|
129
|
+
self.url_map_class = None
|
|
130
|
+
self.subdomain_matching = False
|
|
131
|
+
self.url_defaults = None
|
|
132
|
+
self.template_context_processors = {}
|
|
133
|
+
self._template_fragment_cache = None
|
|
134
|
+
|
|
82
135
|
# Initialize Rust backend
|
|
83
136
|
self._rust_app = None
|
|
84
137
|
self._init_rust_backend()
|
|
138
|
+
# Register auto documentation endpoints
|
|
139
|
+
try:
|
|
140
|
+
if self.openapi_url:
|
|
141
|
+
self.get(self.openapi_url)(self._openapi_route)
|
|
142
|
+
if self.docs_url:
|
|
143
|
+
self.get(self.docs_url)(self._swagger_ui)
|
|
144
|
+
if self.redoc_url:
|
|
145
|
+
self.get(self.redoc_url)(self._redoc_ui)
|
|
146
|
+
except Exception:
|
|
147
|
+
# ignore registration errors in environments where Rust backend
|
|
148
|
+
# isn't available
|
|
149
|
+
pass
|
|
85
150
|
|
|
86
151
|
def _init_rust_backend(self):
|
|
87
152
|
"""Initialize the Rust backend application."""
|
|
@@ -90,7 +155,7 @@ class BustAPI:
|
|
|
90
155
|
|
|
91
156
|
self._rust_app = bustapi_core.PyBustApp()
|
|
92
157
|
except ImportError as e:
|
|
93
|
-
raise RuntimeError(f"Failed to import Rust backend: {e}")
|
|
158
|
+
raise RuntimeError(f"Failed to import Rust backend: {e}") from e
|
|
94
159
|
|
|
95
160
|
def route(self, rule: str, **options) -> Callable:
|
|
96
161
|
"""
|
|
@@ -116,10 +181,14 @@ class BustAPI:
|
|
|
116
181
|
# Store view function
|
|
117
182
|
self._view_functions[endpoint] = f
|
|
118
183
|
|
|
184
|
+
# Store the rule and methods for OpenAPI generation and debugging
|
|
185
|
+
self.url_map[rule] = {"endpoint": endpoint, "methods": methods}
|
|
186
|
+
|
|
119
187
|
# Register with Rust backend
|
|
120
188
|
for method in methods:
|
|
121
189
|
if inspect.iscoroutinefunction(f):
|
|
122
|
-
# Async handler executed synchronously via asyncio.run
|
|
190
|
+
# Async handler executed synchronously via asyncio.run
|
|
191
|
+
# inside wrapper
|
|
123
192
|
self._rust_app.add_route(
|
|
124
193
|
method, rule, self._wrap_async_handler(f, rule)
|
|
125
194
|
)
|
|
@@ -161,6 +230,74 @@ class BustAPI:
|
|
|
161
230
|
"""Convenience decorator for OPTIONS routes."""
|
|
162
231
|
return self.route(rule, methods=["OPTIONS"], **options)
|
|
163
232
|
|
|
233
|
+
# Flask compatibility methods
|
|
234
|
+
def shell_context_processor(self, f):
|
|
235
|
+
"""Register a shell context processor function."""
|
|
236
|
+
self.shell_context_processors.append(f)
|
|
237
|
+
return f
|
|
238
|
+
|
|
239
|
+
def make_shell_context(self):
|
|
240
|
+
"""Create shell context."""
|
|
241
|
+
context = {"app": self}
|
|
242
|
+
for processor in self.shell_context_processors:
|
|
243
|
+
context.update(processor())
|
|
244
|
+
return context
|
|
245
|
+
|
|
246
|
+
def app_context(self):
|
|
247
|
+
"""Create application context."""
|
|
248
|
+
return _AppContext(self)
|
|
249
|
+
|
|
250
|
+
def request_context(self, environ_or_request):
|
|
251
|
+
"""Create request context."""
|
|
252
|
+
return _RequestContext(self, environ_or_request)
|
|
253
|
+
|
|
254
|
+
def test_request_context(self, *args, **kwargs):
|
|
255
|
+
"""Create test request context."""
|
|
256
|
+
return _RequestContext(self, None)
|
|
257
|
+
|
|
258
|
+
def preprocess_request(self):
|
|
259
|
+
"""Preprocess request."""
|
|
260
|
+
for func in self.before_request_funcs:
|
|
261
|
+
result = func()
|
|
262
|
+
if result is not None:
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
def process_response(self, response):
|
|
266
|
+
"""Process response."""
|
|
267
|
+
for func in self.after_request_funcs:
|
|
268
|
+
response = func(response)
|
|
269
|
+
return response
|
|
270
|
+
|
|
271
|
+
def do_teardown_request(self, exc=None):
|
|
272
|
+
"""Teardown request."""
|
|
273
|
+
for func in self.teardown_request_funcs:
|
|
274
|
+
func(exc)
|
|
275
|
+
|
|
276
|
+
def do_teardown_appcontext(self, exc=None):
|
|
277
|
+
"""Teardown app context."""
|
|
278
|
+
for func in self.teardown_appcontext_funcs:
|
|
279
|
+
func(exc)
|
|
280
|
+
|
|
281
|
+
def make_default_options_response(self):
|
|
282
|
+
"""Make default OPTIONS response."""
|
|
283
|
+
from .response import Response
|
|
284
|
+
|
|
285
|
+
return Response("", 200, {"Allow": "GET,HEAD,POST,OPTIONS"})
|
|
286
|
+
|
|
287
|
+
def create_jinja_environment(self):
|
|
288
|
+
"""Create Jinja2 environment."""
|
|
289
|
+
if self.jinja_env is None:
|
|
290
|
+
try:
|
|
291
|
+
from jinja2 import Environment, FileSystemLoader
|
|
292
|
+
|
|
293
|
+
template_folder = self.template_folder or "templates"
|
|
294
|
+
self.jinja_env = Environment(
|
|
295
|
+
loader=FileSystemLoader(template_folder), **self.jinja_options
|
|
296
|
+
)
|
|
297
|
+
except ImportError:
|
|
298
|
+
pass
|
|
299
|
+
return self.jinja_env
|
|
300
|
+
|
|
164
301
|
def _extract_path_params(self, rule: str, path: str):
|
|
165
302
|
"""Extract path params from a Flask-style rule like '/greet/<name>' or '/users/<int:id>'."""
|
|
166
303
|
rule_parts = rule.strip("/").split("/")
|
|
@@ -314,13 +451,15 @@ class BustAPI:
|
|
|
314
451
|
args, kwargs = self._extract_path_params(rule, request.path)
|
|
315
452
|
|
|
316
453
|
# Call the actual handler (Flask-style handlers take path params)
|
|
317
|
-
|
|
318
|
-
|
|
454
|
+
# Note: Async handlers are now handled directly by Rust PyAsyncRouteHandler
|
|
455
|
+
# This wrapper should only handle sync functions for better performance
|
|
456
|
+
result = handler(**kwargs)
|
|
319
457
|
|
|
320
|
-
|
|
458
|
+
# Handle tuple responses properly
|
|
459
|
+
if isinstance(result, tuple):
|
|
460
|
+
response = self._make_response(*result)
|
|
321
461
|
else:
|
|
322
|
-
|
|
323
|
-
response = self._make_response(result)
|
|
462
|
+
response = self._make_response(result)
|
|
324
463
|
|
|
325
464
|
# Run after request handlers
|
|
326
465
|
for after_func in self.after_request_funcs:
|
|
@@ -367,14 +506,15 @@ class BustAPI:
|
|
|
367
506
|
# Extract path params
|
|
368
507
|
args, kwargs = self._extract_path_params(rule, request.path)
|
|
369
508
|
|
|
370
|
-
# Call the handler (
|
|
371
|
-
|
|
372
|
-
|
|
509
|
+
# Call the handler (sync only - async handled by Rust)
|
|
510
|
+
# Note: Async handlers are now handled directly by Rust PyAsyncRouteHandler
|
|
511
|
+
result = handler(**kwargs)
|
|
373
512
|
|
|
374
|
-
|
|
513
|
+
# Handle tuple responses properly
|
|
514
|
+
if isinstance(result, tuple):
|
|
515
|
+
response = self._make_response(*result)
|
|
375
516
|
else:
|
|
376
|
-
|
|
377
|
-
response = self._make_response(result)
|
|
517
|
+
response = self._make_response(result)
|
|
378
518
|
|
|
379
519
|
# Run after request handlers
|
|
380
520
|
for after_func in self.after_request_funcs:
|
|
@@ -400,9 +540,163 @@ class BustAPI:
|
|
|
400
540
|
|
|
401
541
|
return wrapper
|
|
402
542
|
|
|
403
|
-
def _make_response(self,
|
|
543
|
+
def _make_response(self, *args) -> Response:
|
|
404
544
|
"""Convert various return types to Response objects."""
|
|
405
|
-
return make_response(
|
|
545
|
+
return make_response(*args)
|
|
546
|
+
|
|
547
|
+
# --- Templating helpers ---
|
|
548
|
+
def create_jinja_env(self):
|
|
549
|
+
"""Create and cache a Jinja2 environment using the application's template_folder."""
|
|
550
|
+
if self.jinja_env is None:
|
|
551
|
+
try:
|
|
552
|
+
from .templating import create_jinja_env as _create_env
|
|
553
|
+
|
|
554
|
+
self.jinja_env = _create_env(self.template_folder)
|
|
555
|
+
except Exception as e:
|
|
556
|
+
raise RuntimeError(f"Failed to create Jinja environment: {e}") from e
|
|
557
|
+
return self.jinja_env
|
|
558
|
+
|
|
559
|
+
def render_template(self, template_name: str, **context) -> str:
|
|
560
|
+
"""Render a template using the app's Jinja environment."""
|
|
561
|
+
env = self.create_jinja_env()
|
|
562
|
+
from .templating import render_template as _render
|
|
563
|
+
|
|
564
|
+
return _render(env, template_name, context)
|
|
565
|
+
|
|
566
|
+
# --- OpenAPI generation ---
|
|
567
|
+
def _generate_openapi(self) -> Dict:
|
|
568
|
+
"""Generate OpenAPI 3.1.0 specification for the application."""
|
|
569
|
+
try:
|
|
570
|
+
from .openapi.utils import get_openapi_spec
|
|
571
|
+
|
|
572
|
+
# Extract route information from url_map
|
|
573
|
+
routes = []
|
|
574
|
+
for rule, meta in self.url_map.items():
|
|
575
|
+
methods = meta.get("methods", ["GET"])
|
|
576
|
+
endpoint = meta.get("endpoint")
|
|
577
|
+
handler = self._view_functions.get(endpoint)
|
|
578
|
+
|
|
579
|
+
routes.append(
|
|
580
|
+
{
|
|
581
|
+
"path": rule,
|
|
582
|
+
"methods": methods,
|
|
583
|
+
"handler": handler,
|
|
584
|
+
"endpoint": endpoint,
|
|
585
|
+
}
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Generate OpenAPI spec using our custom implementation
|
|
589
|
+
return get_openapi_spec(
|
|
590
|
+
title=self.title,
|
|
591
|
+
version=self.version,
|
|
592
|
+
description=self.description,
|
|
593
|
+
routes=routes,
|
|
594
|
+
servers=[{"url": "/", "description": "Development server"}],
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
except Exception:
|
|
598
|
+
# Fallback to simple implementation if there are issues
|
|
599
|
+
info = {"title": self.title, "version": self.version}
|
|
600
|
+
if self.description:
|
|
601
|
+
info["description"] = self.description
|
|
602
|
+
|
|
603
|
+
paths: Dict[str, Dict] = {}
|
|
604
|
+
for rule, meta in self.url_map.items():
|
|
605
|
+
methods = meta.get("methods", ["GET"])
|
|
606
|
+
paths.setdefault(rule, {})
|
|
607
|
+
for m in methods:
|
|
608
|
+
endpoint = meta.get("endpoint")
|
|
609
|
+
view = self._view_functions.get(endpoint)
|
|
610
|
+
summary = None
|
|
611
|
+
if view is not None:
|
|
612
|
+
summary = (
|
|
613
|
+
(view.__doc__ or "").strip().splitlines()[0]
|
|
614
|
+
if view.__doc__
|
|
615
|
+
else view.__name__
|
|
616
|
+
)
|
|
617
|
+
paths[rule][m.lower()] = {
|
|
618
|
+
"summary": summary or endpoint,
|
|
619
|
+
"operationId": endpoint,
|
|
620
|
+
"responses": {"200": {"description": "Successful response"}},
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {"openapi": "3.1.0", "info": info, "paths": paths}
|
|
624
|
+
|
|
625
|
+
def _openapi_route(self):
|
|
626
|
+
"""Simple route handler that returns the generated OpenAPI JSON."""
|
|
627
|
+
return self._generate_openapi()
|
|
628
|
+
|
|
629
|
+
def _swagger_ui(self):
|
|
630
|
+
"""Swagger UI documentation page."""
|
|
631
|
+
openapi_url = self.openapi_url or "/openapi.json"
|
|
632
|
+
html = f"""
|
|
633
|
+
<!DOCTYPE html>
|
|
634
|
+
<html>
|
|
635
|
+
<head>
|
|
636
|
+
<title>{self.title} - Swagger UI</title>
|
|
637
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
|
|
638
|
+
<style>
|
|
639
|
+
html {{ box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }}
|
|
640
|
+
*, *:before, *:after {{ box-sizing: inherit; }}
|
|
641
|
+
body {{ margin:0; background: #fafafa; }}
|
|
642
|
+
</style>
|
|
643
|
+
</head>
|
|
644
|
+
<body>
|
|
645
|
+
<div id="swagger-ui"></div>
|
|
646
|
+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
|
|
647
|
+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
|
|
648
|
+
<script>
|
|
649
|
+
window.onload = function() {{
|
|
650
|
+
const ui = SwaggerUIBundle({{
|
|
651
|
+
url: '{openapi_url}',
|
|
652
|
+
dom_id: '#swagger-ui',
|
|
653
|
+
deepLinking: true,
|
|
654
|
+
presets: [
|
|
655
|
+
SwaggerUIBundle.presets.apis,
|
|
656
|
+
SwaggerUIStandalonePreset
|
|
657
|
+
],
|
|
658
|
+
plugins: [
|
|
659
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
660
|
+
],
|
|
661
|
+
layout: "StandaloneLayout"
|
|
662
|
+
}});
|
|
663
|
+
}};
|
|
664
|
+
</script>
|
|
665
|
+
</body>
|
|
666
|
+
</html>
|
|
667
|
+
"""
|
|
668
|
+
from .response import make_response
|
|
669
|
+
|
|
670
|
+
response = make_response(html)
|
|
671
|
+
response.headers["Content-Type"] = "text/html"
|
|
672
|
+
return response
|
|
673
|
+
|
|
674
|
+
def _redoc_ui(self):
|
|
675
|
+
"""ReDoc documentation page."""
|
|
676
|
+
openapi_url = self.openapi_url or "/openapi.json"
|
|
677
|
+
html = f"""
|
|
678
|
+
<!DOCTYPE html>
|
|
679
|
+
<html>
|
|
680
|
+
<head>
|
|
681
|
+
<title>{self.title} - ReDoc</title>
|
|
682
|
+
<meta charset="utf-8"/>
|
|
683
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
684
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
685
|
+
<style>
|
|
686
|
+
body {{ margin: 0; padding: 0; }}
|
|
687
|
+
</style>
|
|
688
|
+
</head>
|
|
689
|
+
<body>
|
|
690
|
+
<redoc spec-url='{openapi_url}'></redoc>
|
|
691
|
+
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0/bundles/redoc.standalone.js"></script>
|
|
692
|
+
</body>
|
|
693
|
+
</html>
|
|
694
|
+
"""
|
|
695
|
+
from .response import make_response
|
|
696
|
+
|
|
697
|
+
response = make_response(html)
|
|
698
|
+
response.headers["Content-Type"] = "text/html"
|
|
699
|
+
return response
|
|
406
700
|
|
|
407
701
|
def _handle_exception(self, exception: Exception) -> Response:
|
|
408
702
|
"""Handle exceptions and return appropriate error responses."""
|
|
@@ -462,12 +756,38 @@ class BustAPI:
|
|
|
462
756
|
if debug:
|
|
463
757
|
self.config["DEBUG"] = True
|
|
464
758
|
|
|
759
|
+
# Log startup with colorful output
|
|
760
|
+
if self.logger:
|
|
761
|
+
self.logger.log_startup(f"Starting {self.title} v{self.version}")
|
|
762
|
+
self.logger.info(f"Listening on http://{host}:{port}")
|
|
763
|
+
self.logger.info(f"Debug mode: {'ON' if debug else 'OFF'}")
|
|
764
|
+
|
|
765
|
+
if self.docs_url:
|
|
766
|
+
self.logger.info(f"📚 API docs: http://{host}:{port}{self.docs_url}")
|
|
767
|
+
if self.redoc_url:
|
|
768
|
+
self.logger.info(f"📖 ReDoc: http://{host}:{port}{self.redoc_url}")
|
|
769
|
+
else:
|
|
770
|
+
print(f"🚀 Starting {self.title} v{self.version}")
|
|
771
|
+
print(f"📍 Listening on http://{host}:{port}")
|
|
772
|
+
print(f"🔧 Debug mode: {'ON' if debug else 'OFF'}")
|
|
773
|
+
|
|
774
|
+
if self.docs_url:
|
|
775
|
+
print(f"📚 API docs: http://{host}:{port}{self.docs_url}")
|
|
776
|
+
if self.redoc_url:
|
|
777
|
+
print(f"📖 ReDoc: http://{host}:{port}{self.redoc_url}")
|
|
778
|
+
|
|
465
779
|
try:
|
|
466
780
|
self._rust_app.run(host, port)
|
|
467
781
|
except KeyboardInterrupt:
|
|
468
|
-
|
|
782
|
+
if self.logger:
|
|
783
|
+
self.logger.log_shutdown("Server stopped by user")
|
|
784
|
+
else:
|
|
785
|
+
print("\n🛑 Server stopped by user")
|
|
469
786
|
except Exception as e:
|
|
470
|
-
|
|
787
|
+
if self.logger:
|
|
788
|
+
self.logger.error(f"Server error: {e}")
|
|
789
|
+
else:
|
|
790
|
+
print(f"❌ Server error: {e}")
|
|
471
791
|
|
|
472
792
|
async def run_async(
|
|
473
793
|
self, host: str = "127.0.0.1", port: int = 5000, debug: bool = False, **options
|
|
@@ -501,29 +821,6 @@ class BustAPI:
|
|
|
501
821
|
|
|
502
822
|
return TestClient(self, use_cookies=use_cookies, **kwargs)
|
|
503
823
|
|
|
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
824
|
|
|
528
825
|
class _AppContext:
|
|
529
826
|
"""Application context manager."""
|
|
Binary file
|
bustapi/helpers.py
CHANGED
|
@@ -245,10 +245,10 @@ def _get_current_object():
|
|
|
245
245
|
raise RuntimeError("Working outside of application context")
|
|
246
246
|
|
|
247
247
|
|
|
248
|
-
# Template helpers
|
|
248
|
+
# Template helpers
|
|
249
249
|
def render_template(template_name: str, **context) -> str:
|
|
250
250
|
"""
|
|
251
|
-
Render template (Flask-compatible
|
|
251
|
+
Render template using Jinja2 (Flask-compatible).
|
|
252
252
|
|
|
253
253
|
Args:
|
|
254
254
|
template_name: Template filename
|
|
@@ -256,13 +256,33 @@ def render_template(template_name: str, **context) -> str:
|
|
|
256
256
|
|
|
257
257
|
Returns:
|
|
258
258
|
Rendered template string
|
|
259
|
-
|
|
260
|
-
Note:
|
|
261
|
-
This is a placeholder. Template rendering should be
|
|
262
|
-
implemented with a proper template engine like Jinja2.
|
|
263
259
|
"""
|
|
264
|
-
|
|
265
|
-
|
|
260
|
+
try:
|
|
261
|
+
import os
|
|
262
|
+
|
|
263
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
264
|
+
|
|
265
|
+
# Get template directory (default to 'templates')
|
|
266
|
+
template_dir = context.pop("_template_dir", "templates")
|
|
267
|
+
if not os.path.exists(template_dir):
|
|
268
|
+
os.makedirs(template_dir, exist_ok=True)
|
|
269
|
+
|
|
270
|
+
# Create Jinja2 environment
|
|
271
|
+
env = Environment(
|
|
272
|
+
loader=FileSystemLoader(template_dir),
|
|
273
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Load and render template
|
|
277
|
+
template = env.get_template(template_name)
|
|
278
|
+
return template.render(**context)
|
|
279
|
+
|
|
280
|
+
except ImportError:
|
|
281
|
+
# Fallback if Jinja2 is not installed
|
|
282
|
+
return f"<!-- Template: {template_name} (Jinja2 not installed) -->"
|
|
283
|
+
except Exception as e:
|
|
284
|
+
# Fallback for template errors
|
|
285
|
+
return f"<!-- Template Error: {template_name} - {str(e)} -->"
|
|
266
286
|
|
|
267
287
|
|
|
268
288
|
def render_template_string(source: str, **context) -> str:
|