tachyon-api 0.9.0__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.
- tachyon_api/__init__.py +59 -0
- tachyon_api/app.py +699 -0
- tachyon_api/background.py +72 -0
- tachyon_api/cache.py +270 -0
- tachyon_api/cli/__init__.py +9 -0
- tachyon_api/cli/__main__.py +8 -0
- tachyon_api/cli/commands/__init__.py +5 -0
- tachyon_api/cli/commands/generate.py +190 -0
- tachyon_api/cli/commands/lint.py +186 -0
- tachyon_api/cli/commands/new.py +82 -0
- tachyon_api/cli/commands/openapi.py +128 -0
- tachyon_api/cli/main.py +69 -0
- tachyon_api/cli/templates/__init__.py +8 -0
- tachyon_api/cli/templates/project.py +194 -0
- tachyon_api/cli/templates/service.py +330 -0
- tachyon_api/core/__init__.py +12 -0
- tachyon_api/core/lifecycle.py +106 -0
- tachyon_api/core/websocket.py +92 -0
- tachyon_api/di.py +86 -0
- tachyon_api/exceptions.py +39 -0
- tachyon_api/files.py +14 -0
- tachyon_api/middlewares/__init__.py +4 -0
- tachyon_api/middlewares/core.py +40 -0
- tachyon_api/middlewares/cors.py +159 -0
- tachyon_api/middlewares/logger.py +123 -0
- tachyon_api/models.py +73 -0
- tachyon_api/openapi.py +419 -0
- tachyon_api/params.py +268 -0
- tachyon_api/processing/__init__.py +14 -0
- tachyon_api/processing/dependencies.py +172 -0
- tachyon_api/processing/parameters.py +484 -0
- tachyon_api/processing/response_processor.py +93 -0
- tachyon_api/responses.py +92 -0
- tachyon_api/router.py +161 -0
- tachyon_api/security.py +295 -0
- tachyon_api/testing.py +110 -0
- tachyon_api/utils/__init__.py +15 -0
- tachyon_api/utils/type_converter.py +113 -0
- tachyon_api/utils/type_utils.py +162 -0
- tachyon_api-0.9.0.dist-info/METADATA +291 -0
- tachyon_api-0.9.0.dist-info/RECORD +44 -0
- tachyon_api-0.9.0.dist-info/WHEEL +4 -0
- tachyon_api-0.9.0.dist-info/entry_points.txt +3 -0
- tachyon_api-0.9.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service module templates.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ServiceTemplates:
|
|
7
|
+
"""Templates for `tachyon generate service` command."""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def init(snake_name: str, class_name: str) -> str:
|
|
11
|
+
return f'''"""
|
|
12
|
+
{class_name} module.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .{snake_name}_controller import router
|
|
16
|
+
from .{snake_name}_service import {class_name}Service
|
|
17
|
+
from .{snake_name}_repository import {class_name}Repository
|
|
18
|
+
|
|
19
|
+
__all__ = ["router", "{class_name}Service", "{class_name}Repository"]
|
|
20
|
+
'''
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def controller(snake_name: str, class_name: str, crud: bool = False) -> str:
|
|
24
|
+
if crud:
|
|
25
|
+
return f'''"""
|
|
26
|
+
{class_name} Controller - HTTP endpoints.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from typing import List
|
|
30
|
+
from tachyon_api import Router, Depends, Query
|
|
31
|
+
from .{snake_name}_service import {class_name}Service
|
|
32
|
+
from .{snake_name}_dto import (
|
|
33
|
+
{class_name}Response,
|
|
34
|
+
{class_name}Create,
|
|
35
|
+
{class_name}Update,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
router = Router(prefix="/{snake_name}s", tags=["{class_name}"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.get("/", response_model=List[{class_name}Response])
|
|
42
|
+
def list_{snake_name}s(
|
|
43
|
+
skip: int = Query(0),
|
|
44
|
+
limit: int = Query(100),
|
|
45
|
+
service: {class_name}Service = Depends(),
|
|
46
|
+
):
|
|
47
|
+
"""List all {snake_name}s with pagination."""
|
|
48
|
+
return service.find_all(skip=skip, limit=limit)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get("/{{id}}", response_model={class_name}Response)
|
|
52
|
+
def get_{snake_name}(
|
|
53
|
+
id: str,
|
|
54
|
+
service: {class_name}Service = Depends(),
|
|
55
|
+
):
|
|
56
|
+
"""Get a {snake_name} by ID."""
|
|
57
|
+
return service.find_by_id(id)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.post("/", response_model={class_name}Response)
|
|
61
|
+
def create_{snake_name}(
|
|
62
|
+
data: {class_name}Create,
|
|
63
|
+
service: {class_name}Service = Depends(),
|
|
64
|
+
):
|
|
65
|
+
"""Create a new {snake_name}."""
|
|
66
|
+
return service.create(data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.put("/{{id}}", response_model={class_name}Response)
|
|
70
|
+
def update_{snake_name}(
|
|
71
|
+
id: str,
|
|
72
|
+
data: {class_name}Update,
|
|
73
|
+
service: {class_name}Service = Depends(),
|
|
74
|
+
):
|
|
75
|
+
"""Update a {snake_name}."""
|
|
76
|
+
return service.update(id, data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.delete("/{{id}}")
|
|
80
|
+
def delete_{snake_name}(
|
|
81
|
+
id: str,
|
|
82
|
+
service: {class_name}Service = Depends(),
|
|
83
|
+
):
|
|
84
|
+
"""Delete a {snake_name}."""
|
|
85
|
+
service.delete(id)
|
|
86
|
+
return {{"deleted": True}}
|
|
87
|
+
'''
|
|
88
|
+
else:
|
|
89
|
+
return f'''"""
|
|
90
|
+
{class_name} Controller - HTTP endpoints.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
from tachyon_api import Router, Depends
|
|
94
|
+
from .{snake_name}_service import {class_name}Service
|
|
95
|
+
from .{snake_name}_dto import {class_name}Response
|
|
96
|
+
|
|
97
|
+
router = Router(prefix="/{snake_name}s", tags=["{class_name}"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get("/", response_model={class_name}Response)
|
|
101
|
+
def get_{snake_name}s(
|
|
102
|
+
service: {class_name}Service = Depends(),
|
|
103
|
+
):
|
|
104
|
+
"""Get {snake_name}s."""
|
|
105
|
+
return service.get_all()
|
|
106
|
+
'''
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def service(snake_name: str, class_name: str, crud: bool = False) -> str:
|
|
110
|
+
if crud:
|
|
111
|
+
return f'''"""
|
|
112
|
+
{class_name} Service - Business logic.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
from typing import List, Optional
|
|
116
|
+
from tachyon_api import injectable, HTTPException
|
|
117
|
+
from .{snake_name}_repository import {class_name}Repository
|
|
118
|
+
from .{snake_name}_dto import {class_name}Create, {class_name}Update
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@injectable
|
|
122
|
+
class {class_name}Service:
|
|
123
|
+
"""
|
|
124
|
+
{class_name} business logic.
|
|
125
|
+
|
|
126
|
+
Handles validation, business rules, and orchestrates repository calls.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, repository: {class_name}Repository):
|
|
130
|
+
self.repository = repository
|
|
131
|
+
|
|
132
|
+
def find_all(self, skip: int = 0, limit: int = 100) -> List[dict]:
|
|
133
|
+
"""Get all {snake_name}s with pagination."""
|
|
134
|
+
return self.repository.find_all(skip=skip, limit=limit)
|
|
135
|
+
|
|
136
|
+
def find_by_id(self, id: str) -> dict:
|
|
137
|
+
"""Get a {snake_name} by ID."""
|
|
138
|
+
result = self.repository.find_by_id(id)
|
|
139
|
+
if not result:
|
|
140
|
+
raise HTTPException(status_code=404, detail="{class_name} not found")
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def create(self, data: {class_name}Create) -> dict:
|
|
144
|
+
"""Create a new {snake_name}."""
|
|
145
|
+
return self.repository.create(data)
|
|
146
|
+
|
|
147
|
+
def update(self, id: str, data: {class_name}Update) -> dict:
|
|
148
|
+
"""Update a {snake_name}."""
|
|
149
|
+
existing = self.find_by_id(id)
|
|
150
|
+
return self.repository.update(id, data)
|
|
151
|
+
|
|
152
|
+
def delete(self, id: str) -> None:
|
|
153
|
+
"""Delete a {snake_name}."""
|
|
154
|
+
self.find_by_id(id) # Verify exists
|
|
155
|
+
self.repository.delete(id)
|
|
156
|
+
'''
|
|
157
|
+
else:
|
|
158
|
+
return f'''"""
|
|
159
|
+
{class_name} Service - Business logic.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
from tachyon_api import injectable
|
|
163
|
+
from .{snake_name}_repository import {class_name}Repository
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@injectable
|
|
167
|
+
class {class_name}Service:
|
|
168
|
+
"""
|
|
169
|
+
{class_name} business logic.
|
|
170
|
+
|
|
171
|
+
Handles validation, business rules, and orchestrates repository calls.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, repository: {class_name}Repository):
|
|
175
|
+
self.repository = repository
|
|
176
|
+
|
|
177
|
+
def get_all(self) -> dict:
|
|
178
|
+
"""Get all {snake_name}s."""
|
|
179
|
+
items = self.repository.find_all()
|
|
180
|
+
return {{"items": items, "count": len(items)}}
|
|
181
|
+
'''
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def repository(snake_name: str, class_name: str, crud: bool = False) -> str:
|
|
185
|
+
if crud:
|
|
186
|
+
return f'''"""
|
|
187
|
+
{class_name} Repository - Data access layer.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
from typing import List, Optional
|
|
191
|
+
from tachyon_api import injectable
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@injectable
|
|
195
|
+
class {class_name}Repository:
|
|
196
|
+
"""
|
|
197
|
+
{class_name} data access.
|
|
198
|
+
|
|
199
|
+
Handles database operations. Replace with actual DB implementation.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self):
|
|
203
|
+
# TODO: Replace with actual database connection
|
|
204
|
+
self._data: dict = {{}}
|
|
205
|
+
|
|
206
|
+
def find_all(self, skip: int = 0, limit: int = 100) -> List[dict]:
|
|
207
|
+
"""Get all {snake_name}s with pagination."""
|
|
208
|
+
items = list(self._data.values())
|
|
209
|
+
return items[skip:skip + limit]
|
|
210
|
+
|
|
211
|
+
def find_by_id(self, id: str) -> Optional[dict]:
|
|
212
|
+
"""Find a {snake_name} by ID."""
|
|
213
|
+
return self._data.get(id)
|
|
214
|
+
|
|
215
|
+
def create(self, data) -> dict:
|
|
216
|
+
"""Create a new {snake_name}."""
|
|
217
|
+
import uuid
|
|
218
|
+
id = str(uuid.uuid4())
|
|
219
|
+
item = {{"id": id, **data.__dict__}}
|
|
220
|
+
self._data[id] = item
|
|
221
|
+
return item
|
|
222
|
+
|
|
223
|
+
def update(self, id: str, data) -> dict:
|
|
224
|
+
"""Update a {snake_name}."""
|
|
225
|
+
item = self._data.get(id, {{}})
|
|
226
|
+
for key, value in data.__dict__.items():
|
|
227
|
+
if value is not None:
|
|
228
|
+
item[key] = value
|
|
229
|
+
self._data[id] = item
|
|
230
|
+
return item
|
|
231
|
+
|
|
232
|
+
def delete(self, id: str) -> None:
|
|
233
|
+
"""Delete a {snake_name}."""
|
|
234
|
+
self._data.pop(id, None)
|
|
235
|
+
'''
|
|
236
|
+
else:
|
|
237
|
+
return f'''"""
|
|
238
|
+
{class_name} Repository - Data access layer.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
from typing import List
|
|
242
|
+
from tachyon_api import injectable
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@injectable
|
|
246
|
+
class {class_name}Repository:
|
|
247
|
+
"""
|
|
248
|
+
{class_name} data access.
|
|
249
|
+
|
|
250
|
+
Handles database operations. Replace with actual DB implementation.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(self):
|
|
254
|
+
# TODO: Replace with actual database connection
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
def find_all(self) -> List[dict]:
|
|
258
|
+
"""Get all {snake_name}s."""
|
|
259
|
+
# TODO: Implement database query
|
|
260
|
+
return []
|
|
261
|
+
'''
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def dto(snake_name: str, class_name: str, crud: bool = False) -> str:
|
|
265
|
+
if crud:
|
|
266
|
+
return f'''"""
|
|
267
|
+
{class_name} DTOs - Data Transfer Objects.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
from typing import Optional
|
|
271
|
+
from tachyon_api import Struct
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class {class_name}Base(Struct):
|
|
275
|
+
"""Base {snake_name} fields."""
|
|
276
|
+
name: str
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class {class_name}Create({class_name}Base):
|
|
280
|
+
"""DTO for creating a {snake_name}."""
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class {class_name}Update(Struct):
|
|
285
|
+
"""DTO for updating a {snake_name}."""
|
|
286
|
+
name: Optional[str] = None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class {class_name}Response({class_name}Base):
|
|
290
|
+
"""DTO for {snake_name} responses."""
|
|
291
|
+
id: str
|
|
292
|
+
'''
|
|
293
|
+
else:
|
|
294
|
+
return f'''"""
|
|
295
|
+
{class_name} DTOs - Data Transfer Objects.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
from typing import List
|
|
299
|
+
from tachyon_api import Struct
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class {class_name}Response(Struct):
|
|
303
|
+
"""Response DTO for {snake_name}."""
|
|
304
|
+
items: List[dict]
|
|
305
|
+
count: int
|
|
306
|
+
'''
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def test_service(snake_name: str, class_name: str) -> str:
|
|
310
|
+
return f'''"""
|
|
311
|
+
Tests for {class_name}Service.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
import pytest
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Test{class_name}Service:
|
|
318
|
+
"""Unit tests for {class_name}Service."""
|
|
319
|
+
|
|
320
|
+
def test_placeholder(self):
|
|
321
|
+
"""Placeholder test - implement your tests here."""
|
|
322
|
+
# TODO: Implement actual tests
|
|
323
|
+
# from modules.{snake_name} import {class_name}Service, {class_name}Repository
|
|
324
|
+
#
|
|
325
|
+
# repository = {class_name}Repository()
|
|
326
|
+
# service = {class_name}Service(repository)
|
|
327
|
+
# result = service.get_all()
|
|
328
|
+
# assert result is not None
|
|
329
|
+
assert True
|
|
330
|
+
'''
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core functionality for Tachyon API.
|
|
3
|
+
|
|
4
|
+
This module contains the core components of the framework:
|
|
5
|
+
- lifecycle: Application startup/shutdown event handling
|
|
6
|
+
- websocket: WebSocket route handling
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .lifecycle import LifecycleManager
|
|
10
|
+
from .websocket import WebSocketManager
|
|
11
|
+
|
|
12
|
+
__all__ = ["LifecycleManager", "WebSocketManager"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lifecycle event management for Tachyon applications.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- User-provided lifespan context managers
|
|
6
|
+
- @app.on_event('startup') handlers
|
|
7
|
+
- @app.on_event('shutdown') handlers
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from typing import Callable, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LifecycleManager:
|
|
16
|
+
"""
|
|
17
|
+
Manages application lifecycle events (startup/shutdown).
|
|
18
|
+
|
|
19
|
+
This class encapsulates the logic for:
|
|
20
|
+
- Registering startup/shutdown handlers via decorators
|
|
21
|
+
- Combining user-provided lifespan with event handlers
|
|
22
|
+
- Executing handlers in the correct order
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, user_lifespan: Optional[Callable] = None):
|
|
26
|
+
"""
|
|
27
|
+
Initialize lifecycle manager.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
user_lifespan: Optional async context manager for startup/shutdown
|
|
31
|
+
"""
|
|
32
|
+
self._user_lifespan = user_lifespan
|
|
33
|
+
self._startup_handlers: List[Callable] = []
|
|
34
|
+
self._shutdown_handlers: List[Callable] = []
|
|
35
|
+
|
|
36
|
+
def create_combined_lifespan(self):
|
|
37
|
+
"""
|
|
38
|
+
Create a combined lifespan context manager that handles both
|
|
39
|
+
user-provided lifespan and on_event handlers.
|
|
40
|
+
|
|
41
|
+
Note: This returns a factory that captures `self` and dynamically
|
|
42
|
+
accesses handlers at runtime (not at definition time).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
An async context manager function compatible with Starlette
|
|
46
|
+
"""
|
|
47
|
+
lifecycle_manager = self
|
|
48
|
+
|
|
49
|
+
@asynccontextmanager
|
|
50
|
+
async def combined_lifespan(app):
|
|
51
|
+
# Run startup handlers (accessed dynamically)
|
|
52
|
+
for handler in lifecycle_manager._startup_handlers:
|
|
53
|
+
if asyncio.iscoroutinefunction(handler):
|
|
54
|
+
await handler()
|
|
55
|
+
else:
|
|
56
|
+
handler()
|
|
57
|
+
|
|
58
|
+
# Run user-provided lifespan if any
|
|
59
|
+
if lifecycle_manager._user_lifespan is not None:
|
|
60
|
+
# Pass the app to user lifespan (it expects the Tachyon app)
|
|
61
|
+
async with lifecycle_manager._user_lifespan(app):
|
|
62
|
+
yield
|
|
63
|
+
else:
|
|
64
|
+
yield
|
|
65
|
+
|
|
66
|
+
# Run shutdown handlers (accessed dynamically)
|
|
67
|
+
for handler in lifecycle_manager._shutdown_handlers:
|
|
68
|
+
if asyncio.iscoroutinefunction(handler):
|
|
69
|
+
await handler()
|
|
70
|
+
else:
|
|
71
|
+
handler()
|
|
72
|
+
|
|
73
|
+
return combined_lifespan
|
|
74
|
+
|
|
75
|
+
def on_event_decorator(self, event_type: str):
|
|
76
|
+
"""
|
|
77
|
+
Create a decorator to register startup or shutdown event handlers.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
event_type: Either 'startup' or 'shutdown'
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A decorator that registers the handler function
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
@app.on_event('startup')
|
|
87
|
+
async def on_startup():
|
|
88
|
+
print('Starting up...')
|
|
89
|
+
|
|
90
|
+
@app.on_event('shutdown')
|
|
91
|
+
def on_shutdown():
|
|
92
|
+
print('Shutting down...')
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def decorator(func: Callable):
|
|
96
|
+
if event_type == "startup":
|
|
97
|
+
self._startup_handlers.append(func)
|
|
98
|
+
elif event_type == "shutdown":
|
|
99
|
+
self._shutdown_handlers.append(func)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Invalid event type: {event_type}. Use 'startup' or 'shutdown'."
|
|
103
|
+
)
|
|
104
|
+
return func
|
|
105
|
+
|
|
106
|
+
return decorator
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket handling for Tachyon applications.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- WebSocket route registration
|
|
6
|
+
- Path parameter injection
|
|
7
|
+
- WebSocket handler wrapping
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from starlette.routing import WebSocketRoute
|
|
14
|
+
from starlette.websockets import WebSocket
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WebSocketManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages WebSocket routes and handlers.
|
|
20
|
+
|
|
21
|
+
This class encapsulates the logic for:
|
|
22
|
+
- Registering WebSocket endpoints via decorator
|
|
23
|
+
- Wrapping user handlers with parameter injection
|
|
24
|
+
- Path parameter extraction and injection
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, router):
|
|
28
|
+
"""
|
|
29
|
+
Initialize WebSocket manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
router: The Starlette router instance
|
|
33
|
+
"""
|
|
34
|
+
self._router = router
|
|
35
|
+
|
|
36
|
+
def websocket_decorator(self, path: str):
|
|
37
|
+
"""
|
|
38
|
+
Create a decorator to register a WebSocket endpoint.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: URL path pattern for the WebSocket endpoint
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A decorator that registers the WebSocket handler
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
@app.websocket("/ws")
|
|
48
|
+
async def websocket_endpoint(websocket):
|
|
49
|
+
await websocket.accept()
|
|
50
|
+
data = await websocket.receive_text()
|
|
51
|
+
await websocket.send_text(f"Echo: {data}")
|
|
52
|
+
await websocket.close()
|
|
53
|
+
|
|
54
|
+
@app.websocket("/ws/{room_id}")
|
|
55
|
+
async def room_endpoint(websocket, room_id: str):
|
|
56
|
+
await websocket.accept()
|
|
57
|
+
await websocket.send_text(f"Welcome to {room_id}")
|
|
58
|
+
await websocket.close()
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def decorator(func: Callable):
|
|
62
|
+
self.add_websocket_route(path, func)
|
|
63
|
+
return func
|
|
64
|
+
|
|
65
|
+
return decorator
|
|
66
|
+
|
|
67
|
+
def add_websocket_route(self, path: str, endpoint_func: Callable):
|
|
68
|
+
"""
|
|
69
|
+
Register a WebSocket route with the application.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path: URL path pattern (supports path parameters)
|
|
73
|
+
endpoint_func: The async WebSocket handler function
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
async def websocket_handler(websocket: WebSocket):
|
|
77
|
+
# Extract path parameters
|
|
78
|
+
path_params = websocket.path_params
|
|
79
|
+
|
|
80
|
+
# Build kwargs for the handler
|
|
81
|
+
kwargs = {"websocket": websocket}
|
|
82
|
+
|
|
83
|
+
# Inject path parameters if the handler accepts them
|
|
84
|
+
sig = inspect.signature(endpoint_func)
|
|
85
|
+
for param in sig.parameters.values():
|
|
86
|
+
if param.name != "websocket" and param.name in path_params:
|
|
87
|
+
kwargs[param.name] = path_params[param.name]
|
|
88
|
+
|
|
89
|
+
await endpoint_func(**kwargs)
|
|
90
|
+
|
|
91
|
+
route = WebSocketRoute(path, endpoint=websocket_handler)
|
|
92
|
+
self._router.routes.append(route)
|
tachyon_api/di.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tachyon Web Framework - Dependency Injection Module
|
|
3
|
+
|
|
4
|
+
This module provides a lightweight dependency injection system that supports
|
|
5
|
+
both explicit and implicit dependency resolution with singleton pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Set, Type, TypeVar, Callable, Optional, Any
|
|
9
|
+
|
|
10
|
+
# Global registry of injectable classes
|
|
11
|
+
_registry: Set[Type] = set()
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Depends:
|
|
17
|
+
"""
|
|
18
|
+
Marker class for explicit dependency injection.
|
|
19
|
+
|
|
20
|
+
Use this as a default parameter value to explicitly mark a parameter
|
|
21
|
+
as a dependency that should be resolved and injected automatically.
|
|
22
|
+
|
|
23
|
+
Supports two modes:
|
|
24
|
+
1. Type-based: Depends() - resolves the dependency based on type annotation
|
|
25
|
+
2. Callable-based: Depends(callable) - calls the function to get the value
|
|
26
|
+
|
|
27
|
+
Example (type-based):
|
|
28
|
+
@app.get("/users")
|
|
29
|
+
def get_users(service: UserService = Depends()):
|
|
30
|
+
return service.list_all()
|
|
31
|
+
|
|
32
|
+
Example (callable-based):
|
|
33
|
+
def get_db():
|
|
34
|
+
return DatabaseConnection()
|
|
35
|
+
|
|
36
|
+
@app.get("/items")
|
|
37
|
+
def get_items(db = Depends(get_db)):
|
|
38
|
+
return db.query("SELECT * FROM items")
|
|
39
|
+
|
|
40
|
+
Example (async callable):
|
|
41
|
+
async def get_current_user():
|
|
42
|
+
return await fetch_user_from_token()
|
|
43
|
+
|
|
44
|
+
@app.get("/profile")
|
|
45
|
+
async def profile(user = Depends(get_current_user)):
|
|
46
|
+
return {"name": user.name}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, dependency: Optional[Callable[..., Any]] = None):
|
|
50
|
+
"""
|
|
51
|
+
Initialize a dependency marker.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
dependency: Optional callable (function, lambda, class) that will be
|
|
55
|
+
called to produce the dependency value. If None, the
|
|
56
|
+
dependency is resolved based on the parameter's type annotation.
|
|
57
|
+
"""
|
|
58
|
+
self.dependency = dependency
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def injectable(cls: Type[T]) -> Type[T]:
|
|
62
|
+
"""
|
|
63
|
+
Decorator to mark a class as injectable for dependency injection.
|
|
64
|
+
|
|
65
|
+
Classes marked with this decorator can be automatically resolved and
|
|
66
|
+
injected into endpoint functions and other injectable classes.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
cls: The class to mark as injectable
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The same class, now registered for dependency injection
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
@injectable
|
|
76
|
+
class UserRepository:
|
|
77
|
+
def __init__(self, db: Database):
|
|
78
|
+
self.db = db
|
|
79
|
+
|
|
80
|
+
@injectable
|
|
81
|
+
class UserService:
|
|
82
|
+
def __init__(self, repo: UserRepository):
|
|
83
|
+
self.repo = repo
|
|
84
|
+
"""
|
|
85
|
+
_registry.add(cls)
|
|
86
|
+
return cls
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tachyon Exceptions Module
|
|
3
|
+
|
|
4
|
+
Provides HTTP exception classes for clean error handling in endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HTTPException(Exception):
|
|
11
|
+
"""
|
|
12
|
+
HTTP exception that can be raised in endpoints to return HTTP error responses.
|
|
13
|
+
|
|
14
|
+
Similar to FastAPI's HTTPException, this allows endpoints to abort request
|
|
15
|
+
processing and return a specific HTTP status code with a detail message.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
status_code: The HTTP status code for the response
|
|
19
|
+
detail: A human-readable error message
|
|
20
|
+
headers: Optional dictionary of headers to include in the response
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
@app.get("/items/{item_id}")
|
|
24
|
+
def get_item(item_id: int):
|
|
25
|
+
if item_id not in items:
|
|
26
|
+
raise HTTPException(status_code=404, detail="Item not found")
|
|
27
|
+
return items[item_id]
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
status_code: int,
|
|
33
|
+
detail: Any = None,
|
|
34
|
+
headers: Optional[Dict[str, str]] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.status_code = status_code
|
|
37
|
+
self.detail = detail
|
|
38
|
+
self.headers = headers
|
|
39
|
+
super().__init__(detail)
|
tachyon_api/files.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tachyon Web Framework - File Upload Module
|
|
3
|
+
|
|
4
|
+
This module provides classes for handling file uploads in a FastAPI-compatible way.
|
|
5
|
+
It wraps Starlette's file handling functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from starlette.datastructures import UploadFile as StarletteUploadFile
|
|
9
|
+
|
|
10
|
+
# Re-export Starlette's UploadFile for use in Tachyon
|
|
11
|
+
# This provides a familiar API for users coming from FastAPI
|
|
12
|
+
UploadFile = StarletteUploadFile
|
|
13
|
+
|
|
14
|
+
__all__ = ["UploadFile"]
|