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,40 @@
1
+ # Internal core helpers for Tachyon middleware integration
2
+ from starlette.middleware import Middleware
3
+
4
+
5
+ def apply_middleware_to_router(router_app, middleware_class, **options):
6
+ """
7
+ Insert an ASGI middleware into Starlette's stack and rebuild the stack.
8
+
9
+ Args:
10
+ router_app: internal Starlette instance (app._router)
11
+ middleware_class: ASGI middleware class
12
+ **options: kwargs passed to the middleware constructor
13
+ """
14
+ router_app.user_middleware.insert(0, Middleware(middleware_class, **options))
15
+ router_app.middleware_stack = router_app.build_middleware_stack()
16
+
17
+
18
+ def create_decorated_middleware_class(middleware_func, middleware_type: str = "http"):
19
+ """
20
+ Create an ASGI middleware class from a decorated function with the signature
21
+ (scope, receive, send, app).
22
+
23
+ Args:
24
+ middleware_func: middleware function provided to the decorator
25
+ middleware_type: type ("http" by default) or "*" for all
26
+
27
+ Returns:
28
+ Middleware class that invokes the decorated function
29
+ """
30
+
31
+ class DecoratedMiddleware:
32
+ def __init__(self, app):
33
+ self.app = app
34
+
35
+ async def __call__(self, scope, receive, send):
36
+ if scope.get("type") == middleware_type or middleware_type == "*":
37
+ return await middleware_func(scope, receive, send, self.app)
38
+ return await self.app(scope, receive, send)
39
+
40
+ return DecoratedMiddleware
@@ -0,0 +1,159 @@
1
+ from typing import Iterable, Optional, Sequence, Tuple
2
+
3
+
4
+ class CORSMiddleware:
5
+ """
6
+ ASGI middleware for handling CORS.
7
+
8
+ Parameters:
9
+ - allow_origins: list of allowed origins (e.g., ["https://foo.com", "*"])
10
+ - allow_methods: list of allowed methods (default ["*"])
11
+ - allow_headers: list of allowed headers (default ["*"])
12
+ - allow_credentials: if True, adds Access-Control-Allow-Credentials: true
13
+ - expose_headers: list of headers to expose to the client app
14
+ - max_age: seconds to cache the preflight response
15
+
16
+ Behavior:
17
+ - If it's a preflight (OPTIONS + Access-Control-Request-Method), responds 200 with
18
+ the appropriate CORS headers.
19
+ - For normal requests, injects CORS headers into the response.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ app,
25
+ allow_origins: Iterable[str] = ("*",),
26
+ allow_methods: Iterable[str] = ("*",),
27
+ allow_headers: Iterable[str] = ("*",),
28
+ allow_credentials: bool = False,
29
+ expose_headers: Iterable[str] = (),
30
+ max_age: int = 600,
31
+ ):
32
+ self.app = app
33
+ self.allow_origins = [o for o in allow_origins]
34
+ self.allow_methods = [m.upper() for m in allow_methods]
35
+ self.allow_headers = [h for h in allow_headers]
36
+ self.allow_credentials = allow_credentials
37
+ self.expose_headers = [h for h in expose_headers]
38
+ self.max_age = max_age
39
+
40
+ @staticmethod
41
+ def _get_header(headers: Sequence[Tuple[bytes, bytes]], name: str) -> Optional[str]:
42
+ # Find a header in a case-insensitive manner and decode it
43
+ lower = name.lower().encode()
44
+ for k, v in headers or []:
45
+ if k.lower() == lower:
46
+ try:
47
+ return v.decode()
48
+ except Exception:
49
+ return None
50
+ return None
51
+
52
+ @staticmethod
53
+ def _append_header(headers: list[Tuple[bytes, bytes]], name: str, value: str):
54
+ # Append header encoded as bytes as required by ASGI
55
+ headers.append((name.encode(), value.encode()))
56
+
57
+ def _origin_allowed(self, origin: Optional[str]) -> bool:
58
+ if origin is None:
59
+ return False
60
+ if "*" in self.allow_origins:
61
+ return True
62
+ return origin in self.allow_origins
63
+
64
+ async def __call__(self, scope, receive, send):
65
+ if scope.get("type") != "http":
66
+ return await self.app(scope, receive, send)
67
+
68
+ req_headers: Sequence[Tuple[bytes, bytes]] = scope.get("headers", []) or []
69
+ origin = self._get_header(req_headers, "origin")
70
+ method = scope.get("method", "").upper()
71
+ is_preflight = (
72
+ method == "OPTIONS"
73
+ and self._get_header(req_headers, "access-control-request-method")
74
+ is not None
75
+ )
76
+
77
+ # Build common CORS headers
78
+ def build_cors_headers() -> list[Tuple[bytes, bytes]]:
79
+ headers_out: list[Tuple[bytes, bytes]] = []
80
+ if not self._origin_allowed(origin):
81
+ return headers_out
82
+
83
+ # Access-Control-Allow-Origin
84
+ if "*" in self.allow_origins and not self.allow_credentials:
85
+ self._append_header(headers_out, "access-control-allow-origin", "*")
86
+ else:
87
+ self._append_header(headers_out, "access-control-allow-origin", origin)
88
+ # Necessary for proxies/caches when echoing the origin
89
+ self._append_header(headers_out, "vary", "Origin")
90
+
91
+ # Credentials
92
+ if self.allow_credentials:
93
+ self._append_header(
94
+ headers_out, "access-control-allow-credentials", "true"
95
+ )
96
+
97
+ return headers_out
98
+
99
+ # Handle preflight
100
+ if is_preflight:
101
+ resp_headers = build_cors_headers()
102
+ if resp_headers:
103
+ # Methods
104
+ allow_methods = (
105
+ ", ".join(self.allow_methods)
106
+ if "*" not in self.allow_methods
107
+ else "*"
108
+ )
109
+ self._append_header(
110
+ resp_headers, "access-control-allow-methods", allow_methods
111
+ )
112
+
113
+ # Requested headers or wildcard
114
+ req_acrh = self._get_header(
115
+ req_headers, "access-control-request-headers"
116
+ )
117
+ if "*" in self.allow_headers:
118
+ allow_headers = req_acrh or "*"
119
+ else:
120
+ allow_headers = ", ".join(self.allow_headers)
121
+ if allow_headers:
122
+ self._append_header(
123
+ resp_headers, "access-control-allow-headers", allow_headers
124
+ )
125
+
126
+ # Max-Age
127
+ if self.max_age:
128
+ self._append_header(
129
+ resp_headers, "access-control-max-age", str(self.max_age)
130
+ )
131
+
132
+ # Respond directly
133
+ await send(
134
+ {
135
+ "type": "http.response.start",
136
+ "status": 200,
137
+ "headers": resp_headers,
138
+ }
139
+ )
140
+ await send({"type": "http.response.body", "body": b""})
141
+ return
142
+ # If origin not allowed, continue the chain (app will respond accordingly)
143
+
144
+ # Normal requests: inject CORS headers into the response
145
+ cors_headers = build_cors_headers()
146
+
147
+ # Expose-Headers
148
+ if cors_headers and self.expose_headers:
149
+ expose = ", ".join(self.expose_headers)
150
+ self._append_header(cors_headers, "access-control-expose-headers", expose)
151
+
152
+ async def send_wrapper(message):
153
+ if message.get("type") == "http.response.start" and cors_headers:
154
+ headers = list(message.get("headers", []) or [])
155
+ headers.extend(cors_headers)
156
+ message["headers"] = headers
157
+ return await send(message)
158
+
159
+ return await self.app(scope, receive, send_wrapper)
@@ -0,0 +1,123 @@
1
+ import logging
2
+ import time
3
+ from typing import Optional, Sequence, Tuple
4
+
5
+
6
+ class LoggerMiddleware:
7
+ """
8
+ ASGI middleware for logging requests and responses.
9
+
10
+ Options:
11
+ - logger: logging.Logger instance (defaults to "tachyon.logger")
12
+ - level: log level (defaults to logging.INFO)
13
+ - include_headers: if True, log request and response headers
14
+ - redact_headers: list of header names to redact
15
+ - log_request_body: if True, try to read and log the request body (HTTP)
16
+
17
+ Note: To keep it simple and non-intrusive, the body is only logged if it's
18
+ available in a single body message (typical in tests/sync). In production,
19
+ reading the body implies buffering receive/send, which we intentionally avoid
20
+ here to prevent side effects.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ app,
26
+ logger: Optional[logging.Logger] = None,
27
+ level: int = logging.INFO,
28
+ include_headers: bool = False,
29
+ redact_headers: Optional[Sequence[str]] = None,
30
+ log_request_body: bool = False,
31
+ ):
32
+ self.app = app
33
+ self.logger = logger or logging.getLogger("tachyon.logger")
34
+ self.level = level
35
+ self.include_headers = include_headers
36
+ self.redact_headers = {h.lower() for h in (redact_headers or [])}
37
+ self.log_request_body = log_request_body
38
+
39
+ async def __call__(self, scope, receive, send):
40
+ if scope.get("type") != "http":
41
+ return await self.app(scope, receive, send)
42
+
43
+ method: str = scope.get("method", "")
44
+ path: str = scope.get("path", "")
45
+ start = time.time()
46
+ status_code_holder = {"status": None}
47
+ response_headers_holder: list[Tuple[bytes, bytes]] = []
48
+
49
+ # Simple, non-intrusive request body preview
50
+ request_body_preview = None
51
+ if self.log_request_body:
52
+ # Non-intrusive attempt: peek the body if the first receive contains it fully
53
+ body_chunks = []
54
+ more_body = True
55
+
56
+ async def recv_wrapper():
57
+ nonlocal more_body
58
+ message = await receive()
59
+ if message.get("type") == "http.request":
60
+ body = message.get("body", b"")
61
+ if body:
62
+ body_chunks.append(body)
63
+ more_body = message.get("more_body", False)
64
+ return message
65
+
66
+ # Perform a one-time peek without fully consuming the stream
67
+ # If body is present and more_body is False, log a small preview
68
+ first_msg = await recv_wrapper()
69
+
70
+ async def receive_passthrough():
71
+ # Return the pre-received message first, then pass-through
72
+ nonlocal first_msg
73
+ if first_msg is not None:
74
+ m = first_msg
75
+ first_msg = None
76
+ return m
77
+ return await receive()
78
+
79
+ receive_to_use = receive_passthrough
80
+ if body_chunks and not more_body:
81
+ try:
82
+ request_body_preview = b"".join(body_chunks)[:2048].decode(
83
+ "utf-8", "replace"
84
+ )
85
+ except Exception:
86
+ request_body_preview = "<non-text body>"
87
+ else:
88
+ receive_to_use = receive
89
+
90
+ def _normalized_headers(headers: Sequence[Tuple[bytes, bytes]]):
91
+ out = []
92
+ for k, v in headers:
93
+ name = k.decode().lower()
94
+ if name in self.redact_headers:
95
+ out.append((name, "<redacted>"))
96
+ else:
97
+ out.append((name, v.decode(errors="replace")))
98
+ return out
99
+
100
+ self.logger.log(self.level, f"--> {method} {path}")
101
+ if self.include_headers:
102
+ req_headers = _normalized_headers(scope.get("headers", []) or [])
103
+ self.logger.log(self.level, f" req headers: {req_headers}")
104
+ if request_body_preview is not None:
105
+ self.logger.log(self.level, f" req body: {request_body_preview}")
106
+
107
+ async def send_wrapper(message):
108
+ if message.get("type") == "http.response.start":
109
+ status_code_holder["status"] = message.get("status", 0)
110
+ response_headers_holder[:] = list(message.get("headers", []) or [])
111
+ return await send(message)
112
+
113
+ try:
114
+ await self.app(scope, receive_to_use, send_wrapper)
115
+ finally:
116
+ duration = time.time() - start
117
+ status = status_code_holder["status"] or 0
118
+ self.logger.log(
119
+ self.level, f"<-- {method} {path} {status} ({duration:.4f}s)"
120
+ )
121
+ if self.include_headers and response_headers_holder:
122
+ res_headers = _normalized_headers(response_headers_holder)
123
+ self.logger.log(self.level, f" res headers: {res_headers}")
tachyon_api/models.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ Tachyon Web Framework - Data Models Module
3
+
4
+ This module provides the base model class for request/response data validation
5
+ using msgspec for high-performance JSON serialization and validation.
6
+ The module enhances msgspec with orjson for even faster JSON processing.
7
+ """
8
+
9
+ import datetime
10
+ import uuid
11
+ from typing import Any, Dict, Type, TypeVar, Optional, Union
12
+
13
+ import msgspec
14
+ import orjson
15
+ from msgspec import Struct, Meta
16
+
17
+ __all__ = ["Struct", "Meta", "encode_json", "decode_json"]
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ def _orjson_default(obj: Any) -> Any:
23
+ """Default function for orjson to serialize types it doesn't support natively."""
24
+ if isinstance(obj, (datetime.date, datetime.datetime)):
25
+ return obj.isoformat()
26
+ if isinstance(obj, uuid.UUID):
27
+ return str(obj)
28
+ if isinstance(obj, Struct):
29
+ return msgspec.to_builtins(obj)
30
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
31
+
32
+
33
+ def encode_json(obj: Any, option: Optional[int] = None) -> bytes:
34
+ """
35
+ Encode a Python object to JSON using orjson.
36
+
37
+ Args:
38
+ obj: Object to encode (can be a Struct instance or any JSON-serializable object)
39
+ option: orjson option flags (e.g., orjson.OPT_INDENT_2)
40
+
41
+ Returns:
42
+ JSON-encoded bytes
43
+ """
44
+ opts = (
45
+ option
46
+ or orjson.OPT_SERIALIZE_DATACLASS | orjson.OPT_SERIALIZE_UUID | orjson.OPT_UTC_Z
47
+ )
48
+ return orjson.dumps(obj, default=_orjson_default, option=opts)
49
+
50
+
51
+ def decode_json(data: Union[bytes, str], type_: Type[T] = Dict[str, Any]) -> T:
52
+ """
53
+ Decode JSON to a Python object using orjson.
54
+ If a Struct type is provided, the decoded data will be converted to that type.
55
+
56
+ Args:
57
+ data: JSON data as bytes or string
58
+ type_: Target type (default is Dict[str, Any])
59
+
60
+ Returns:
61
+ Decoded object of the specified type
62
+ """
63
+ if isinstance(data, str):
64
+ data = data.encode("utf-8")
65
+
66
+ # First use orjson for fast JSON parsing
67
+ parsed_data = orjson.loads(data)
68
+
69
+ # If the target type is a Struct or similar msgspec type, use msgspec.convert
70
+ if isinstance(type_, type) and issubclass(type_, Struct):
71
+ return msgspec.convert(parsed_data, type_)
72
+
73
+ return parsed_data