tachyon-api 0.5.5__py3-none-any.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 tachyon-api might be problematic. Click here for more details.

tachyon_api/openapi.py ADDED
@@ -0,0 +1,362 @@
1
+ from typing import Dict, Any, Optional, List, Type
2
+ from dataclasses import dataclass, field
3
+
4
+ from .models import Struct
5
+
6
+ # Type mapping from Python types to OpenAPI schema types
7
+ TYPE_MAP = {int: "integer", str: "string", bool: "boolean", float: "number"}
8
+
9
+
10
+ def _generate_schema_for_struct(struct_class: Type[Struct]) -> Dict[str, Any]:
11
+ """
12
+ Generate a JSON Schema dictionary from a tachyon_api.models.Struct.
13
+
14
+ Args:
15
+ struct_class: The Struct class to generate schema for
16
+
17
+ Returns:
18
+ Dictionary containing the OpenAPI schema for the struct
19
+ """
20
+ properties = {}
21
+ required = []
22
+
23
+ # Use msgspec's introspection tools
24
+ for field_name in struct_class.__struct_fields__:
25
+ field_type = struct_class.__annotations__.get(field_name)
26
+ properties[field_name] = {
27
+ "type": TYPE_MAP.get(field_type, "string"),
28
+ "title": field_name.replace("_", " ").title(),
29
+ }
30
+ # For now, assume all fields are required (can be enhanced later)
31
+ required.append(field_name)
32
+
33
+ return {"type": "object", "properties": properties, "required": required}
34
+
35
+
36
+ @dataclass
37
+ class Contact:
38
+ """Contact information for the API"""
39
+
40
+ name: Optional[str] = None
41
+ url: Optional[str] = None
42
+ email: Optional[str] = None
43
+
44
+ def to_dict(self) -> Dict[str, Any]:
45
+ """Convert to OpenAPI contact object"""
46
+ result = {}
47
+ if self.name:
48
+ result["name"] = self.name
49
+ if self.url:
50
+ result["url"] = self.url
51
+ if self.email:
52
+ result["email"] = self.email
53
+ return result
54
+
55
+
56
+ @dataclass
57
+ class License:
58
+ """License information for the API"""
59
+
60
+ name: str
61
+ url: Optional[str] = None
62
+
63
+ def to_dict(self) -> Dict[str, Any]:
64
+ """Convert to OpenAPI license object"""
65
+ result = {"name": self.name}
66
+ if self.url:
67
+ result["url"] = self.url
68
+ return result
69
+
70
+
71
+ @dataclass
72
+ class Info:
73
+ """General information about the API"""
74
+
75
+ title: str = "Tachyon API"
76
+ description: Optional[str] = "A fast API built with Tachyon"
77
+ version: str = "0.1.0"
78
+ terms_of_service: Optional[str] = None
79
+ contact: Optional[Contact] = None
80
+ license: Optional[License] = None
81
+
82
+ def to_dict(self) -> Dict[str, Any]:
83
+ """Convert to OpenAPI info object"""
84
+ result: Dict[str, Any] = {"title": self.title, "version": self.version}
85
+ if self.description:
86
+ result["description"] = self.description
87
+ if self.terms_of_service:
88
+ result["termsOfService"] = self.terms_of_service
89
+ if self.contact:
90
+ result["contact"] = self.contact.to_dict()
91
+ if self.license:
92
+ result["license"] = self.license.to_dict()
93
+ return result
94
+
95
+
96
+ @dataclass
97
+ class Server:
98
+ """Server information"""
99
+
100
+ url: str
101
+ description: Optional[str] = None
102
+
103
+ def to_dict(self) -> Dict[str, Any]:
104
+ """Convert to OpenAPI server object"""
105
+ result = {"url": self.url}
106
+ if self.description:
107
+ result["description"] = self.description
108
+ return result
109
+
110
+
111
+ @dataclass
112
+ class OpenAPIConfig:
113
+ """Configuration for OpenAPI/Swagger documentation"""
114
+
115
+ info: Info = field(default_factory=Info)
116
+ servers: List[Server] = field(default_factory=list)
117
+ openapi_version: str = "3.0.0"
118
+ docs_url: str = "/docs"
119
+ redoc_url: str = "/redoc"
120
+ openapi_url: str = "/openapi.json"
121
+ include_in_schema: bool = True
122
+ # Scalar configuration
123
+ scalar_js_url: str = "https://cdn.jsdelivr.net/npm/@scalar/api-reference"
124
+ scalar_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png"
125
+ # Swagger UI configuration (legacy support)
126
+ swagger_ui_oauth2_redirect_url: Optional[str] = None
127
+ swagger_ui_init_oauth: Optional[Dict[str, Any]] = None
128
+ swagger_ui_parameters: Optional[Dict[str, Any]] = None
129
+ swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png"
130
+ swagger_js_url: str = (
131
+ "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"
132
+ )
133
+ swagger_css_url: str = (
134
+ "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"
135
+ )
136
+ redoc_js_url: str = (
137
+ "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
138
+ )
139
+
140
+ def to_openapi_dict(self) -> Dict[str, Any]:
141
+ """Generate the complete OpenAPI dictionary"""
142
+ openapi_dict = {
143
+ "openapi": self.openapi_version,
144
+ "info": self.info.to_dict(),
145
+ "paths": {},
146
+ "components": {"schemas": {}},
147
+ }
148
+
149
+ if self.servers:
150
+ openapi_dict["servers"] = [server.to_dict() for server in self.servers]
151
+
152
+ return openapi_dict
153
+
154
+
155
+ class OpenAPIGenerator:
156
+ """Generator for OpenAPI documentation"""
157
+
158
+ def __init__(self, config: Optional[OpenAPIConfig] = None):
159
+ """
160
+ Initialize the OpenAPI generator.
161
+
162
+ Args:
163
+ config: Optional OpenAPI configuration. Uses defaults if not provided.
164
+ """
165
+ self.config = config or OpenAPIConfig()
166
+ self._openapi_schema: Optional[Dict[str, Any]] = None
167
+
168
+ def get_openapi_schema(self) -> Dict[str, Any]:
169
+ """Get the complete OpenAPI schema"""
170
+ if self._openapi_schema is None:
171
+ self._openapi_schema = self.config.to_openapi_dict()
172
+ return self._openapi_schema
173
+
174
+ def get_swagger_ui_html(self, openapi_url: str, title: str) -> str:
175
+ """Generate HTML for Swagger UI"""
176
+ swagger_ui_parameters = self.config.swagger_ui_parameters or {}
177
+
178
+ # Convert parameters to JSON string, handling Python booleans correctly
179
+ params_json = (
180
+ str(swagger_ui_parameters)
181
+ .replace("'", '"')
182
+ .replace("True", "true")
183
+ .replace("False", "false")
184
+ )
185
+
186
+ html = f"""<!DOCTYPE html>
187
+ <html>
188
+ <head>
189
+ <link type="text/css" rel="stylesheet" href="{self.config.swagger_css_url}">
190
+ <link rel="shortcut icon" href="{self.config.swagger_favicon_url}">
191
+ <title>{title}</title>
192
+ </head>
193
+ <body>
194
+ <div id="swagger-ui"></div>
195
+ <script src="{self.config.swagger_js_url}"></script>
196
+ <script>
197
+ const ui = SwaggerUIBundle({{
198
+ url: '{openapi_url}',
199
+ dom_id: '#swagger-ui',
200
+ presets: [
201
+ SwaggerUIBundle.presets.apis,
202
+ SwaggerUIBundle.presets.standalone
203
+ ],
204
+ layout: "BaseLayout",
205
+ ...{params_json}
206
+ }})
207
+ </script>
208
+ </body>
209
+ </html>"""
210
+ return html
211
+
212
+ def get_redoc_html(self, openapi_url: str, title: str) -> str:
213
+ """Generate HTML for ReDoc"""
214
+ html = f"""<!DOCTYPE html>
215
+ <html>
216
+ <head>
217
+ <title>{title}</title>
218
+ <meta charset="utf-8"/>
219
+ <meta name="viewport" content="width=device-width, initial-scale=1">
220
+ <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
221
+ <style>
222
+ body {{
223
+ margin: 0;
224
+ padding: 0;
225
+ }}
226
+ </style>
227
+ </head>
228
+ <body>
229
+ <redoc spec-url='{openapi_url}'></redoc>
230
+ <script src="{self.config.redoc_js_url}"></script>
231
+ </body>
232
+ </html>"""
233
+ return html
234
+
235
+ def get_scalar_html(self, openapi_url: str, title: str) -> str:
236
+ """Generate HTML for Scalar API Reference"""
237
+ html = f"""<!DOCTYPE html>
238
+ <html>
239
+ <head>
240
+ <title>{title}</title>
241
+ <meta charset="utf-8" />
242
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
243
+ <link rel="shortcut icon" href="{self.config.scalar_favicon_url}">
244
+ <style>
245
+ body {{
246
+ margin: 0;
247
+ padding: 0;
248
+ }}
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <script
253
+ id="api-reference"
254
+ data-url="{openapi_url}"
255
+ src="{self.config.scalar_js_url}"></script>
256
+ </body>
257
+ </html>"""
258
+ return html
259
+
260
+ def add_path(self, path: str, method: str, operation_data: Dict[str, Any]) -> None:
261
+ """
262
+ Add a path operation to the OpenAPI schema.
263
+
264
+ Args:
265
+ path: The URL path (e.g., "/items/{item_id}")
266
+ method: HTTP method (e.g., "get", "post")
267
+ operation_data: OpenAPI operation object
268
+ """
269
+ if self._openapi_schema is None:
270
+ self._openapi_schema = self.config.to_openapi_dict()
271
+
272
+ if path not in self._openapi_schema["paths"]:
273
+ self._openapi_schema["paths"][path] = {}
274
+
275
+ self._openapi_schema["paths"][path][method.lower()] = operation_data
276
+
277
+ def add_schema(self, name: str, schema_data: Dict[str, Any]) -> None:
278
+ """
279
+ Add a component schema to the OpenAPI specification.
280
+
281
+ Args:
282
+ name: Schema name (e.g., "Item", "User")
283
+ schema_data: OpenAPI schema object
284
+ """
285
+ if self._openapi_schema is None:
286
+ self._openapi_schema = self.config.to_openapi_dict()
287
+
288
+ self._openapi_schema["components"]["schemas"][name] = schema_data
289
+
290
+
291
+ def create_openapi_config(
292
+ title: str = "Tachyon API",
293
+ description: Optional[str] = "A fast API built with Tachyon",
294
+ version: str = "0.1.0",
295
+ openapi_version: str = "3.0.0",
296
+ docs_url: str = "/docs",
297
+ redoc_url: str = "/redoc",
298
+ openapi_url: str = "/openapi.json",
299
+ contact: Optional[Contact] = None,
300
+ license: Optional[License] = None,
301
+ servers: Optional[List[Server]] = None,
302
+ terms_of_service: Optional[str] = None,
303
+ # Scalar configuration
304
+ scalar_js_url: str = "https://cdn.jsdelivr.net/npm/@scalar/api-reference",
305
+ scalar_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
306
+ # Swagger UI configuration (legacy support)
307
+ swagger_ui_parameters: Optional[Dict[str, Any]] = None,
308
+ swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
309
+ swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
310
+ swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
311
+ redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
312
+ ) -> OpenAPIConfig:
313
+ """
314
+ Create a customizable OpenAPI configuration similar to FastAPI.
315
+
316
+ Args:
317
+ title: API title
318
+ description: API description
319
+ version: API version
320
+ openapi_version: OpenAPI specification version
321
+ docs_url: URL for Scalar API Reference documentation (default)
322
+ redoc_url: URL for ReDoc documentation
323
+ openapi_url: URL for OpenAPI JSON schema
324
+ contact: Contact information
325
+ license: License information
326
+ servers: List of servers
327
+ terms_of_service: Terms of service URL
328
+ scalar_js_url: Scalar API Reference JavaScript URL
329
+ scalar_favicon_url: Favicon URL for Scalar
330
+ swagger_ui_parameters: Additional Swagger UI parameters
331
+ swagger_favicon_url: Favicon URL for Swagger UI
332
+ swagger_js_url: Swagger UI JavaScript URL
333
+ swagger_css_url: Swagger UI CSS URL
334
+ redoc_js_url: ReDoc JavaScript URL
335
+
336
+ Returns:
337
+ Configured OpenAPIConfig instance
338
+ """
339
+ info = Info(
340
+ title=title,
341
+ description=description,
342
+ version=version,
343
+ terms_of_service=terms_of_service,
344
+ contact=contact,
345
+ license=license,
346
+ )
347
+
348
+ return OpenAPIConfig(
349
+ info=info,
350
+ servers=servers or [],
351
+ openapi_version=openapi_version,
352
+ docs_url=docs_url,
353
+ redoc_url=redoc_url,
354
+ openapi_url=openapi_url,
355
+ scalar_js_url=scalar_js_url,
356
+ scalar_favicon_url=scalar_favicon_url,
357
+ swagger_ui_parameters=swagger_ui_parameters,
358
+ swagger_favicon_url=swagger_favicon_url,
359
+ swagger_js_url=swagger_js_url,
360
+ swagger_css_url=swagger_css_url,
361
+ redoc_js_url=redoc_js_url,
362
+ )
tachyon_api/params.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ Tachyon Web Framework - Parameter Definition Module
3
+
4
+ This module provides parameter marker classes for defining how endpoint function
5
+ parameters should be resolved from HTTP requests (query strings, path variables,
6
+ and request bodies).
7
+ """
8
+
9
+ from typing import Any, Optional
10
+
11
+
12
+ class Query:
13
+ """
14
+ Marker class for query string parameters.
15
+
16
+ Use this to define parameters that should be extracted from the URL query string
17
+ with optional default values and automatic type conversion.
18
+
19
+ Args:
20
+ default: Default value if parameter is not provided. Use ... for required parameters.
21
+ description: Optional description for OpenAPI documentation.
22
+
23
+ Example:
24
+ @app.get("/search")
25
+ def search(
26
+ q: str = Query(...), # Required query parameter
27
+ limit: int = Query(10), # Optional with default value
28
+ active: bool = Query(False) # Optional boolean parameter
29
+ ):
30
+ return {"query": q, "limit": limit, "active": active}
31
+
32
+ Note:
33
+ - Boolean parameters accept: "true", "1", "t", "yes" (case-insensitive) as True
34
+ - Type conversion is automatic based on parameter annotation
35
+ - Missing required parameters return 422 Unprocessable Entity
36
+ - Invalid type conversions return 422 Unprocessable Entity
37
+ """
38
+
39
+ def __init__(self, default: Any = ..., description: Optional[str] = None):
40
+ """
41
+ Initialize a Query parameter marker.
42
+
43
+ Args:
44
+ default: Default value for the parameter. Use ... (Ellipsis) for required parameters.
45
+ description: Optional description for API documentation.
46
+ """
47
+ self.default = default
48
+ self.description = description
49
+
50
+
51
+ class Path:
52
+ """
53
+ Marker class for path parameters.
54
+
55
+ Use this to define parameters that should be extracted from the URL path.
56
+ Path parameters are always required.
57
+
58
+ Args:
59
+ description: Optional description for OpenAPI documentation.
60
+ """
61
+
62
+ def __init__(self, description: Optional[str] = None):
63
+ """
64
+ Initialize a Path parameter marker.
65
+
66
+ Args:
67
+ description: Optional description for API documentation.
68
+ """
69
+ self.description = description
70
+
71
+
72
+ class Body:
73
+ """
74
+ Marker class for request body parameters.
75
+
76
+ Use this to define parameters that should be extracted and validated from
77
+ the JSON request body. The parameter type should be a Struct subclass.
78
+
79
+ Args:
80
+ description: Optional description for OpenAPI documentation.
81
+ """
82
+
83
+ def __init__(self, description: Optional[str] = None):
84
+ """
85
+ Initialize a Body parameter marker.
86
+
87
+ Args:
88
+ description: Optional description for API documentation.
89
+ """
90
+ self.description = description
@@ -0,0 +1,49 @@
1
+ """
2
+ Simple response helpers for Tachyon API
3
+
4
+ Provides convenient response helpers while keeping full compatibility
5
+ with Starlette responses.
6
+ """
7
+
8
+ from starlette.responses import JSONResponse, HTMLResponse # noqa
9
+
10
+
11
+ # Simple helper functions for common response patterns
12
+ def success_response(data=None, message="Success", status_code=200):
13
+ """Create a success response with consistent structure"""
14
+ return JSONResponse(
15
+ {"success": True, "message": message, "data": data}, status_code=status_code
16
+ )
17
+
18
+
19
+ def error_response(error, status_code=400, code=None):
20
+ """Create an error response with consistent structure"""
21
+ response_data = {"success": False, "error": error}
22
+ if code:
23
+ response_data["code"] = code
24
+
25
+ return JSONResponse(response_data, status_code=status_code)
26
+
27
+
28
+ def not_found_response(error="Resource not found"):
29
+ """Create a 404 not found response"""
30
+ return error_response(error, status_code=404, code="NOT_FOUND")
31
+
32
+
33
+ def conflict_response(error="Resource conflict"):
34
+ """Create a 409 conflict response"""
35
+ return error_response(error, status_code=409, code="CONFLICT")
36
+
37
+
38
+ def validation_error_response(error="Validation failed", errors=None):
39
+ """Create a 422 validation error response"""
40
+ response_data = {"success": False, "error": error, "code": "VALIDATION_ERROR"}
41
+ if errors:
42
+ response_data["errors"] = errors
43
+
44
+ return JSONResponse(response_data, status_code=422)
45
+
46
+
47
+ # Re-export Starlette responses for convenience
48
+ # JSONResponse is already imported above
49
+ # HTMLResponse is now also imported
tachyon_api/router.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ Tachyon Router Module
3
+
4
+ Provides route grouping functionality similar to FastAPI's APIRouter,
5
+ allowing for better organization of routes with common prefixes, tags, and dependencies.
6
+ """
7
+
8
+ from functools import partial
9
+ from typing import List, Optional, Any, Callable, Dict
10
+
11
+ from .di import Depends
12
+
13
+
14
+ class Router:
15
+ """
16
+ Router class for grouping related routes with common configuration.
17
+
18
+ Similar to FastAPI's APIRouter, allows grouping routes with:
19
+ - Common prefixes
20
+ - Common tags
21
+ - Common dependencies
22
+ - Better organization of related endpoints
23
+
24
+ Note: Router stores route definitions but doesn't implement the actual routing logic.
25
+ The routing logic is handled by the main Tachyon app when the router is included.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ prefix: str = "",
31
+ tags: Optional[List[str]] = None,
32
+ dependencies: Optional[List[Depends]] = None,
33
+ responses: Optional[Dict[int, Dict[str, Any]]] = None,
34
+ ):
35
+ """
36
+ Initialize a new Router instance.
37
+
38
+ Args:
39
+ prefix: Common prefix for all routes in this router
40
+ tags: List of tags to apply to all routes
41
+ dependencies: List of dependencies to apply to all routes
42
+ responses: Common responses for OpenAPI documentation
43
+ """
44
+ # Normalize prefix - ensure it starts with / if not empty
45
+ if prefix and not prefix.startswith("/"):
46
+ prefix = "/" + prefix
47
+ elif prefix is None:
48
+ prefix = ""
49
+
50
+ self.prefix = prefix
51
+ self.tags = tags or []
52
+ self.dependencies = dependencies or []
53
+ self.responses = responses or {}
54
+ self.routes: List[Dict[str, Any]] = []
55
+
56
+ # Create HTTP method decorators using the same pattern as Tachyon
57
+ http_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
58
+ for method in http_methods:
59
+ setattr(
60
+ self,
61
+ method.lower(),
62
+ partial(self._create_route_decorator, http_method=method),
63
+ )
64
+
65
+ def _create_route_decorator(self, path: str, *, http_method: str, **kwargs):
66
+ """
67
+ Create a decorator for the specified HTTP method.
68
+
69
+ This method is similar to Tachyon's _create_decorator but stores routes
70
+ instead of registering them immediately.
71
+
72
+ Args:
73
+ path: URL path pattern (will be prefixed with router prefix)
74
+ http_method: HTTP method name (GET, POST, PUT, DELETE, etc.)
75
+ **kwargs: Additional route options (summary, description, tags, etc.)
76
+
77
+ Returns:
78
+ A decorator function that stores the endpoint with the router
79
+ """
80
+
81
+ def decorator(endpoint_func: Callable):
82
+ # Combine router tags with route-specific tags
83
+ route_tags = list(self.tags) # Start with router tags
84
+ if "tags" in kwargs:
85
+ if isinstance(kwargs["tags"], list):
86
+ route_tags.extend(kwargs["tags"])
87
+ else:
88
+ route_tags.append(kwargs["tags"])
89
+
90
+ # Update kwargs with combined tags
91
+ if route_tags:
92
+ kwargs["tags"] = route_tags
93
+
94
+ # Store the route information for later registration
95
+ route_info = {
96
+ "path": path,
97
+ "method": http_method,
98
+ "func": endpoint_func,
99
+ "dependencies": self.dependencies.copy(),
100
+ **kwargs,
101
+ }
102
+
103
+ self.routes.append(route_info)
104
+ return endpoint_func
105
+
106
+ return decorator
107
+
108
+ def get_full_path(self, path: str) -> str:
109
+ """
110
+ Get the full path by combining router prefix with route path.
111
+
112
+ Args:
113
+ path: The route path
114
+
115
+ Returns:
116
+ Full path with prefix applied
117
+ """
118
+ if not self.prefix:
119
+ return path
120
+
121
+ # Handle root path specially
122
+ if path == "/":
123
+ return self.prefix
124
+
125
+ # Combine prefix and path, avoiding double slashes
126
+ if path.startswith("/"):
127
+ return self.prefix + path
128
+ else:
129
+ return self.prefix + "/" + path
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2025 Juan Manuel Panozzo Zénere
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.