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.

@@ -0,0 +1,5 @@
1
+ from .cors import CORSMiddleware
2
+ from .logger import LoggerMiddleware
3
+
4
+ __all__ = ["CORSMiddleware", "LoggerMiddleware"]
5
+
@@ -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,142 @@
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") is not None
74
+ )
75
+
76
+ # Build common CORS headers
77
+ def build_cors_headers() -> list[Tuple[bytes, bytes]]:
78
+ headers_out: list[Tuple[bytes, bytes]] = []
79
+ if not self._origin_allowed(origin):
80
+ return headers_out
81
+
82
+ # Access-Control-Allow-Origin
83
+ if "*" in self.allow_origins and not self.allow_credentials:
84
+ self._append_header(headers_out, "access-control-allow-origin", "*")
85
+ else:
86
+ self._append_header(headers_out, "access-control-allow-origin", origin)
87
+ # Necessary for proxies/caches when echoing the origin
88
+ self._append_header(headers_out, "vary", "Origin")
89
+
90
+ # Credentials
91
+ if self.allow_credentials:
92
+ self._append_header(headers_out, "access-control-allow-credentials", "true")
93
+
94
+ return headers_out
95
+
96
+ # Handle preflight
97
+ if is_preflight:
98
+ resp_headers = build_cors_headers()
99
+ if resp_headers:
100
+ # Methods
101
+ allow_methods = ", ".join(self.allow_methods) if "*" not in self.allow_methods else "*"
102
+ self._append_header(resp_headers, "access-control-allow-methods", allow_methods)
103
+
104
+ # Requested headers or wildcard
105
+ req_acrh = self._get_header(req_headers, "access-control-request-headers")
106
+ if "*" in self.allow_headers:
107
+ allow_headers = req_acrh or "*"
108
+ else:
109
+ allow_headers = ", ".join(self.allow_headers)
110
+ if allow_headers:
111
+ self._append_header(resp_headers, "access-control-allow-headers", allow_headers)
112
+
113
+ # Max-Age
114
+ if self.max_age:
115
+ self._append_header(resp_headers, "access-control-max-age", str(self.max_age))
116
+
117
+ # Respond directly
118
+ await send({
119
+ "type": "http.response.start",
120
+ "status": 200,
121
+ "headers": resp_headers,
122
+ })
123
+ await send({"type": "http.response.body", "body": b""})
124
+ return
125
+ # If origin not allowed, continue the chain (app will respond accordingly)
126
+
127
+ # Normal requests: inject CORS headers into the response
128
+ cors_headers = build_cors_headers()
129
+
130
+ # Expose-Headers
131
+ if cors_headers and self.expose_headers:
132
+ expose = ", ".join(self.expose_headers)
133
+ self._append_header(cors_headers, "access-control-expose-headers", expose)
134
+
135
+ async def send_wrapper(message):
136
+ if message.get("type") == "http.response.start" and cors_headers:
137
+ headers = list(message.get("headers", []) or [])
138
+ headers.extend(cors_headers)
139
+ message["headers"] = headers
140
+ return await send(message)
141
+
142
+ return await self.app(scope, receive, send_wrapper)
@@ -0,0 +1,119 @@
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("utf-8", "replace")
83
+ except Exception:
84
+ request_body_preview = "<non-text body>"
85
+ else:
86
+ receive_to_use = receive
87
+
88
+ def _normalized_headers(headers: Sequence[Tuple[bytes, bytes]]):
89
+ out = []
90
+ for k, v in headers:
91
+ name = k.decode().lower()
92
+ if name in self.redact_headers:
93
+ out.append((name, "<redacted>"))
94
+ else:
95
+ out.append((name, v.decode(errors="replace")))
96
+ return out
97
+
98
+ self.logger.log(self.level, f"--> {method} {path}")
99
+ if self.include_headers:
100
+ req_headers = _normalized_headers(scope.get("headers", []) or [])
101
+ self.logger.log(self.level, f" req headers: {req_headers}")
102
+ if request_body_preview is not None:
103
+ self.logger.log(self.level, f" req body: {request_body_preview}")
104
+
105
+ async def send_wrapper(message):
106
+ if message.get("type") == "http.response.start":
107
+ status_code_holder["status"] = message.get("status", 0)
108
+ response_headers_holder[:] = list(message.get("headers", []) or [])
109
+ return await send(message)
110
+
111
+ try:
112
+ await self.app(scope, receive_to_use, send_wrapper)
113
+ finally:
114
+ duration = time.time() - start
115
+ status = status_code_holder["status"] or 0
116
+ self.logger.log(self.level, f"<-- {method} {path} {status} ({duration:.4f}s)")
117
+ if self.include_headers and response_headers_holder:
118
+ res_headers = _normalized_headers(response_headers_holder)
119
+ 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