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,72 @@
1
+ """
2
+ Tachyon Background Tasks Module
3
+
4
+ Provides background task functionality for running tasks after response is sent.
5
+ """
6
+
7
+ import asyncio
8
+ from typing import Any, Callable, List, Tuple
9
+
10
+
11
+ class BackgroundTasks:
12
+ """
13
+ Background tasks that run after the response has been sent.
14
+
15
+ Tasks are executed in order after the response is complete.
16
+ Errors in tasks are caught and logged but don't affect the response.
17
+
18
+ Example:
19
+ @app.get("/send-notification")
20
+ def send_notification(background_tasks: BackgroundTasks):
21
+ background_tasks.add_task(send_email, "user@example.com", "Hello!")
22
+ return {"message": "Notification scheduled"}
23
+ """
24
+
25
+ def __init__(self):
26
+ self._tasks: List[Tuple[Callable, tuple, dict]] = []
27
+
28
+ def add_task(
29
+ self,
30
+ func: Callable[..., Any],
31
+ *args: Any,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ """
35
+ Add a task to be run in the background.
36
+
37
+ Args:
38
+ func: The function to call. Can be sync or async.
39
+ *args: Positional arguments to pass to the function.
40
+ **kwargs: Keyword arguments to pass to the function.
41
+
42
+ Example:
43
+ background_tasks.add_task(write_log, "User logged in")
44
+ background_tasks.add_task(send_email, to="user@example.com", subject="Hi")
45
+ """
46
+ self._tasks.append((func, args, kwargs))
47
+
48
+ async def run_tasks(self) -> None:
49
+ """
50
+ Execute all queued background tasks.
51
+
52
+ This method is called automatically after the response is sent.
53
+ Each task is run in order, and errors are caught to prevent
54
+ one failing task from stopping the others.
55
+ """
56
+ for func, args, kwargs in self._tasks:
57
+ try:
58
+ result = func(*args, **kwargs)
59
+ if asyncio.iscoroutine(result):
60
+ await result
61
+ except Exception:
62
+ # Log error but continue with other tasks
63
+ # In production, you'd want proper logging here
64
+ pass
65
+
66
+ def __len__(self) -> int:
67
+ """Return the number of pending tasks."""
68
+ return len(self._tasks)
69
+
70
+ def __bool__(self) -> bool:
71
+ """BackgroundTasks is always truthy (to allow `if background_tasks:`)."""
72
+ return True
tachyon_api/cache.py ADDED
@@ -0,0 +1,270 @@
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
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ import asyncio
19
+ import hashlib
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(
32
+ self, key: str, value: Any, ttl: Optional[float] = None
33
+ ) -> None: # pragma: no cover - interface
34
+ raise NotImplementedError
35
+
36
+ def delete(self, key: str) -> None: # pragma: no cover - interface
37
+ raise NotImplementedError
38
+
39
+ def clear(self) -> None: # pragma: no cover - interface
40
+ raise NotImplementedError
41
+
42
+
43
+ class InMemoryCacheBackend(BaseCacheBackend):
44
+ """Simple in-memory cache with TTL using wall-clock time."""
45
+
46
+ def __init__(self) -> None:
47
+ self._store: dict[str, Tuple[float | None, Any]] = {}
48
+
49
+ def _is_expired(self, expires_at: float | None) -> bool:
50
+ return expires_at is not None and time.time() >= expires_at
51
+
52
+ def get(self, key: str) -> Any:
53
+ item = self._store.get(key)
54
+ if not item:
55
+ return None
56
+ expires_at, value = item
57
+ if self._is_expired(expires_at):
58
+ # Lazy expiration
59
+ try:
60
+ del self._store[key]
61
+ except KeyError:
62
+ pass
63
+ return None
64
+ return value
65
+
66
+ def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
67
+ expires_at = time.time() + ttl if ttl and ttl > 0 else None
68
+ self._store[key] = (expires_at, value)
69
+
70
+ def delete(self, key: str) -> None:
71
+ self._store.pop(key, None)
72
+
73
+ def clear(self) -> None:
74
+ self._store.clear()
75
+
76
+
77
+ @dataclass
78
+ class CacheConfig:
79
+ backend: BaseCacheBackend
80
+ default_ttl: float = 60.0
81
+ key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None
82
+ enabled: bool = True
83
+
84
+
85
+ _cache_config: Optional[CacheConfig] = None
86
+
87
+
88
+ def _default_key_builder(func: Callable, args: tuple, kwargs: dict) -> str:
89
+ parts = [getattr(func, "__module__", ""), getattr(func, "__qualname__", ""), "|"]
90
+ # Stable kwargs order
91
+ items = [repr(a) for a in args] + [
92
+ f"{k}={repr(v)}" for k, v in sorted(kwargs.items())
93
+ ]
94
+ raw_key = ":".join(parts + items)
95
+ return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
96
+
97
+
98
+ def create_cache_config(
99
+ backend: Optional[BaseCacheBackend] = None,
100
+ default_ttl: float = 60.0,
101
+ key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None,
102
+ enabled: bool = True,
103
+ ) -> CacheConfig:
104
+ """Create and set the global cache configuration.
105
+
106
+ Returns the created CacheConfig and sets it as the active global config.
107
+ """
108
+ global _cache_config
109
+ cfg = CacheConfig(
110
+ backend=backend or InMemoryCacheBackend(),
111
+ default_ttl=default_ttl,
112
+ key_builder=key_builder,
113
+ enabled=enabled,
114
+ )
115
+ _cache_config = cfg
116
+ return cfg
117
+
118
+
119
+ def set_cache_config(config: CacheConfig) -> None:
120
+ """Set the global cache configuration object."""
121
+ global _cache_config
122
+ _cache_config = config
123
+
124
+
125
+ def get_cache_config() -> CacheConfig:
126
+ """Get the current cache configuration, creating a default one if missing."""
127
+ global _cache_config
128
+ if _cache_config is None:
129
+ _cache_config = create_cache_config()
130
+ return _cache_config
131
+
132
+
133
+ def cache(
134
+ TTL: Optional[float] = None,
135
+ *,
136
+ key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None,
137
+ unless: Optional[Callable[[tuple, dict], bool]] = None,
138
+ backend: Optional[BaseCacheBackend] = None,
139
+ ):
140
+ """Cache decorator with TTL and pluggable backend.
141
+
142
+ Args:
143
+ TTL: Time-to-live in seconds for this decorator instance. Falls back to config.default_ttl.
144
+ key_builder: Optional custom function to build cache keys.
145
+ unless: Predicate receiving (args, kwargs). If returns True, skip cache for that call.
146
+ backend: Optional backend override for this decorator.
147
+ """
148
+
149
+ def decorator(func: Callable):
150
+ cfg = get_cache_config()
151
+ be = backend or cfg.backend
152
+ ttl_value = cfg.default_ttl if TTL is None else TTL
153
+ kb = (
154
+ key_builder
155
+ or cfg.key_builder
156
+ or (lambda f, a, kw: _default_key_builder(f, a, kw))
157
+ )
158
+
159
+ if asyncio.iscoroutinefunction(func):
160
+
161
+ @wraps(func)
162
+ async def async_wrapper(*args, **kwargs):
163
+ if not cfg.enabled or (unless and unless(args, kwargs)):
164
+ return await func(*args, **kwargs)
165
+ key = kb(func, args, kwargs)
166
+ cached = be.get(key)
167
+ if cached is not None:
168
+ return cached
169
+ result = await func(*args, **kwargs)
170
+ try:
171
+ be.set(key, result, ttl_value)
172
+ except Exception:
173
+ # Backend errors should not break the app
174
+ pass
175
+ return result
176
+
177
+ return async_wrapper
178
+ else:
179
+
180
+ @wraps(func)
181
+ def wrapper(*args, **kwargs):
182
+ if not cfg.enabled or (unless and unless(args, kwargs)):
183
+ return func(*args, **kwargs)
184
+ key = kb(func, args, kwargs)
185
+ cached = be.get(key)
186
+ if cached is not None:
187
+ return cached
188
+ result = func(*args, **kwargs)
189
+ try:
190
+ be.set(key, result, ttl_value)
191
+ except Exception:
192
+ pass
193
+ return result
194
+
195
+ return wrapper
196
+
197
+ return decorator
198
+
199
+
200
+ class RedisCacheBackend(BaseCacheBackend):
201
+ """Adapter backend for Redis-like clients.
202
+
203
+ Expects a client with .get(key) -> bytes|str|None and .set(key, value, ex=ttl_seconds).
204
+ The stored values should be JSON-serializable or pickled externally by the user.
205
+ """
206
+
207
+ def __init__(self, client) -> None:
208
+ self.client = client
209
+
210
+ def get(self, key: str) -> Any:
211
+ value = self.client.get(key)
212
+ # Many clients return bytes; decode if possible
213
+ if isinstance(value, bytes):
214
+ try:
215
+ return value.decode("utf-8")
216
+ except Exception:
217
+ return value
218
+ return value
219
+
220
+ def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
221
+ # Use ex (expire seconds) if available
222
+ kwargs = {}
223
+ if ttl and ttl > 0:
224
+ kwargs["ex"] = int(ttl)
225
+ # Best effort set
226
+ self.client.set(key, value, **kwargs)
227
+
228
+ def delete(self, key: str) -> None:
229
+ try:
230
+ self.client.delete(key)
231
+ except Exception:
232
+ pass
233
+
234
+ def clear(self) -> None:
235
+ # Not standardized; no-op by default
236
+ pass
237
+
238
+
239
+ class MemcachedCacheBackend(BaseCacheBackend):
240
+ """Adapter backend for Memcached-like clients.
241
+
242
+ Expects a client with .get(key) and .set(key, value, expire=ttl_seconds).
243
+ """
244
+
245
+ def __init__(self, client) -> None:
246
+ self.client = client
247
+
248
+ def get(self, key: str) -> Any:
249
+ return self.client.get(key)
250
+
251
+ def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
252
+ expire = int(ttl) if ttl and ttl > 0 else 0
253
+ try:
254
+ # pymemcache: set(key, value, expire=...)
255
+ self.client.set(key, value, expire=expire)
256
+ except TypeError:
257
+ # python-binary-memcached: set(key, value, time=...)
258
+ self.client.set(key, value, time=expire)
259
+
260
+ def delete(self, key: str) -> None:
261
+ try:
262
+ self.client.delete(key)
263
+ except Exception:
264
+ pass
265
+
266
+ def clear(self) -> None:
267
+ try:
268
+ self.client.flush_all()
269
+ except Exception:
270
+ pass
@@ -0,0 +1,9 @@
1
+ """
2
+ Tachyon CLI - Command Line Interface
3
+
4
+ NestJS-style CLI for scaffolding, code generation, and quality tools.
5
+ """
6
+
7
+ from .main import app
8
+
9
+ __all__ = ["app"]
@@ -0,0 +1,8 @@
1
+ """
2
+ Entry point for `python -m tachyon_api.cli`
3
+ """
4
+
5
+ from .main import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,5 @@
1
+ """CLI Commands"""
2
+
3
+ from . import new, generate, openapi, lint
4
+
5
+ __all__ = ["new", "generate", "openapi", "lint"]
@@ -0,0 +1,190 @@
1
+ """
2
+ tachyon generate - Generate components (service, controller, repository, dto)
3
+ """
4
+
5
+ import typer
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from ..templates import ServiceTemplates
10
+
11
+ app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ def _to_class_name(name: str) -> str:
15
+ """Convert snake_case or kebab-case to PascalCase."""
16
+ return "".join(word.capitalize() for word in name.replace("-", "_").split("_"))
17
+
18
+
19
+ def _to_snake_case(name: str) -> str:
20
+ """Ensure name is in snake_case."""
21
+ return name.replace("-", "_").lower()
22
+
23
+
24
+ def _create_file(path: Path, content: str, name: str):
25
+ """Create a file and print status."""
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ path.write_text(content)
28
+ typer.echo(f" šŸ“„ Created {name}")
29
+
30
+
31
+ @app.command()
32
+ def service(
33
+ name: str = typer.Argument(..., help="Service name (e.g., 'auth', 'users')"),
34
+ path: Optional[Path] = typer.Option(
35
+ None, "--path", "-p", help="Base path for modules (default: ./modules)"
36
+ ),
37
+ no_tests: bool = typer.Option(
38
+ False, "--no-tests", help="Skip test file generation"
39
+ ),
40
+ crud: bool = typer.Option(
41
+ False, "--crud", help="Generate with basic CRUD operations"
42
+ ),
43
+ ):
44
+ """
45
+ šŸ”§ Generate a complete service module.
46
+
47
+ Creates: controller, service, repository, dto, and tests.
48
+
49
+ Example:
50
+ tachyon g service auth
51
+ tachyon g service products --crud
52
+ tachyon g service users --path src/modules
53
+ """
54
+ snake_name = _to_snake_case(name)
55
+ class_name = _to_class_name(name)
56
+
57
+ base_path = path or Path.cwd() / "modules"
58
+ service_path = base_path / snake_name
59
+
60
+ if service_path.exists():
61
+ typer.secho(f"āŒ Module '{snake_name}' already exists!", fg=typer.colors.RED)
62
+ raise typer.Exit(1)
63
+
64
+ typer.echo(f"\nšŸ”§ Generating service: {typer.style(snake_name, bold=True)}\n")
65
+
66
+ # Create directory
67
+ service_path.mkdir(parents=True, exist_ok=True)
68
+ tests_path = service_path / "tests"
69
+ tests_path.mkdir(exist_ok=True)
70
+
71
+ # Generate files
72
+ files = {
73
+ "__init__.py": ServiceTemplates.init(snake_name, class_name),
74
+ f"{snake_name}_controller.py": ServiceTemplates.controller(
75
+ snake_name, class_name, crud
76
+ ),
77
+ f"{snake_name}_service.py": ServiceTemplates.service(
78
+ snake_name, class_name, crud
79
+ ),
80
+ f"{snake_name}_repository.py": ServiceTemplates.repository(
81
+ snake_name, class_name, crud
82
+ ),
83
+ f"{snake_name}_dto.py": ServiceTemplates.dto(snake_name, class_name, crud),
84
+ }
85
+
86
+ for filename, content in files.items():
87
+ _create_file(service_path / filename, content, filename)
88
+
89
+ # Generate tests
90
+ if not no_tests:
91
+ _create_file(tests_path / "__init__.py", "", "tests/__init__.py")
92
+ _create_file(
93
+ tests_path / f"test_{snake_name}_service.py",
94
+ ServiceTemplates.test_service(snake_name, class_name),
95
+ f"tests/test_{snake_name}_service.py",
96
+ )
97
+
98
+ typer.echo(
99
+ f"\nāœ… Service {typer.style(snake_name, bold=True, fg=typer.colors.GREEN)} generated!"
100
+ )
101
+ typer.echo("\nšŸ“– Don't forget to register in app.py:")
102
+ typer.echo(f" from modules.{snake_name} import router as {snake_name}_router")
103
+ typer.echo(f" app.include_router({snake_name}_router)")
104
+ typer.echo()
105
+
106
+
107
+ @app.command()
108
+ def controller(
109
+ name: str = typer.Argument(..., help="Controller name"),
110
+ path: Optional[Path] = typer.Option(None, "--path", "-p"),
111
+ ):
112
+ """
113
+ šŸ“” Generate a controller (router) file.
114
+
115
+ Example:
116
+ tachyon g controller users
117
+ """
118
+ snake_name = _to_snake_case(name)
119
+ class_name = _to_class_name(name)
120
+
121
+ base_path = path or Path.cwd() / "modules" / snake_name
122
+ base_path.mkdir(parents=True, exist_ok=True)
123
+
124
+ typer.echo(f"\nšŸ“” Generating controller: {snake_name}\n")
125
+
126
+ _create_file(
127
+ base_path / f"{snake_name}_controller.py",
128
+ ServiceTemplates.controller(snake_name, class_name, False),
129
+ f"{snake_name}_controller.py",
130
+ )
131
+
132
+ typer.echo("\nāœ… Controller generated!")
133
+
134
+
135
+ @app.command("repo")
136
+ @app.command("repository")
137
+ def repository(
138
+ name: str = typer.Argument(..., help="Repository name"),
139
+ path: Optional[Path] = typer.Option(None, "--path", "-p"),
140
+ ):
141
+ """
142
+ šŸ—„ļø Generate a repository file.
143
+
144
+ Example:
145
+ tachyon g repository users
146
+ tachyon g repo products
147
+ """
148
+ snake_name = _to_snake_case(name)
149
+ class_name = _to_class_name(name)
150
+
151
+ base_path = path or Path.cwd() / "modules" / snake_name
152
+ base_path.mkdir(parents=True, exist_ok=True)
153
+
154
+ typer.echo(f"\nšŸ—„ļø Generating repository: {snake_name}\n")
155
+
156
+ _create_file(
157
+ base_path / f"{snake_name}_repository.py",
158
+ ServiceTemplates.repository(snake_name, class_name, False),
159
+ f"{snake_name}_repository.py",
160
+ )
161
+
162
+ typer.echo("\nāœ… Repository generated!")
163
+
164
+
165
+ @app.command()
166
+ def dto(
167
+ name: str = typer.Argument(..., help="DTO name"),
168
+ path: Optional[Path] = typer.Option(None, "--path", "-p"),
169
+ ):
170
+ """
171
+ šŸ“¦ Generate a DTO (Data Transfer Object) file.
172
+
173
+ Example:
174
+ tachyon g dto users
175
+ """
176
+ snake_name = _to_snake_case(name)
177
+ class_name = _to_class_name(name)
178
+
179
+ base_path = path or Path.cwd() / "modules" / snake_name
180
+ base_path.mkdir(parents=True, exist_ok=True)
181
+
182
+ typer.echo(f"\nšŸ“¦ Generating DTO: {snake_name}\n")
183
+
184
+ _create_file(
185
+ base_path / f"{snake_name}_dto.py",
186
+ ServiceTemplates.dto(snake_name, class_name, False),
187
+ f"{snake_name}_dto.py",
188
+ )
189
+
190
+ typer.echo("\nāœ… DTO generated!")