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.

@@ -0,0 +1,269 @@
1
+ """
2
+ BustAPI OpenAPI Documentation UI
3
+
4
+ Custom implementation for Swagger UI and ReDoc documentation interfaces.
5
+ """
6
+
7
+ import json
8
+ from typing import Any, Dict, Optional
9
+
10
+ try:
11
+ from starlette.responses import HTMLResponse
12
+ except ImportError:
13
+ # Fallback if Starlette is not available
14
+ class HTMLResponse:
15
+ def __init__(
16
+ self,
17
+ content: str,
18
+ status_code: int = 200,
19
+ headers: Optional[Dict[str, str]] = None,
20
+ ):
21
+ self.content = content
22
+ self.status_code = status_code
23
+ self.headers = headers or {}
24
+
25
+
26
+ def jsonable_encoder(obj: Any) -> Any:
27
+ """Simple JSON encoder fallback."""
28
+ if hasattr(obj, "dict"):
29
+ return obj.dict()
30
+ elif hasattr(obj, "__dict__"):
31
+ return obj.__dict__
32
+ else:
33
+ return obj
34
+
35
+
36
+ # BustAPI Swagger UI default parameters
37
+ swagger_ui_default_parameters: Dict[str, Any] = {
38
+ "dom_id": "#swagger-ui",
39
+ "layout": "BaseLayout",
40
+ "deepLinking": True,
41
+ "showExtensions": True,
42
+ "showCommonExtensions": True,
43
+ }
44
+
45
+
46
+ def get_swagger_ui_html(
47
+ *,
48
+ openapi_url: str,
49
+ title: str,
50
+ swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
51
+ swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
52
+ swagger_favicon_url: str = (
53
+ "data:image/svg+xml;base64,"
54
+ "PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iOCIgZmlsbD0iIzAwN0FGRiIvPgo8cGF0aCBkPSJNOCAxNkMxMiAxNiAxNiAxMiAxNiA4QzE2IDEyIDIwIDE2IDI0IDE2QzIwIDE2IDE2IDIwIDE2IDI0QzE2IDIwIDEyIDE2IDggMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K"
55
+ ),
56
+ oauth2_redirect_url: Optional[str] = None,
57
+ init_oauth: Optional[Dict[str, Any]] = None,
58
+ swagger_ui_parameters: Optional[Dict[str, Any]] = None,
59
+ ) -> HTMLResponse:
60
+ """
61
+ Generate and return the HTML that loads Swagger UI for the interactive
62
+ API docs (normally served at `/docs`).
63
+
64
+ This is BustAPI's custom implementation with BustAPI branding and optimizations.
65
+ """
66
+ current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
67
+ if swagger_ui_parameters:
68
+ current_swagger_ui_parameters.update(swagger_ui_parameters)
69
+
70
+ html = f"""
71
+ <!DOCTYPE html>
72
+ <html>
73
+ <head>
74
+ <meta charset="utf-8">
75
+ <meta name="viewport" content="width=device-width, initial-scale=1">
76
+ <link type="text/css" rel="stylesheet" href="{swagger_css_url}">
77
+ <link rel="shortcut icon" href="{swagger_favicon_url}">
78
+ <title>{title}</title>
79
+ <style>
80
+ .swagger-ui .topbar {{ display: none; }}
81
+ .swagger-ui .info .title {{ color: #007AFF; }}
82
+ .swagger-ui .info .title:after {{
83
+ content: " - Powered by BustAPI";
84
+ font-size: 14px;
85
+ color: #666;
86
+ font-weight: normal;
87
+ }}
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <div id="swagger-ui"></div>
92
+ <script src="{swagger_js_url}"></script>
93
+ <script>
94
+ const ui = SwaggerUIBundle({{
95
+ url: '{openapi_url}',
96
+ """
97
+
98
+ for key, value in current_swagger_ui_parameters.items():
99
+ html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"
100
+
101
+ if oauth2_redirect_url:
102
+ html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
103
+
104
+ html += """
105
+ presets: [
106
+ SwaggerUIBundle.presets.apis,
107
+ SwaggerUIBundle.SwaggerUIStandalonePreset
108
+ ],
109
+ plugins: [
110
+ SwaggerUIBundle.plugins.DownloadUrl
111
+ ]
112
+ }});"""
113
+
114
+ if init_oauth:
115
+ html += f"""
116
+ ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))});
117
+ """
118
+
119
+ html += """
120
+ </script>
121
+ </body>
122
+ </html>
123
+ """
124
+ return HTMLResponse(html)
125
+
126
+
127
+ def get_redoc_html(
128
+ *,
129
+ openapi_url: str,
130
+ title: str,
131
+ redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js",
132
+ redoc_favicon_url: str = (
133
+ "data:image/svg+xml;base64,"
134
+ "PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iOCIgZmlsbD0iIzAwN0FGRiIvPgo8cGF0aCBkPSJNOCAxNkMxMiAxNiAxNiAxMiAxNiA4QzE2IDEyIDIwIDE2IDI0IDE2QzIwIDE2IDE2IDIwIDE2IDI0QzE2IDIwIDEyIDE2IDggMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K"
135
+ ),
136
+ with_google_fonts: bool = True,
137
+ ) -> HTMLResponse:
138
+ """
139
+ Generate and return the HTML that loads ReDoc for the alternative
140
+ automatic interactive documentation.
141
+
142
+ This is BustAPI's custom implementation with BustAPI branding.
143
+ """
144
+ html = f"""
145
+ <!DOCTYPE html>
146
+ <html>
147
+ <head>
148
+ <title>{title}</title>
149
+ <!-- needed for adaptive design -->
150
+ <meta charset="utf-8"/>
151
+ <meta name="viewport" content="width=device-width, initial-scale=1">
152
+ """
153
+ if with_google_fonts:
154
+ html += """
155
+ <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
156
+ """
157
+ html += f"""
158
+ <link rel="shortcut icon" href="{redoc_favicon_url}">
159
+ <!--
160
+ ReDoc doesn't change outer page styles
161
+ -->
162
+ <style>
163
+ body {{
164
+ margin: 0;
165
+ padding: 0;
166
+ }}
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <noscript>
171
+ ReDoc requires Javascript to function. Please enable it to browse the documentation.
172
+ </noscript>
173
+ <redoc spec-url="{openapi_url}"></redoc>
174
+ <script src="{redoc_js_url}"> </script>
175
+ </body>
176
+ </html>
177
+ """
178
+ return HTMLResponse(html)
179
+
180
+
181
+ def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
182
+ """
183
+ Generate the HTML response with the OAuth2 redirection for Swagger UI.
184
+
185
+ You normally don't need to use or change this.
186
+ """
187
+ # copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
188
+ html = """
189
+ <!doctype html>
190
+ <html lang="en-US">
191
+ <head>
192
+ <title>Swagger UI: OAuth2 Redirect</title>
193
+ </head>
194
+ <body>
195
+ <script>
196
+ 'use strict';
197
+ function run () {
198
+ var oauth2 = window.opener.swaggerUIRedirectOauth2;
199
+ var sentState = oauth2.state;
200
+ var redirectUrl = oauth2.redirectUrl;
201
+ var isValid, qp, arr;
202
+
203
+ if (/code|token|error/.test(window.location.hash)) {
204
+ qp = window.location.hash.substring(1).replace('?', '&');
205
+ } else {
206
+ qp = location.search.substring(1);
207
+ }
208
+
209
+ arr = qp.split("&");
210
+ arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
211
+ qp = qp ? JSON.parse('{' + arr.join() + '}',
212
+ function (key, value) {
213
+ return key === "" ? value : decodeURIComponent(value);
214
+ }
215
+ ) : {};
216
+
217
+ isValid = qp.state === sentState;
218
+
219
+ if ((
220
+ oauth2.auth.schema.get("flow") === "accessCode" ||
221
+ oauth2.auth.schema.get("flow") === "authorizationCode" ||
222
+ oauth2.auth.schema.get("flow") === "authorization_code"
223
+ ) && !oauth2.auth.code) {
224
+ if (!isValid) {
225
+ oauth2.errCb({
226
+ authId: oauth2.auth.name,
227
+ source: "auth",
228
+ level: "warning",
229
+ message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
230
+ });
231
+ }
232
+
233
+ if (qp.code) {
234
+ delete oauth2.state;
235
+ oauth2.auth.code = qp.code;
236
+ oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
237
+ } else {
238
+ let oauthErrorMsg;
239
+ if (qp.error) {
240
+ oauthErrorMsg = "["+qp.error+"]: " +
241
+ (qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
242
+ (qp.error_uri ? "More info: "+qp.error_uri : "");
243
+ }
244
+
245
+ oauth2.errCb({
246
+ authId: oauth2.auth.name,
247
+ source: "auth",
248
+ level: "error",
249
+ message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
250
+ });
251
+ }
252
+ } else {
253
+ oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
254
+ }
255
+ window.close();
256
+ }
257
+
258
+ if (document.readyState !== 'loading') {
259
+ run();
260
+ } else {
261
+ document.addEventListener('DOMContentLoaded', function () {
262
+ run();
263
+ });
264
+ }
265
+ </script>
266
+ </body>
267
+ </html>
268
+ """
269
+ return HTMLResponse(content=html)
@@ -0,0 +1,128 @@
1
+ """
2
+ BustAPI OpenAPI Models
3
+
4
+ Simplified OpenAPI 3.1.0 specification models for BustAPI.
5
+ Custom implementation optimized for BustAPI's needs.
6
+ """
7
+
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ class OpenAPIInfo:
12
+ """API information for OpenAPI spec."""
13
+
14
+ def __init__(self, title: str, version: str, description: Optional[str] = None):
15
+ self.title = title
16
+ self.version = version
17
+ self.description = description
18
+
19
+
20
+ class OpenAPIServer:
21
+ """Server information for OpenAPI spec."""
22
+
23
+ def __init__(self, url: str, description: Optional[str] = None):
24
+ self.url = url
25
+ self.description = description
26
+
27
+
28
+ class OpenAPIResponse:
29
+ """Response information for OpenAPI spec."""
30
+
31
+ def __init__(self, description: str, content: Optional[Dict[str, Any]] = None):
32
+ self.description = description
33
+ self.content = content or {}
34
+
35
+
36
+ class OpenAPIOperation:
37
+ """Operation information for OpenAPI spec."""
38
+
39
+ def __init__(
40
+ self,
41
+ summary: Optional[str] = None,
42
+ description: Optional[str] = None,
43
+ operation_id: Optional[str] = None,
44
+ responses: Optional[Dict[str, OpenAPIResponse]] = None,
45
+ tags: Optional[List[str]] = None,
46
+ ):
47
+ self.summary = summary
48
+ self.description = description
49
+ self.operationId = operation_id
50
+ self.responses = responses or {"200": OpenAPIResponse("Successful response")}
51
+ self.tags = tags or []
52
+
53
+
54
+ class OpenAPIPathItem:
55
+ """Path item for OpenAPI spec."""
56
+
57
+ def __init__(self):
58
+ self.get: Optional[OpenAPIOperation] = None
59
+ self.post: Optional[OpenAPIOperation] = None
60
+ self.put: Optional[OpenAPIOperation] = None
61
+ self.delete: Optional[OpenAPIOperation] = None
62
+ self.patch: Optional[OpenAPIOperation] = None
63
+ self.head: Optional[OpenAPIOperation] = None
64
+ self.options: Optional[OpenAPIOperation] = None
65
+
66
+
67
+ class OpenAPISpec:
68
+ """Complete OpenAPI specification."""
69
+
70
+ def __init__(
71
+ self,
72
+ info: OpenAPIInfo,
73
+ servers: Optional[List[OpenAPIServer]] = None,
74
+ paths: Optional[Dict[str, OpenAPIPathItem]] = None,
75
+ ):
76
+ self.openapi = "3.1.0"
77
+ self.info = info
78
+ self.servers = servers or []
79
+ self.paths = paths or {}
80
+
81
+ def to_dict(self) -> Dict[str, Any]:
82
+ """Convert to dictionary for JSON serialization."""
83
+ result = {
84
+ "openapi": self.openapi,
85
+ "info": {
86
+ "title": self.info.title,
87
+ "version": self.info.version,
88
+ },
89
+ "paths": {},
90
+ }
91
+
92
+ if self.info.description:
93
+ result["info"]["description"] = self.info.description
94
+
95
+ if self.servers:
96
+ result["servers"] = [
97
+ {"url": server.url, "description": server.description}
98
+ for server in self.servers
99
+ ]
100
+
101
+ for path, path_item in self.paths.items():
102
+ path_dict = {}
103
+
104
+ for method in ["get", "post", "put", "delete", "patch", "head", "options"]:
105
+ operation = getattr(path_item, method, None)
106
+ if operation:
107
+ op_dict = {
108
+ "responses": {
109
+ code: {"description": resp.description}
110
+ for code, resp in operation.responses.items()
111
+ }
112
+ }
113
+
114
+ if operation.summary:
115
+ op_dict["summary"] = operation.summary
116
+ if operation.description:
117
+ op_dict["description"] = operation.description
118
+ if operation.operationId:
119
+ op_dict["operationId"] = operation.operationId
120
+ if operation.tags:
121
+ op_dict["tags"] = operation.tags
122
+
123
+ path_dict[method] = op_dict
124
+
125
+ if path_dict:
126
+ result["paths"][path] = path_dict
127
+
128
+ return result
@@ -0,0 +1,158 @@
1
+ """
2
+ BustAPI OpenAPI Utils
3
+
4
+ Utilities for generating OpenAPI specifications for BustAPI applications.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from .models import (
10
+ OpenAPIInfo,
11
+ OpenAPIOperation,
12
+ OpenAPIPathItem,
13
+ OpenAPIResponse,
14
+ OpenAPIServer,
15
+ OpenAPISpec,
16
+ )
17
+
18
+
19
+ def get_openapi_spec(
20
+ title: str,
21
+ version: str,
22
+ description: Optional[str] = None,
23
+ routes: Optional[List[Any]] = None,
24
+ servers: Optional[List[Dict[str, str]]] = None,
25
+ ) -> Dict[str, Any]:
26
+ """
27
+ Generate OpenAPI specification for BustAPI application.
28
+
29
+ Args:
30
+ title: API title
31
+ version: API version
32
+ description: API description
33
+ routes: List of routes to document
34
+ servers: List of server configurations
35
+
36
+ Returns:
37
+ OpenAPI specification as dictionary
38
+ """
39
+ # Create info object
40
+ info = OpenAPIInfo(title=title, version=version, description=description)
41
+
42
+ # Create servers
43
+ server_objects = []
44
+ if servers:
45
+ for server in servers:
46
+ server_objects.append(
47
+ OpenAPIServer(
48
+ url=server.get("url", "/"), description=server.get("description")
49
+ )
50
+ )
51
+
52
+ # Create paths from routes
53
+ paths = {}
54
+ if routes:
55
+ for route in routes:
56
+ path_item = create_path_item_from_route(route)
57
+ if path_item:
58
+ paths[route.get("path", "/")] = path_item
59
+
60
+ # Create OpenAPI spec
61
+ spec = OpenAPISpec(info=info, servers=server_objects, paths=paths)
62
+
63
+ return spec.to_dict()
64
+
65
+
66
+ def create_path_item_from_route(route: Dict[str, Any]) -> Optional[OpenAPIPathItem]:
67
+ """
68
+ Create OpenAPI path item from route information.
69
+
70
+ Args:
71
+ route: Route information dictionary
72
+
73
+ Returns:
74
+ OpenAPI path item or None
75
+ """
76
+ if not route:
77
+ return None
78
+
79
+ path_item = OpenAPIPathItem()
80
+ methods = route.get("methods", ["GET"])
81
+ handler = route.get("handler")
82
+
83
+ for method in methods:
84
+ method_lower = method.lower()
85
+ if hasattr(path_item, method_lower):
86
+ operation = create_operation_from_handler(handler, method)
87
+ setattr(path_item, method_lower, operation)
88
+
89
+ return path_item
90
+
91
+
92
+ def create_operation_from_handler(handler: Any, method: str) -> OpenAPIOperation:
93
+ """
94
+ Create OpenAPI operation from route handler.
95
+
96
+ Args:
97
+ handler: Route handler function
98
+ method: HTTP method
99
+
100
+ Returns:
101
+ OpenAPI operation
102
+ """
103
+ if not handler:
104
+ return OpenAPIOperation()
105
+
106
+ # Extract information from handler
107
+ summary = None
108
+ description = None
109
+ operation_id = None
110
+
111
+ if hasattr(handler, "__name__"):
112
+ operation_id = f"{method.lower()}_{handler.__name__}"
113
+
114
+ if hasattr(handler, "__doc__") and handler.__doc__:
115
+ doc_lines = handler.__doc__.strip().split("\n")
116
+ if doc_lines:
117
+ summary = doc_lines[0].strip()
118
+ if len(doc_lines) > 1:
119
+ description = "\n".join(line.strip() for line in doc_lines[1:]).strip()
120
+
121
+ # Create default responses
122
+ responses = {
123
+ "200": OpenAPIResponse("Successful response"),
124
+ "422": OpenAPIResponse("Validation Error"),
125
+ }
126
+
127
+ return OpenAPIOperation(
128
+ summary=summary,
129
+ description=description,
130
+ operation_id=operation_id,
131
+ responses=responses,
132
+ )
133
+
134
+
135
+ def extract_route_info(app) -> List[Dict[str, Any]]:
136
+ """
137
+ Extract route information from BustAPI application.
138
+
139
+ Args:
140
+ app: BustAPI application instance
141
+
142
+ Returns:
143
+ List of route information dictionaries
144
+ """
145
+ routes = []
146
+
147
+ # Try to get routes from the app
148
+ if hasattr(app, "_routes"):
149
+ for path, route_info in app._routes.items():
150
+ routes.append(
151
+ {
152
+ "path": path,
153
+ "methods": route_info.get("methods", ["GET"]),
154
+ "handler": route_info.get("handler"),
155
+ }
156
+ )
157
+
158
+ return routes
bustapi/templating.py ADDED
@@ -0,0 +1,30 @@
1
+ """Simple Jinja2-based templating helpers for BustAPI.
2
+
3
+ This module provides a small wrapper around Jinja2 Environment creation and
4
+ rendering so the application can call `render_template` similarly to Flask.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+ try:
10
+ import jinja2
11
+ except Exception: # pragma: no cover - optional dependency
12
+ jinja2 = None
13
+
14
+
15
+ def create_jinja_env(template_folder: Optional[str] = None):
16
+ if jinja2 is None:
17
+ raise RuntimeError(
18
+ "Jinja2 is not installed. Add 'jinja2' to your dependencies."
19
+ )
20
+ loader = jinja2.FileSystemLoader(template_folder or "templates")
21
+ env = jinja2.Environment(loader=loader, autoescape=True)
22
+ return env
23
+
24
+
25
+ def render_template(
26
+ env, template_name: str, context: Optional[Dict[str, Any]] = None
27
+ ) -> str:
28
+ context = context or {}
29
+ template = env.get_template(template_name)
30
+ return template.render(**context)
bustapi/testing.py CHANGED
@@ -92,7 +92,8 @@ class TestResponse:
92
92
  return "application/json" in content_type.lower()
93
93
 
94
94
  def __repr__(self) -> str:
95
- return f'<TestResponse {self.status_code} [{self.headers.get("Content-Type", "")}]>'
95
+ content_type = self.headers.get("Content-Type", "")
96
+ return f"<TestResponse {self.status_code} [{content_type}]>"
96
97
 
97
98
 
98
99
  class TestClient: