tachyon-api 0.5.5__py3-none-any.whl → 0.5.7__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.
- tachyon_api/__init__.py +20 -0
- tachyon_api/app.py +291 -45
- tachyon_api/cache.py +261 -0
- tachyon_api/openapi.py +106 -22
- tachyon_api/responses.py +27 -3
- tachyon_api-0.5.7.dist-info/METADATA +145 -0
- {tachyon_api-0.5.5.dist-info → tachyon_api-0.5.7.dist-info}/RECORD +9 -8
- tachyon_api-0.5.5.dist-info/METADATA +0 -239
- {tachyon_api-0.5.5.dist-info → tachyon_api-0.5.7.dist-info}/LICENSE +0 -0
- {tachyon_api-0.5.5.dist-info → tachyon_api-0.5.7.dist-info}/WHEEL +0 -0
tachyon_api/cache.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache utilities: decorator with TTL and pluggable backends.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- BaseCacheBackend protocol and an in-memory implementation
|
|
6
|
+
- CacheConfig dataclass and helpers to set global/app config
|
|
7
|
+
- cache decorator usable on any sync/async function (including routes)
|
|
8
|
+
|
|
9
|
+
Design notes:
|
|
10
|
+
- Key builder defaults to a stable representation of function + args/kwargs
|
|
11
|
+
- Unless predicate allows opt-out per-call
|
|
12
|
+
- TTL can be provided per-decorator or falls back to global default
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import asyncio
|
|
18
|
+
import hashlib
|
|
19
|
+
import inspect
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from functools import wraps
|
|
22
|
+
from typing import Any, Callable, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseCacheBackend:
|
|
26
|
+
"""Minimal cache backend interface."""
|
|
27
|
+
|
|
28
|
+
def get(self, key: str) -> Any: # pragma: no cover - interface
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None: # pragma: no cover - interface
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
def delete(self, key: str) -> None: # pragma: no cover - interface
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
def clear(self) -> None: # pragma: no cover - interface
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InMemoryCacheBackend(BaseCacheBackend):
|
|
42
|
+
"""Simple in-memory cache with TTL using wall-clock time."""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._store: dict[str, Tuple[float | None, Any]] = {}
|
|
46
|
+
|
|
47
|
+
def _is_expired(self, expires_at: float | None) -> bool:
|
|
48
|
+
return expires_at is not None and time.time() >= expires_at
|
|
49
|
+
|
|
50
|
+
def get(self, key: str) -> Any:
|
|
51
|
+
item = self._store.get(key)
|
|
52
|
+
if not item:
|
|
53
|
+
return None
|
|
54
|
+
expires_at, value = item
|
|
55
|
+
if self._is_expired(expires_at):
|
|
56
|
+
# Lazy expiration
|
|
57
|
+
try:
|
|
58
|
+
del self._store[key]
|
|
59
|
+
except KeyError:
|
|
60
|
+
pass
|
|
61
|
+
return None
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
|
65
|
+
expires_at = time.time() + ttl if ttl and ttl > 0 else None
|
|
66
|
+
self._store[key] = (expires_at, value)
|
|
67
|
+
|
|
68
|
+
def delete(self, key: str) -> None:
|
|
69
|
+
self._store.pop(key, None)
|
|
70
|
+
|
|
71
|
+
def clear(self) -> None:
|
|
72
|
+
self._store.clear()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class CacheConfig:
|
|
77
|
+
backend: BaseCacheBackend
|
|
78
|
+
default_ttl: float = 60.0
|
|
79
|
+
key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None
|
|
80
|
+
enabled: bool = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_cache_config: Optional[CacheConfig] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _default_key_builder(func: Callable, args: tuple, kwargs: dict) -> str:
|
|
87
|
+
parts = [getattr(func, "__module__", ""), getattr(func, "__qualname__", ""), "|"]
|
|
88
|
+
# Stable kwargs order
|
|
89
|
+
items = [repr(a) for a in args] + [f"{k}={repr(v)}" for k, v in sorted(kwargs.items())]
|
|
90
|
+
raw_key = ":".join(parts + items)
|
|
91
|
+
return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_cache_config(
|
|
95
|
+
backend: Optional[BaseCacheBackend] = None,
|
|
96
|
+
default_ttl: float = 60.0,
|
|
97
|
+
key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None,
|
|
98
|
+
enabled: bool = True,
|
|
99
|
+
) -> CacheConfig:
|
|
100
|
+
"""Create and set the global cache configuration.
|
|
101
|
+
|
|
102
|
+
Returns the created CacheConfig and sets it as the active global config.
|
|
103
|
+
"""
|
|
104
|
+
global _cache_config
|
|
105
|
+
cfg = CacheConfig(
|
|
106
|
+
backend=backend or InMemoryCacheBackend(),
|
|
107
|
+
default_ttl=default_ttl,
|
|
108
|
+
key_builder=key_builder,
|
|
109
|
+
enabled=enabled,
|
|
110
|
+
)
|
|
111
|
+
_cache_config = cfg
|
|
112
|
+
return cfg
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def set_cache_config(config: CacheConfig) -> None:
|
|
116
|
+
"""Set the global cache configuration object."""
|
|
117
|
+
global _cache_config
|
|
118
|
+
_cache_config = config
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_cache_config() -> CacheConfig:
|
|
122
|
+
"""Get the current cache configuration, creating a default one if missing."""
|
|
123
|
+
global _cache_config
|
|
124
|
+
if _cache_config is None:
|
|
125
|
+
_cache_config = create_cache_config()
|
|
126
|
+
return _cache_config
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cache(
|
|
130
|
+
TTL: Optional[float] = None,
|
|
131
|
+
*,
|
|
132
|
+
key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None,
|
|
133
|
+
unless: Optional[Callable[[tuple, dict], bool]] = None,
|
|
134
|
+
backend: Optional[BaseCacheBackend] = None,
|
|
135
|
+
):
|
|
136
|
+
"""Cache decorator with TTL and pluggable backend.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
TTL: Time-to-live in seconds for this decorator instance. Falls back to config.default_ttl.
|
|
140
|
+
key_builder: Optional custom function to build cache keys.
|
|
141
|
+
unless: Predicate receiving (args, kwargs). If returns True, skip cache for that call.
|
|
142
|
+
backend: Optional backend override for this decorator.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def decorator(func: Callable):
|
|
146
|
+
cfg = get_cache_config()
|
|
147
|
+
be = backend or cfg.backend
|
|
148
|
+
ttl_value = cfg.default_ttl if TTL is None else TTL
|
|
149
|
+
kb = key_builder or cfg.key_builder or (lambda f, a, kw: _default_key_builder(f, a, kw))
|
|
150
|
+
|
|
151
|
+
if asyncio.iscoroutinefunction(func):
|
|
152
|
+
@wraps(func)
|
|
153
|
+
async def async_wrapper(*args, **kwargs):
|
|
154
|
+
if not cfg.enabled or (unless and unless(args, kwargs)):
|
|
155
|
+
return await func(*args, **kwargs)
|
|
156
|
+
key = kb(func, args, kwargs)
|
|
157
|
+
cached = be.get(key)
|
|
158
|
+
if cached is not None:
|
|
159
|
+
return cached
|
|
160
|
+
result = await func(*args, **kwargs)
|
|
161
|
+
try:
|
|
162
|
+
be.set(key, result, ttl_value)
|
|
163
|
+
except Exception:
|
|
164
|
+
# Backend errors should not break the app
|
|
165
|
+
pass
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
return async_wrapper
|
|
169
|
+
else:
|
|
170
|
+
@wraps(func)
|
|
171
|
+
def wrapper(*args, **kwargs):
|
|
172
|
+
if not cfg.enabled or (unless and unless(args, kwargs)):
|
|
173
|
+
return func(*args, **kwargs)
|
|
174
|
+
key = kb(func, args, kwargs)
|
|
175
|
+
cached = be.get(key)
|
|
176
|
+
if cached is not None:
|
|
177
|
+
return cached
|
|
178
|
+
result = func(*args, **kwargs)
|
|
179
|
+
try:
|
|
180
|
+
be.set(key, result, ttl_value)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
return wrapper
|
|
186
|
+
|
|
187
|
+
return decorator
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class RedisCacheBackend(BaseCacheBackend):
|
|
191
|
+
"""Adapter backend for Redis-like clients.
|
|
192
|
+
|
|
193
|
+
Expects a client with .get(key) -> bytes|str|None and .set(key, value, ex=ttl_seconds).
|
|
194
|
+
The stored values should be JSON-serializable or pickled externally by the user.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, client) -> None:
|
|
198
|
+
self.client = client
|
|
199
|
+
|
|
200
|
+
def get(self, key: str) -> Any:
|
|
201
|
+
value = self.client.get(key)
|
|
202
|
+
# Many clients return bytes; decode if possible
|
|
203
|
+
if isinstance(value, bytes):
|
|
204
|
+
try:
|
|
205
|
+
return value.decode("utf-8")
|
|
206
|
+
except Exception:
|
|
207
|
+
return value
|
|
208
|
+
return value
|
|
209
|
+
|
|
210
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
|
211
|
+
# Use ex (expire seconds) if available
|
|
212
|
+
kwargs = {}
|
|
213
|
+
if ttl and ttl > 0:
|
|
214
|
+
kwargs["ex"] = int(ttl)
|
|
215
|
+
# Best effort set
|
|
216
|
+
self.client.set(key, value, **kwargs)
|
|
217
|
+
|
|
218
|
+
def delete(self, key: str) -> None:
|
|
219
|
+
try:
|
|
220
|
+
self.client.delete(key)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
def clear(self) -> None:
|
|
225
|
+
# Not standardized; no-op by default
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class MemcachedCacheBackend(BaseCacheBackend):
|
|
230
|
+
"""Adapter backend for Memcached-like clients.
|
|
231
|
+
|
|
232
|
+
Expects a client with .get(key) and .set(key, value, expire=ttl_seconds).
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def __init__(self, client) -> None:
|
|
236
|
+
self.client = client
|
|
237
|
+
|
|
238
|
+
def get(self, key: str) -> Any:
|
|
239
|
+
return self.client.get(key)
|
|
240
|
+
|
|
241
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
|
242
|
+
expire = int(ttl) if ttl and ttl > 0 else 0
|
|
243
|
+
try:
|
|
244
|
+
# pymemcache: set(key, value, expire=...)
|
|
245
|
+
self.client.set(key, value, expire=expire)
|
|
246
|
+
except TypeError:
|
|
247
|
+
# python-binary-memcached: set(key, value, time=...)
|
|
248
|
+
self.client.set(key, value, time=expire)
|
|
249
|
+
|
|
250
|
+
def delete(self, key: str) -> None:
|
|
251
|
+
try:
|
|
252
|
+
self.client.delete(key)
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
def clear(self) -> None:
|
|
257
|
+
try:
|
|
258
|
+
self.client.flush_all()
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
|
tachyon_api/openapi.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
from typing import Dict, Any, Optional, List, Type
|
|
1
|
+
from typing import Dict, Any, Optional, List, Type, get_origin, get_args
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
+
import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
import typing
|
|
3
6
|
|
|
4
7
|
from .models import Struct
|
|
5
8
|
|
|
@@ -7,30 +10,112 @@ from .models import Struct
|
|
|
7
10
|
TYPE_MAP = {int: "integer", str: "string", bool: "boolean", float: "number"}
|
|
8
11
|
|
|
9
12
|
|
|
10
|
-
def
|
|
13
|
+
def _schema_for_python_type(
|
|
14
|
+
py_type: Type,
|
|
15
|
+
components: Dict[str, Dict[str, Any]],
|
|
16
|
+
visited: set[Type],
|
|
17
|
+
) -> Dict[str, Any]:
|
|
18
|
+
"""Return OpenAPI schema for a Python type, adding components for Structs if needed."""
|
|
19
|
+
origin = get_origin(py_type)
|
|
20
|
+
args = get_args(py_type)
|
|
21
|
+
|
|
22
|
+
# Optional[T] (Union[T, None])
|
|
23
|
+
if origin is typing.Union and args:
|
|
24
|
+
non_none = [a for a in args if a is not type(None)] # noqa: E721
|
|
25
|
+
if len(non_none) == 1:
|
|
26
|
+
inner = non_none[0]
|
|
27
|
+
schema = _schema_for_python_type(inner, components, visited)
|
|
28
|
+
schema["nullable"] = True
|
|
29
|
+
return schema
|
|
30
|
+
|
|
31
|
+
# List[T]
|
|
32
|
+
if origin in (list, List):
|
|
33
|
+
item = args[0] if args else str
|
|
34
|
+
item_schema = _schema_for_python_type(item, components, visited)
|
|
35
|
+
return {"type": "array", "items": item_schema}
|
|
36
|
+
|
|
37
|
+
# Struct subclass
|
|
38
|
+
if isinstance(py_type, type) and issubclass(py_type, Struct):
|
|
39
|
+
name = py_type.__name__
|
|
40
|
+
if py_type not in visited:
|
|
41
|
+
visited.add(py_type)
|
|
42
|
+
components[name] = _generate_struct_schema(py_type, components, visited)
|
|
43
|
+
return {"$ref": f"#/components/schemas/{name}"}
|
|
44
|
+
|
|
45
|
+
# Special formats
|
|
46
|
+
if py_type is uuid.UUID:
|
|
47
|
+
return {"type": "string", "format": "uuid"}
|
|
48
|
+
if py_type is datetime.datetime:
|
|
49
|
+
return {"type": "string", "format": "date-time"}
|
|
50
|
+
if py_type is datetime.date:
|
|
51
|
+
return {"type": "string", "format": "date"}
|
|
52
|
+
|
|
53
|
+
# Scalars
|
|
54
|
+
return {"type": TYPE_MAP.get(py_type, "string")}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _unwrap_optional(py_type: Type) -> tuple[Type, bool]:
|
|
58
|
+
origin = get_origin(py_type)
|
|
59
|
+
args = get_args(py_type)
|
|
60
|
+
if origin is typing.Union and args:
|
|
61
|
+
non_none = [a for a in args if a is not type(None)] # noqa: E721
|
|
62
|
+
if len(non_none) == 1:
|
|
63
|
+
return non_none[0], True
|
|
64
|
+
return py_type, False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _generate_struct_schema(
|
|
68
|
+
struct_class: Type[Struct],
|
|
69
|
+
components: Dict[str, Dict[str, Any]],
|
|
70
|
+
visited: set[Type],
|
|
71
|
+
) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Generate a JSON Schema dictionary for a msgspec Struct, populating components for nested Structs.
|
|
11
74
|
"""
|
|
12
|
-
|
|
75
|
+
properties: Dict[str, Any] = {}
|
|
76
|
+
required: List[str] = []
|
|
13
77
|
|
|
14
|
-
|
|
15
|
-
|
|
78
|
+
annotations = getattr(struct_class, "__annotations__", {})
|
|
79
|
+
for field_name in getattr(struct_class, "__struct_fields__", annotations.keys()):
|
|
80
|
+
field_type = annotations.get(field_name, str)
|
|
81
|
+
base_type, is_opt = _unwrap_optional(field_type)
|
|
16
82
|
|
|
17
|
-
|
|
18
|
-
|
|
83
|
+
# Build property schema
|
|
84
|
+
prop_schema = _schema_for_python_type(base_type, components, visited)
|
|
85
|
+
if is_opt:
|
|
86
|
+
prop_schema["nullable"] = True
|
|
87
|
+
|
|
88
|
+
properties[field_name] = prop_schema
|
|
89
|
+
|
|
90
|
+
# Determine required: mark non-Optional fields as required
|
|
91
|
+
if not is_opt:
|
|
92
|
+
required.append(field_name)
|
|
93
|
+
|
|
94
|
+
schema: Dict[str, Any] = {"type": "object", "properties": properties}
|
|
95
|
+
if required:
|
|
96
|
+
schema["required"] = required
|
|
97
|
+
return schema
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_components_for_struct(struct_class: Type[Struct]) -> Dict[str, Dict[str, Any]]:
|
|
19
101
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# For now, assume all fields are required (can be enhanced later)
|
|
31
|
-
required.append(field_name)
|
|
102
|
+
Build components schemas for the given Struct and all nested Structs.
|
|
103
|
+
|
|
104
|
+
Returns a dict mapping component name to schema, including the top-level struct.
|
|
105
|
+
"""
|
|
106
|
+
components: Dict[str, Dict[str, Any]] = {}
|
|
107
|
+
visited: set[Type] = set()
|
|
108
|
+
name = struct_class.__name__
|
|
109
|
+
components[name] = _generate_struct_schema(struct_class, components, visited)
|
|
110
|
+
return components
|
|
111
|
+
|
|
32
112
|
|
|
33
|
-
|
|
113
|
+
def _generate_schema_for_struct(struct_class: Type[Struct]) -> Dict[str, Any]:
|
|
114
|
+
"""
|
|
115
|
+
Backward-compatible API: generate only the schema for the provided Struct (no nested components registration).
|
|
116
|
+
"""
|
|
117
|
+
comps = build_components_for_struct(struct_class)
|
|
118
|
+
return comps[struct_class.__name__]
|
|
34
119
|
|
|
35
120
|
|
|
36
121
|
@dataclass
|
|
@@ -202,7 +287,7 @@ class OpenAPIGenerator:
|
|
|
202
287
|
SwaggerUIBundle.presets.standalone
|
|
203
288
|
],
|
|
204
289
|
layout: "BaseLayout",
|
|
205
|
-
...{
|
|
290
|
+
...{{}}
|
|
206
291
|
}})
|
|
207
292
|
</script>
|
|
208
293
|
</body>
|
|
@@ -268,7 +353,6 @@ class OpenAPIGenerator:
|
|
|
268
353
|
"""
|
|
269
354
|
if self._openapi_schema is None:
|
|
270
355
|
self._openapi_schema = self.config.to_openapi_dict()
|
|
271
|
-
|
|
272
356
|
if path not in self._openapi_schema["paths"]:
|
|
273
357
|
self._openapi_schema["paths"][path] = {}
|
|
274
358
|
|
tachyon_api/responses.py
CHANGED
|
@@ -6,12 +6,24 @@ with Starlette responses.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from starlette.responses import JSONResponse, HTMLResponse # noqa
|
|
9
|
+
import orjson
|
|
10
|
+
from .models import encode_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TachyonJSONResponse(JSONResponse):
|
|
14
|
+
"""High-performance JSON response using orjson for serialization."""
|
|
15
|
+
|
|
16
|
+
media_type = "application/json"
|
|
17
|
+
|
|
18
|
+
def render(self, content) -> bytes: # type: ignore[override]
|
|
19
|
+
# Use centralized encoder to support Struct, UUID, date, datetime, etc.
|
|
20
|
+
return encode_json(content)
|
|
9
21
|
|
|
10
22
|
|
|
11
23
|
# Simple helper functions for common response patterns
|
|
12
24
|
def success_response(data=None, message="Success", status_code=200):
|
|
13
25
|
"""Create a success response with consistent structure"""
|
|
14
|
-
return
|
|
26
|
+
return TachyonJSONResponse(
|
|
15
27
|
{"success": True, "message": message, "data": data}, status_code=status_code
|
|
16
28
|
)
|
|
17
29
|
|
|
@@ -22,7 +34,7 @@ def error_response(error, status_code=400, code=None):
|
|
|
22
34
|
if code:
|
|
23
35
|
response_data["code"] = code
|
|
24
36
|
|
|
25
|
-
return
|
|
37
|
+
return TachyonJSONResponse(response_data, status_code=status_code)
|
|
26
38
|
|
|
27
39
|
|
|
28
40
|
def not_found_response(error="Resource not found"):
|
|
@@ -41,7 +53,19 @@ def validation_error_response(error="Validation failed", errors=None):
|
|
|
41
53
|
if errors:
|
|
42
54
|
response_data["errors"] = errors
|
|
43
55
|
|
|
44
|
-
return
|
|
56
|
+
return TachyonJSONResponse(response_data, status_code=422)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def response_validation_error_response(error="Response validation error"):
|
|
60
|
+
"""Create a 500 response validation error response"""
|
|
61
|
+
# Normalize message with prefix and include 'detail' for backward compatibility
|
|
62
|
+
msg = str(error)
|
|
63
|
+
if not msg.lower().startswith("response validation error"):
|
|
64
|
+
msg = f"Response validation error: {msg}"
|
|
65
|
+
return TachyonJSONResponse(
|
|
66
|
+
{"success": False, "error": msg, "detail": msg, "code": "RESPONSE_VALIDATION_ERROR"},
|
|
67
|
+
status_code=500,
|
|
68
|
+
)
|
|
45
69
|
|
|
46
70
|
|
|
47
71
|
# Re-export Starlette responses for convenience
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tachyon-api
|
|
3
|
+
Version: 0.5.7
|
|
4
|
+
Summary: A lightweight, FastAPI-inspired web framework
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Author: Juan Manuel Panozzo Zénere
|
|
7
|
+
Author-email: jm.panozzozenere@gmail.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: msgspec (>=0.19.0,<0.20.0)
|
|
16
|
+
Requires-Dist: orjson (>=3.11.1,<4.0.0)
|
|
17
|
+
Requires-Dist: ruff (>=0.12.7,<0.13.0)
|
|
18
|
+
Requires-Dist: starlette (>=0.47.2,<0.48.0)
|
|
19
|
+
Requires-Dist: typer (>=0.16.0,<0.17.0)
|
|
20
|
+
Requires-Dist: uvicorn (>=0.35.0,<0.36.0)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# 🚀 Tachyon API
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
**A lightweight, high-performance API framework for Python with the elegance of FastAPI and the speed of light.**
|
|
31
|
+
|
|
32
|
+
Tachyon API combines the intuitive decorator-based syntax you love with minimal dependencies and maximal performance. Built with Test-Driven Development from the ground up, it offers a cleaner, faster alternative with full ASGI compatibility.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from tachyon_api import Tachyon
|
|
36
|
+
from tachyon_api.models import Struct
|
|
37
|
+
|
|
38
|
+
app = Tachyon()
|
|
39
|
+
|
|
40
|
+
class User(Struct):
|
|
41
|
+
name: str
|
|
42
|
+
age: int
|
|
43
|
+
|
|
44
|
+
@app.get("/")
|
|
45
|
+
def hello_world():
|
|
46
|
+
return {"message": "Tachyon is running at lightspeed!"}
|
|
47
|
+
|
|
48
|
+
@app.post("/users")
|
|
49
|
+
def create_user(user: User):
|
|
50
|
+
return {"created": user.name}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## ✨ Features
|
|
54
|
+
|
|
55
|
+
- 🔍 Intuitive API (decorators) and minimal core
|
|
56
|
+
- 🧩 Implicit & explicit DI
|
|
57
|
+
- 📚 OpenAPI with Scalar, Swagger, ReDoc
|
|
58
|
+
- 🛠️ Router system
|
|
59
|
+
- 🔄 Middlewares (class + decorator)
|
|
60
|
+
- 🧠 Cache decorator with TTL (in-memory, Redis, Memcached)
|
|
61
|
+
- 🚀 High-performance JSON (msgspec + orjson)
|
|
62
|
+
- 🧾 Unified error format (422/500)
|
|
63
|
+
- 🧰 Default JSON response (TachyonJSONResponse)
|
|
64
|
+
- 🔒 End-to-end safety: request Body validation + typed response_model
|
|
65
|
+
- 📘 Deep OpenAPI schemas: nested Structs, Optional/List (nullable/array), formats (uuid, date-time)
|
|
66
|
+
|
|
67
|
+
## 🧪 Test-Driven Development
|
|
68
|
+
|
|
69
|
+
Tachyon API is built with TDD principles at its core. The test suite covers routing, DI, params, body validation, responses, OpenAPI generation, caching, and example flows.
|
|
70
|
+
|
|
71
|
+
## 🔌 Core Dependencies
|
|
72
|
+
|
|
73
|
+
- Starlette (ASGI)
|
|
74
|
+
- msgspec (validation/serialization)
|
|
75
|
+
- orjson (fast JSON)
|
|
76
|
+
- uvicorn (server)
|
|
77
|
+
|
|
78
|
+
## 💉 Dependency Injection System
|
|
79
|
+
|
|
80
|
+
- Implicit injection: annotate with registered types
|
|
81
|
+
- Explicit injection: Depends() for clarity and control
|
|
82
|
+
|
|
83
|
+
## 🔄 Middleware Support
|
|
84
|
+
|
|
85
|
+
- Built-in: CORSMiddleware, LoggerMiddleware
|
|
86
|
+
- Use app.add_middleware(...) or @app.middleware()
|
|
87
|
+
|
|
88
|
+
## ⚡ Cache with TTL
|
|
89
|
+
|
|
90
|
+
- @cache(TTL=...) on routes and functions
|
|
91
|
+
- Per-app config and pluggable backends (InMemory, Redis, Memcached)
|
|
92
|
+
|
|
93
|
+
## 📚 Example Application
|
|
94
|
+
|
|
95
|
+
The example demonstrates clean architecture, routers, middlewares, caching, and now end-to-end safety with OpenAPI:
|
|
96
|
+
|
|
97
|
+
- /orjson-demo: default JSON powered by orjson
|
|
98
|
+
- /api/v1/users/e2e: Body + response_model, unified errors and deep OpenAPI schemas
|
|
99
|
+
|
|
100
|
+
Run the example:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
cd example
|
|
104
|
+
python app.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Docs at /docs (Scalar), /swagger, /redoc.
|
|
108
|
+
|
|
109
|
+
## ✅ Response models, OpenAPI params, and deep schemas
|
|
110
|
+
|
|
111
|
+
- Response models: set response_model=YourStruct to validate/convert outputs via msgspec before serializing.
|
|
112
|
+
- Parameter schemas: Optional[T] → nullable: true; List[T] → type: array with items.
|
|
113
|
+
- Deep schemas: nested Struct components, Optional/List items, and formats (uuid, date-time) are generated and referenced in components.
|
|
114
|
+
|
|
115
|
+
## 🧾 Default JSON response and unified error format
|
|
116
|
+
|
|
117
|
+
- Default response: TachyonJSONResponse serializes complex types (UUID/date/datetime, Struct) via orjson and centralized encoders.
|
|
118
|
+
- 422 Validation: { success: false, error, code: VALIDATION_ERROR, [errors] }.
|
|
119
|
+
- 500 Response model: { success: false, error: "Response validation error: ...", detail, code: RESPONSE_VALIDATION_ERROR }.
|
|
120
|
+
|
|
121
|
+
## 📝 Contributing
|
|
122
|
+
|
|
123
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
124
|
+
|
|
125
|
+
1. Fork the repository
|
|
126
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
127
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
128
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
129
|
+
5. Open a Pull Request
|
|
130
|
+
|
|
131
|
+
## 📜 License
|
|
132
|
+
|
|
133
|
+
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
|
|
134
|
+
|
|
135
|
+
## 🔮 Roadmap
|
|
136
|
+
|
|
137
|
+
- Exception system and global handlers
|
|
138
|
+
- CLI, scaffolding, and code quality tooling
|
|
139
|
+
- Authentication middleware and benchmarks
|
|
140
|
+
- More examples and deployment guides
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
*Built with 💜 by developers, for developers*
|
|
145
|
+
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
tachyon_api/__init__.py,sha256=
|
|
2
|
-
tachyon_api/app.py,sha256=
|
|
1
|
+
tachyon_api/__init__.py,sha256=CEg69nSVW9iCLYgqVNvSr_7EICb9nrLuaDVxKlpm430,1207
|
|
2
|
+
tachyon_api/app.py,sha256=R4EHr85hc_1k76YasseqZrus7b9by5rGij3IAh_UACg,36160
|
|
3
|
+
tachyon_api/cache.py,sha256=FAlWcVbXf47_OWVtP92D2_J7ytwLD2pUV3ROy95u0Uc,8249
|
|
3
4
|
tachyon_api/di.py,sha256=FyFghfUjU0OaE_lW2BqItgsZWXFbLvwFsvVywqFjl6c,1505
|
|
4
5
|
tachyon_api/middlewares/__init__.py,sha256=BR1kqTj2IKdwhvVZCVn9p_rOFS0aOYoaC0bNX0C_0q4,121
|
|
5
6
|
tachyon_api/middlewares/core.py,sha256=-dJTrI6KJbQzatopCUC5lau_JIZpl063697qdE4W0SY,1440
|
|
6
7
|
tachyon_api/middlewares/cors.py,sha256=jZMqfFSz-14dwNIOOMqKaQm8Dypnf0V_WVW_4Uuc4AE,5716
|
|
7
8
|
tachyon_api/middlewares/logger.py,sha256=8W543kmfu7f5LpDQOox8Z4t3QT1knko7bUx7kWLCzWw,4832
|
|
8
9
|
tachyon_api/models.py,sha256=9pA5_ddMAcWW2dErAgeyG926NFezfCU68uuhW0qGMs4,2224
|
|
9
|
-
tachyon_api/openapi.py,sha256=
|
|
10
|
+
tachyon_api/openapi.py,sha256=7gob0J9uQ6-fjEFHbS-KT0oH7pOko18TEW_dcT5FEkI,14623
|
|
10
11
|
tachyon_api/params.py,sha256=gLGIsFEGr33_G0kakpqOFHRrf86w3egFJAjquZp3Lo0,2810
|
|
11
|
-
tachyon_api/responses.py,sha256=
|
|
12
|
+
tachyon_api/responses.py,sha256=f-oe4EFQb-JkYEhWwE63qf9V8GLZ0PxouwsHQQnTj7M,2488
|
|
12
13
|
tachyon_api/router.py,sha256=TlzgJ-UAfxBAQKy4YNVQIBObIDNzlebQ4G9qxsswJQ0,4204
|
|
13
|
-
tachyon_api-0.5.
|
|
14
|
-
tachyon_api-0.5.
|
|
15
|
-
tachyon_api-0.5.
|
|
16
|
-
tachyon_api-0.5.
|
|
14
|
+
tachyon_api-0.5.7.dist-info/LICENSE,sha256=UZXUTSuWBt8V377-2_YE4ug9t6rcBAKrfOVhIBQ8zhk,712
|
|
15
|
+
tachyon_api-0.5.7.dist-info/METADATA,sha256=aIG9RflcTQ1deMOaBWfFt3s4cyUqVQ2MQfB9mUuXNoU,5042
|
|
16
|
+
tachyon_api-0.5.7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
tachyon_api-0.5.7.dist-info/RECORD,,
|