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,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)
|
tachyon_api/responses.py
ADDED
|
@@ -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
|
tachyon_api/security.py
ADDED
|
@@ -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]
|