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/__init__.py +31 -0
- tachyon_api/app.py +607 -0
- tachyon_api/di.py +59 -0
- tachyon_api/middlewares/__init__.py +5 -0
- tachyon_api/middlewares/core.py +40 -0
- tachyon_api/middlewares/cors.py +142 -0
- tachyon_api/middlewares/logger.py +119 -0
- tachyon_api/models.py +73 -0
- tachyon_api/openapi.py +362 -0
- tachyon_api/params.py +90 -0
- tachyon_api/responses.py +49 -0
- tachyon_api/router.py +129 -0
- tachyon_api-0.5.5.dist-info/LICENSE +17 -0
- tachyon_api-0.5.5.dist-info/METADATA +239 -0
- tachyon_api-0.5.5.dist-info/RECORD +16 -0
- tachyon_api-0.5.5.dist-info/WHEEL +4 -0
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
|
tachyon_api/responses.py
ADDED
|
@@ -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/>.
|