bustapi 0.1.0__cp311-cp311-manylinux_2_34_x86_64.whl → 0.1.5__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 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 Team"
27
- __email__ = "hello@bustapi.dev"
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
- logging.getLogger("bustapi").addHandler(logging.NullHandler())
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
- self.url_map = {}
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 inside wrapper
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
- if inspect.iscoroutinefunction(handler):
318
- import asyncio # Import locally where needed
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
- result = asyncio.run(handler(**kwargs))
458
+ # Handle tuple responses properly
459
+ if isinstance(result, tuple):
460
+ response = self._make_response(*result)
321
461
  else:
322
- result = handler(**kwargs)
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 (await if coroutine)
371
- if inspect.iscoroutinefunction(handler):
372
- import asyncio # Import locally where needed
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
- result = asyncio.run(handler(**kwargs))
513
+ # Handle tuple responses properly
514
+ if isinstance(result, tuple):
515
+ response = self._make_response(*result)
375
516
  else:
376
- result = handler(**kwargs)
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, result: Any) -> Response:
543
+ def _make_response(self, *args) -> Response:
404
544
  """Convert various return types to Response objects."""
405
- return make_response(result)
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
- print("\nShutting down server...")
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
- print(f"Server error: {e}")
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."""
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 (placeholders)
248
+ # Template helpers
249
249
  def render_template(template_name: str, **context) -> str:
250
250
  """
251
- Render template (Flask-compatible placeholder).
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
- # TODO: Implement template rendering
265
- return f"<!-- Template: {template_name} -->"
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: