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.
Files changed (44) hide show
  1. tachyon_api/__init__.py +59 -0
  2. tachyon_api/app.py +699 -0
  3. tachyon_api/background.py +72 -0
  4. tachyon_api/cache.py +270 -0
  5. tachyon_api/cli/__init__.py +9 -0
  6. tachyon_api/cli/__main__.py +8 -0
  7. tachyon_api/cli/commands/__init__.py +5 -0
  8. tachyon_api/cli/commands/generate.py +190 -0
  9. tachyon_api/cli/commands/lint.py +186 -0
  10. tachyon_api/cli/commands/new.py +82 -0
  11. tachyon_api/cli/commands/openapi.py +128 -0
  12. tachyon_api/cli/main.py +69 -0
  13. tachyon_api/cli/templates/__init__.py +8 -0
  14. tachyon_api/cli/templates/project.py +194 -0
  15. tachyon_api/cli/templates/service.py +330 -0
  16. tachyon_api/core/__init__.py +12 -0
  17. tachyon_api/core/lifecycle.py +106 -0
  18. tachyon_api/core/websocket.py +92 -0
  19. tachyon_api/di.py +86 -0
  20. tachyon_api/exceptions.py +39 -0
  21. tachyon_api/files.py +14 -0
  22. tachyon_api/middlewares/__init__.py +4 -0
  23. tachyon_api/middlewares/core.py +40 -0
  24. tachyon_api/middlewares/cors.py +159 -0
  25. tachyon_api/middlewares/logger.py +123 -0
  26. tachyon_api/models.py +73 -0
  27. tachyon_api/openapi.py +419 -0
  28. tachyon_api/params.py +268 -0
  29. tachyon_api/processing/__init__.py +14 -0
  30. tachyon_api/processing/dependencies.py +172 -0
  31. tachyon_api/processing/parameters.py +484 -0
  32. tachyon_api/processing/response_processor.py +93 -0
  33. tachyon_api/responses.py +92 -0
  34. tachyon_api/router.py +161 -0
  35. tachyon_api/security.py +295 -0
  36. tachyon_api/testing.py +110 -0
  37. tachyon_api/utils/__init__.py +15 -0
  38. tachyon_api/utils/type_converter.py +113 -0
  39. tachyon_api/utils/type_utils.py +162 -0
  40. tachyon_api-0.9.0.dist-info/METADATA +291 -0
  41. tachyon_api-0.9.0.dist-info/RECORD +44 -0
  42. tachyon_api-0.9.0.dist-info/WHEEL +4 -0
  43. tachyon_api-0.9.0.dist-info/entry_points.txt +3 -0
  44. tachyon_api-0.9.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,93 @@
1
+ """
2
+ Response processing for Tachyon applications.
3
+
4
+ Handles:
5
+ - Response model validation
6
+ - Struct serialization
7
+ - Background task execution
8
+ - Response type detection
9
+ """
10
+
11
+ import asyncio
12
+ import msgspec
13
+ from typing import Any, Optional
14
+
15
+ from starlette.responses import Response
16
+
17
+ from ..models import Struct
18
+ from ..responses import TachyonJSONResponse, response_validation_error_response
19
+
20
+
21
+ class ResponseProcessor:
22
+ """
23
+ Processes endpoint return values into HTTP responses.
24
+
25
+ This class encapsulates the logic for:
26
+ - Running background tasks
27
+ - Validating against response_model
28
+ - Converting Struct to JSON
29
+ - Creating TachyonJSONResponse
30
+ """
31
+
32
+ @staticmethod
33
+ async def process_response(
34
+ payload: Any,
35
+ response_model: Optional[type],
36
+ background_tasks: Optional[Any],
37
+ ) -> Response:
38
+ """
39
+ Process the endpoint return value into an HTTP response.
40
+
41
+ Args:
42
+ payload: The value returned by the endpoint
43
+ response_model: Optional Struct class for validation
44
+ background_tasks: Optional BackgroundTasks instance to execute
45
+
46
+ Returns:
47
+ A Starlette Response object
48
+ """
49
+ # Run background tasks if any were registered
50
+ if background_tasks is not None:
51
+ await background_tasks.run_tasks()
52
+
53
+ # If the endpoint already returned a Response object, return it directly
54
+ if isinstance(payload, Response):
55
+ return payload
56
+
57
+ # Validate/convert response against response_model if provided
58
+ if response_model is not None:
59
+ try:
60
+ payload = msgspec.convert(payload, response_model)
61
+ except Exception as e:
62
+ return response_validation_error_response(str(e))
63
+
64
+ # Convert Struct objects to dictionaries for JSON serialization
65
+ if isinstance(payload, Struct):
66
+ payload = msgspec.to_builtins(payload)
67
+ elif isinstance(payload, dict):
68
+ # Convert any Struct values in the dictionary
69
+ for key, value in payload.items():
70
+ if isinstance(value, Struct):
71
+ payload[key] = msgspec.to_builtins(value)
72
+
73
+ return TachyonJSONResponse(payload)
74
+
75
+ @staticmethod
76
+ async def call_endpoint(
77
+ endpoint_func,
78
+ kwargs_to_inject: dict,
79
+ ) -> Any:
80
+ """
81
+ Call the endpoint function with injected parameters.
82
+
83
+ Args:
84
+ endpoint_func: The endpoint function to call
85
+ kwargs_to_inject: Dictionary of parameters to inject
86
+
87
+ Returns:
88
+ The payload returned by the endpoint
89
+ """
90
+ if asyncio.iscoroutinefunction(endpoint_func):
91
+ return await endpoint_func(**kwargs_to_inject)
92
+ else:
93
+ return endpoint_func(**kwargs_to_inject)
@@ -0,0 +1,92 @@
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
+ from .models import encode_json
10
+
11
+
12
+ class TachyonJSONResponse(JSONResponse):
13
+ """High-performance JSON response using orjson for serialization."""
14
+
15
+ media_type = "application/json"
16
+
17
+ def render(self, content) -> bytes: # type: ignore[override]
18
+ # Use centralized encoder to support Struct, UUID, date, datetime, etc.
19
+ return encode_json(content)
20
+
21
+
22
+ # Simple helper functions for common response patterns
23
+ def success_response(data=None, message="Success", status_code=200):
24
+ """Create a success response with consistent structure"""
25
+ return TachyonJSONResponse(
26
+ {"success": True, "message": message, "data": data}, status_code=status_code
27
+ )
28
+
29
+
30
+ def error_response(error, status_code=400, code=None):
31
+ """Create an error response with consistent structure"""
32
+ response_data = {"success": False, "error": error}
33
+ if code:
34
+ response_data["code"] = code
35
+
36
+ return TachyonJSONResponse(response_data, status_code=status_code)
37
+
38
+
39
+ def not_found_response(error="Resource not found"):
40
+ """Create a 404 not found response"""
41
+ return error_response(error, status_code=404, code="NOT_FOUND")
42
+
43
+
44
+ def conflict_response(error="Resource conflict"):
45
+ """Create a 409 conflict response"""
46
+ return error_response(error, status_code=409, code="CONFLICT")
47
+
48
+
49
+ def validation_error_response(error="Validation failed", errors=None):
50
+ """Create a 422 validation error response"""
51
+ response_data = {"success": False, "error": error, "code": "VALIDATION_ERROR"}
52
+ if errors:
53
+ response_data["errors"] = errors
54
+
55
+ return TachyonJSONResponse(response_data, status_code=422)
56
+
57
+
58
+ def response_validation_error_response(error="Response validation error"):
59
+ """Create a 500 response validation error response"""
60
+ # Normalize message with prefix and include 'detail' for backward compatibility
61
+ msg = str(error)
62
+ if not msg.lower().startswith("response validation error"):
63
+ msg = f"Response validation error: {msg}"
64
+ return TachyonJSONResponse(
65
+ {
66
+ "success": False,
67
+ "error": msg,
68
+ "detail": msg,
69
+ "code": "RESPONSE_VALIDATION_ERROR",
70
+ },
71
+ status_code=500,
72
+ )
73
+
74
+
75
+ def internal_server_error_response():
76
+ """Create a 500 internal server error response for unhandled exceptions.
77
+
78
+ This intentionally avoids leaking internal exception details in the payload.
79
+ """
80
+ return TachyonJSONResponse(
81
+ {
82
+ "success": False,
83
+ "error": "Internal Server Error",
84
+ "code": "INTERNAL_SERVER_ERROR",
85
+ },
86
+ status_code=500,
87
+ )
88
+
89
+
90
+ # Re-export Starlette responses for convenience
91
+ # JSONResponse is already imported above
92
+ # HTMLResponse is now also imported
tachyon_api/router.py ADDED
@@ -0,0 +1,161 @@
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 websocket(self, path: str):
109
+ """
110
+ Decorator to register a WebSocket endpoint with this router.
111
+
112
+ Args:
113
+ path: URL path pattern for the WebSocket endpoint
114
+
115
+ Returns:
116
+ A decorator that stores the WebSocket handler
117
+
118
+ Example:
119
+ router = Router(prefix="/api")
120
+
121
+ @router.websocket("/ws")
122
+ async def websocket_endpoint(websocket):
123
+ await websocket.accept()
124
+ await websocket.send_text("Hello from router!")
125
+ await websocket.close()
126
+ """
127
+
128
+ def decorator(endpoint_func: Callable):
129
+ route_info = {
130
+ "path": path,
131
+ "method": "WEBSOCKET",
132
+ "func": endpoint_func,
133
+ "is_websocket": True,
134
+ }
135
+ self.routes.append(route_info)
136
+ return endpoint_func
137
+
138
+ return decorator
139
+
140
+ def get_full_path(self, path: str) -> str:
141
+ """
142
+ Get the full path by combining router prefix with route path.
143
+
144
+ Args:
145
+ path: The route path
146
+
147
+ Returns:
148
+ Full path with prefix applied
149
+ """
150
+ if not self.prefix:
151
+ return path
152
+
153
+ # Handle root path specially
154
+ if path == "/":
155
+ return self.prefix
156
+
157
+ # Combine prefix and path, avoiding double slashes
158
+ if path.startswith("/"):
159
+ return self.prefix + path
160
+ else:
161
+ return self.prefix + "/" + path
@@ -0,0 +1,295 @@
1
+ """
2
+ Tachyon Security Module
3
+
4
+ Provides security schemes for authentication and authorization,
5
+ compatible with FastAPI's security utilities.
6
+ """
7
+
8
+ import base64
9
+ from typing import Optional
10
+
11
+ from starlette.requests import Request
12
+
13
+ from .exceptions import HTTPException
14
+
15
+
16
+ class HTTPAuthorizationCredentials:
17
+ """
18
+ Credentials extracted from HTTP Authorization header.
19
+
20
+ Attributes:
21
+ scheme: The authentication scheme (e.g., "Bearer", "Basic")
22
+ credentials: The credentials value (e.g., the token or encoded credentials)
23
+ """
24
+
25
+ def __init__(self, scheme: str, credentials: str):
26
+ self.scheme = scheme
27
+ self.credentials = credentials
28
+
29
+
30
+ class HTTPBasicCredentials:
31
+ """
32
+ Credentials extracted from HTTP Basic authentication.
33
+
34
+ Attributes:
35
+ username: The decoded username
36
+ password: The decoded password
37
+ """
38
+
39
+ def __init__(self, username: str, password: str):
40
+ self.username = username
41
+ self.password = password
42
+
43
+
44
+ class HTTPBearer:
45
+ """
46
+ HTTP Bearer token authentication scheme.
47
+
48
+ Extracts Bearer token from the Authorization header.
49
+
50
+ Args:
51
+ auto_error: If True (default), raises HTTPException on missing/invalid token.
52
+ If False, returns None instead.
53
+
54
+ Example:
55
+ security = HTTPBearer()
56
+
57
+ @app.get("/protected")
58
+ def protected(credentials: HTTPAuthorizationCredentials = Depends(security)):
59
+ return {"token": credentials.credentials}
60
+ """
61
+
62
+ def __init__(self, auto_error: bool = True):
63
+ self.auto_error = auto_error
64
+
65
+ async def __call__(
66
+ self, request: Request
67
+ ) -> Optional[HTTPAuthorizationCredentials]:
68
+ authorization = request.headers.get("Authorization")
69
+
70
+ if not authorization:
71
+ if self.auto_error:
72
+ raise HTTPException(status_code=403, detail="Not authenticated")
73
+ return None
74
+
75
+ parts = authorization.split()
76
+ if len(parts) != 2:
77
+ if self.auto_error:
78
+ raise HTTPException(
79
+ status_code=403, detail="Invalid authorization header"
80
+ )
81
+ return None
82
+
83
+ scheme, credentials = parts
84
+
85
+ if scheme.lower() != "bearer":
86
+ if self.auto_error:
87
+ raise HTTPException(
88
+ status_code=403, detail="Invalid authentication scheme"
89
+ )
90
+ return None
91
+
92
+ return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
93
+
94
+
95
+ class HTTPBasic:
96
+ """
97
+ HTTP Basic authentication scheme.
98
+
99
+ Extracts and decodes username/password from the Authorization header.
100
+
101
+ Args:
102
+ auto_error: If True (default), raises HTTPException on missing/invalid credentials.
103
+ If False, returns None instead.
104
+ realm: The realm name to include in WWW-Authenticate header.
105
+
106
+ Example:
107
+ security = HTTPBasic()
108
+
109
+ @app.get("/admin")
110
+ def admin(credentials: HTTPBasicCredentials = Depends(security)):
111
+ if credentials.username == "admin" and credentials.password == "secret":
112
+ return {"message": "Welcome, admin!"}
113
+ raise HTTPException(status_code=401, detail="Invalid credentials")
114
+ """
115
+
116
+ def __init__(self, auto_error: bool = True, realm: Optional[str] = None):
117
+ self.auto_error = auto_error
118
+ self.realm = realm or "simple"
119
+
120
+ async def __call__(self, request: Request) -> Optional[HTTPBasicCredentials]:
121
+ authorization = request.headers.get("Authorization")
122
+
123
+ if not authorization:
124
+ if self.auto_error:
125
+ raise HTTPException(
126
+ status_code=401,
127
+ detail="Not authenticated",
128
+ headers={"WWW-Authenticate": f'Basic realm="{self.realm}"'},
129
+ )
130
+ return None
131
+
132
+ parts = authorization.split()
133
+ if len(parts) != 2 or parts[0].lower() != "basic":
134
+ if self.auto_error:
135
+ raise HTTPException(
136
+ status_code=401,
137
+ detail="Invalid authentication credentials",
138
+ headers={"WWW-Authenticate": f'Basic realm="{self.realm}"'},
139
+ )
140
+ return None
141
+
142
+ try:
143
+ decoded = base64.b64decode(parts[1]).decode("utf-8")
144
+ username, password = decoded.split(":", 1)
145
+ return HTTPBasicCredentials(username=username, password=password)
146
+ except Exception:
147
+ if self.auto_error:
148
+ raise HTTPException(
149
+ status_code=401,
150
+ detail="Invalid authentication credentials",
151
+ headers={"WWW-Authenticate": f'Basic realm="{self.realm}"'},
152
+ )
153
+ return None
154
+
155
+
156
+ class APIKeyHeader:
157
+ """
158
+ API Key authentication via HTTP header.
159
+
160
+ Args:
161
+ name: The header name to look for (e.g., "X-API-Key")
162
+ auto_error: If True (default), raises HTTPException on missing key.
163
+
164
+ Example:
165
+ api_key = APIKeyHeader(name="X-API-Key")
166
+
167
+ @app.get("/api")
168
+ def api_endpoint(key: str = Depends(api_key)):
169
+ return {"api_key": key}
170
+ """
171
+
172
+ def __init__(self, name: str, auto_error: bool = True):
173
+ self.name = name
174
+ self.auto_error = auto_error
175
+
176
+ async def __call__(self, request: Request) -> Optional[str]:
177
+ api_key = request.headers.get(self.name)
178
+
179
+ if not api_key:
180
+ if self.auto_error:
181
+ raise HTTPException(status_code=403, detail="Not authenticated")
182
+ return None
183
+
184
+ return api_key
185
+
186
+
187
+ class APIKeyQuery:
188
+ """
189
+ API Key authentication via query parameter.
190
+
191
+ Args:
192
+ name: The query parameter name to look for (e.g., "api_key")
193
+ auto_error: If True (default), raises HTTPException on missing key.
194
+
195
+ Example:
196
+ api_key = APIKeyQuery(name="api_key")
197
+
198
+ @app.get("/api")
199
+ def api_endpoint(key: str = Depends(api_key)):
200
+ return {"api_key": key}
201
+ """
202
+
203
+ def __init__(self, name: str, auto_error: bool = True):
204
+ self.name = name
205
+ self.auto_error = auto_error
206
+
207
+ async def __call__(self, request: Request) -> Optional[str]:
208
+ api_key = request.query_params.get(self.name)
209
+
210
+ if not api_key:
211
+ if self.auto_error:
212
+ raise HTTPException(status_code=403, detail="Not authenticated")
213
+ return None
214
+
215
+ return api_key
216
+
217
+
218
+ class APIKeyCookie:
219
+ """
220
+ API Key authentication via cookie.
221
+
222
+ Args:
223
+ name: The cookie name to look for (e.g., "session_token")
224
+ auto_error: If True (default), raises HTTPException on missing key.
225
+
226
+ Example:
227
+ api_key = APIKeyCookie(name="session_token")
228
+
229
+ @app.get("/api")
230
+ def api_endpoint(key: str = Depends(api_key)):
231
+ return {"api_key": key}
232
+ """
233
+
234
+ def __init__(self, name: str, auto_error: bool = True):
235
+ self.name = name
236
+ self.auto_error = auto_error
237
+
238
+ async def __call__(self, request: Request) -> Optional[str]:
239
+ api_key = request.cookies.get(self.name)
240
+
241
+ if not api_key:
242
+ if self.auto_error:
243
+ raise HTTPException(status_code=403, detail="Not authenticated")
244
+ return None
245
+
246
+ return api_key
247
+
248
+
249
+ class OAuth2PasswordBearer:
250
+ """
251
+ OAuth2 Password Bearer token scheme.
252
+
253
+ Extracts the token from Authorization header (Bearer scheme).
254
+ Similar to HTTPBearer but returns just the token string and uses 401 status.
255
+
256
+ Args:
257
+ tokenUrl: The URL to obtain the token (for OpenAPI documentation).
258
+ auto_error: If True (default), raises HTTPException on missing token.
259
+
260
+ Example:
261
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
262
+
263
+ @app.get("/users/me")
264
+ def get_current_user(token: str = Depends(oauth2_scheme)):
265
+ # Decode and validate token here
266
+ return {"token": token}
267
+ """
268
+
269
+ def __init__(self, tokenUrl: str, auto_error: bool = True):
270
+ self.tokenUrl = tokenUrl
271
+ self.auto_error = auto_error
272
+
273
+ async def __call__(self, request: Request) -> Optional[str]:
274
+ authorization = request.headers.get("Authorization")
275
+
276
+ if not authorization:
277
+ if self.auto_error:
278
+ raise HTTPException(
279
+ status_code=401,
280
+ detail="Not authenticated",
281
+ headers={"WWW-Authenticate": "Bearer"},
282
+ )
283
+ return None
284
+
285
+ parts = authorization.split()
286
+ if len(parts) != 2 or parts[0].lower() != "bearer":
287
+ if self.auto_error:
288
+ raise HTTPException(
289
+ status_code=401,
290
+ detail="Invalid authentication credentials",
291
+ headers={"WWW-Authenticate": "Bearer"},
292
+ )
293
+ return None
294
+
295
+ return parts[1]