swiftpy-framework 0.1.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 (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import weakref
5
+ from collections.abc import Callable
6
+ from contextvars import ContextVar
7
+ from typing import Any
8
+
9
+ # Use None as default to avoid shared mutable default reference
10
+ _context: ContextVar[dict[str, Any] | None] = ContextVar(
11
+ "swiftpy_context",
12
+ default=None,
13
+ )
14
+
15
+ _WATCHERS_KEY = "__swiftpy_context_watchers__"
16
+
17
+
18
+ def _current_task_or_none() -> asyncio.Task[Any] | None:
19
+ try:
20
+ return asyncio.current_task()
21
+ except RuntimeError:
22
+ return None
23
+
24
+
25
+ class Context:
26
+ """
27
+ Request-local context storage for SwiftPY.
28
+
29
+ Backed by Python's contextvars.ContextVar with copy-on-write task isolation.
30
+ """
31
+
32
+ @staticmethod
33
+ def _get_dict() -> dict[str, Any]:
34
+ """Internal helper to fetch or initialize the request context dict."""
35
+ ctx = _context.get()
36
+ if ctx is None:
37
+ ctx = {}
38
+ _context.set(ctx)
39
+ return ctx
40
+
41
+ @staticmethod
42
+ def _notify_watchers(
43
+ ctx_dict: dict[str, Any] | None,
44
+ key: str | None,
45
+ value: Any,
46
+ ) -> None:
47
+ if not ctx_dict:
48
+ return
49
+ watchers_map = ctx_dict.get(_WATCHERS_KEY)
50
+ if not isinstance(watchers_map, dict):
51
+ return
52
+ cur_task = _current_task_or_none()
53
+ target_keys = list(watchers_map.keys()) if key is None else [key]
54
+ for watch_key in target_keys:
55
+ entries = watchers_map.get(watch_key)
56
+ if not entries:
57
+ continue
58
+ for owner_task, cb_ref in entries:
59
+ if owner_task is not cur_task:
60
+ continue
61
+ cb = cb_ref() if isinstance(cb_ref, weakref.ref) else cb_ref
62
+ if cb is not None:
63
+ cb(value)
64
+
65
+ @staticmethod
66
+ def watch(
67
+ key: str,
68
+ callback: weakref.ref[Callable[[Any], None]] | Callable[[Any], None],
69
+ ) -> None:
70
+ """Register a task-scoped watcher invoked when `key` changes or context clears."""
71
+ current = Context._get_dict()
72
+ raw_watchers = current.get(_WATCHERS_KEY)
73
+ watchers_map: dict[str, tuple[tuple[Any, Any], ...]] = (
74
+ dict(raw_watchers) if isinstance(raw_watchers, dict) else {}
75
+ )
76
+ cur_task = _current_task_or_none()
77
+ existing = watchers_map.get(key, ())
78
+ live_entries = [
79
+ entry
80
+ for entry in existing
81
+ if not (isinstance(entry[1], weakref.ref) and entry[1]() is None)
82
+ ]
83
+ live_entries.append((cur_task, callback))
84
+ watchers_map[key] = tuple(live_entries)
85
+
86
+ new_ctx = current.copy()
87
+ new_ctx[_WATCHERS_KEY] = watchers_map
88
+ _context.set(new_ctx)
89
+
90
+ @staticmethod
91
+ def set(key: str, value: Any) -> None:
92
+ """
93
+ Store a key-value pair in the current task context.
94
+ Uses copy-on-write to isolate mutations across task boundaries.
95
+ """
96
+ current = Context._get_dict()
97
+ # Create a shallow copy of the dict container for the write operation
98
+ new_ctx = current.copy()
99
+ new_ctx[key] = value
100
+ _context.set(new_ctx)
101
+ if key != _WATCHERS_KEY:
102
+ Context._notify_watchers(new_ctx, key, value)
103
+
104
+ @staticmethod
105
+ def get(key: str, default: Any = None) -> Any:
106
+ """Retrieve a value from the current task context."""
107
+ ctx = _context.get()
108
+ if ctx is None:
109
+ return default
110
+ return ctx.get(key, default)
111
+
112
+ @staticmethod
113
+ def has(key: str) -> bool:
114
+ """Return True if the key exists in the current context."""
115
+ ctx = _context.get()
116
+ return key in ctx if ctx is not None else False
117
+
118
+ @staticmethod
119
+ def all() -> dict[str, Any]:
120
+ """Return a shallow copy of all context variables to prevent external mutation."""
121
+ ctx = _context.get()
122
+ if ctx is None:
123
+ return {}
124
+ return {k: v for k, v in ctx.items() if k != _WATCHERS_KEY}
125
+
126
+ @staticmethod
127
+ def clear() -> None:
128
+ """Reset the current task context."""
129
+ previous = _context.get()
130
+ _context.set(None)
131
+ Context._notify_watchers(previous, None, None)
132
+
133
+ @staticmethod
134
+ def push(key: str, value: Any) -> None:
135
+ stack = Context.get(key, [])
136
+ Context.set(key, [*stack, value])
137
+
138
+ @staticmethod
139
+ def pop(key: str) -> Any:
140
+ stack = Context.get(key, [])
141
+
142
+ if not stack:
143
+ return None
144
+
145
+ value = stack[-1]
146
+ Context.set(key, stack[:-1])
147
+
148
+ return value
@@ -0,0 +1,352 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import logging
5
+ import traceback
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, TypeVar, get_type_hints
8
+
9
+ from swiftpy.database.exceptions import ModelNotFoundError, RecordNotFoundError
10
+ from swiftpy.filesystem.exceptions import FileValidationError
11
+ from swiftpy.http.exceptions import (
12
+ ClientDisconnect,
13
+ HttpException,
14
+ PayloadTooLargeError,
15
+ )
16
+ from swiftpy.http.response import Response, json_response, unprocessable
17
+ from swiftpy.validation.exceptions import ValidationException
18
+
19
+ if TYPE_CHECKING:
20
+ from swiftpy.http.request import Request
21
+
22
+ logger = logging.getLogger("swiftpy.core.exceptions")
23
+ E = TypeVar("E", bound=Exception)
24
+
25
+
26
+ class NotFoundError(HttpException):
27
+ """Exception mapped automatically to HTTP 404 Not Found."""
28
+
29
+ def __init__(
30
+ self,
31
+ message: str = "Not Found",
32
+ headers: dict[str, str] | None = None,
33
+ ) -> None:
34
+ super().__init__(message=message, status_code=404, headers=headers)
35
+
36
+
37
+ class UnauthorizedError(HttpException):
38
+ """Exception mapped automatically to HTTP 401 Unauthorized."""
39
+
40
+ def __init__(
41
+ self,
42
+ message: str = "Unauthorized",
43
+ headers: dict[str, str] | None = None,
44
+ ) -> None:
45
+ super().__init__(message=message, status_code=401, headers=headers)
46
+
47
+
48
+ class ForbiddenError(HttpException):
49
+ """Exception mapped automatically to HTTP 403 Forbidden."""
50
+
51
+ def __init__(
52
+ self,
53
+ message: str = "Forbidden",
54
+ headers: dict[str, str] | None = None,
55
+ ) -> None:
56
+ super().__init__(message=message, status_code=403, headers=headers)
57
+
58
+
59
+ class ValidationError(HttpException):
60
+ """Exception mapped automatically to HTTP 422 Unprocessable Entity."""
61
+
62
+ def __init__(
63
+ self,
64
+ errors: dict[str, list[str]] | None = None,
65
+ message: str = "Validation failed",
66
+ headers: dict[str, str] | None = None,
67
+ ) -> None:
68
+ self.errors = errors or {}
69
+ super().__init__(message=message, status_code=422, headers=headers)
70
+
71
+
72
+ class MethodNotAllowedError(HttpException):
73
+ """Exception mapped automatically to HTTP 405 Method Not Allowed."""
74
+
75
+ def __init__(
76
+ self,
77
+ message: str = "Method Not Allowed",
78
+ headers: dict[str, str] | None = None,
79
+ ) -> None:
80
+ super().__init__(message=message, status_code=405, headers=headers)
81
+
82
+
83
+ class BadRequestError(HttpException):
84
+ """Exception mapped automatically to HTTP 400 Bad Request."""
85
+
86
+ def __init__(
87
+ self,
88
+ message: str = "Bad Request",
89
+ headers: dict[str, str] | None = None,
90
+ ) -> None:
91
+ super().__init__(message=message, status_code=400, headers=headers)
92
+
93
+
94
+ class InternalServerError(HttpException):
95
+ """Exception mapped automatically to HTTP 500 Internal Server Error."""
96
+
97
+ def __init__(
98
+ self,
99
+ message: str = "Internal Server Error",
100
+ headers: dict[str, str] | None = None,
101
+ ) -> None:
102
+ super().__init__(message=message, status_code=500, headers=headers)
103
+
104
+
105
+ def _infer_exception_handler_order(
106
+ handler: Callable[..., Any],
107
+ *,
108
+ exception_type: type[Exception] | None = None,
109
+ ) -> tuple[str, str]:
110
+ """Resolve the supported (request, exc) or (exc, request) convention.
111
+
112
+ We accept both conventions, but require an unambiguous result derived from
113
+ the parameter names or the resolved type hints. Ambiguous or unsupported
114
+ signatures raise TypeError during registration so the handler contract is
115
+ explicit instead of guessing at runtime.
116
+ """
117
+ try:
118
+ sig = inspect.signature(handler)
119
+ except (ValueError, TypeError) as exc: # pragma: no cover - defensive
120
+ raise TypeError(f"Invalid exception handler signature for {handler!r}") from exc
121
+
122
+ params = list(sig.parameters.values())
123
+ if len(params) != 2:
124
+ raise TypeError(
125
+ "Exception handlers must accept exactly two parameters: (request, exc) or (exc, request)."
126
+ )
127
+
128
+ try:
129
+ hints = get_type_hints(handler)
130
+ except (AttributeError, NameError, TypeError, ValueError):
131
+ hints = {}
132
+
133
+ first_name = params[0].name.lower()
134
+ second_name = params[1].name.lower()
135
+ first_hint = hints.get(params[0].name, params[0].annotation)
136
+ second_hint = hints.get(params[1].name, params[1].annotation)
137
+
138
+ request_like = (
139
+ first_name in {"request", "req"}
140
+ or getattr(first_hint, "__name__", "") in {"Request", "HttpRequest"}
141
+ )
142
+ exception_like = (
143
+ first_name in {"exc", "error"}
144
+ or (
145
+ exception_type is not None
146
+ and isinstance(first_hint, type)
147
+ and issubclass(first_hint, BaseException)
148
+ and issubclass(first_hint, exception_type)
149
+ )
150
+ )
151
+ second_request_like = (
152
+ second_name in {"request", "req"}
153
+ or getattr(second_hint, "__name__", "") in {"Request", "HttpRequest"}
154
+ )
155
+ second_exception_like = (
156
+ second_name in {"exc", "error"}
157
+ or (
158
+ exception_type is not None
159
+ and isinstance(second_hint, type)
160
+ and issubclass(second_hint, BaseException)
161
+ and issubclass(second_hint, exception_type)
162
+ )
163
+ )
164
+
165
+ if request_like and second_exception_like:
166
+ return "request_first", "request_first"
167
+ if exception_like and second_request_like:
168
+ return "exception_first", "exception_first"
169
+
170
+ if first_name in {"request", "req"} and second_name in {"exc", "error"}:
171
+ return "request_first", "request_first"
172
+ if first_name in {"exc", "error"} and second_name in {"request", "req"}:
173
+ return "exception_first", "exception_first"
174
+
175
+ if request_like and second_request_like:
176
+ raise TypeError(
177
+ "Ambiguous exception handler signature: both parameters appear to be Request-like. "
178
+ "Use (request, exc) or (exc, request) with explicit annotations."
179
+ )
180
+ if exception_like and second_exception_like:
181
+ raise TypeError(
182
+ "Ambiguous exception handler signature: both parameters appear to be exception-like. "
183
+ "Use (request, exc) or (exc, request) with explicit annotations."
184
+ )
185
+
186
+ raise TypeError(
187
+ "Unsupported exception handler signature; expected (request, exc) or (exc, request)."
188
+ )
189
+
190
+
191
+ class ExceptionHandler:
192
+ """
193
+ Global application exception handler.
194
+ Formats HTTP status codes, structured validation errors,
195
+ and returns full tracebacks in development or safe messages in production.
196
+ """
197
+
198
+ def __init__(self, debug: bool = False) -> None:
199
+ self.debug = debug
200
+ self._custom_handlers: dict[type[Exception], Callable[..., Any]] = {}
201
+
202
+ def register(
203
+ self,
204
+ exc_type: type[E],
205
+ handler: Callable[..., Any],
206
+ ) -> None:
207
+ """Register a custom handler for a specific exception class."""
208
+ _infer_exception_handler_order(handler, exception_type=exc_type)
209
+ self._custom_handlers[exc_type] = handler
210
+
211
+ def __call__(
212
+ self,
213
+ exc_type: type[E],
214
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
215
+ """Decorator to register a custom exception handler."""
216
+
217
+ def decorator(handler: Callable[..., Any]) -> Callable[..., Any]:
218
+ self.register(exc_type, handler)
219
+ return handler
220
+
221
+ return decorator
222
+
223
+ async def handle(
224
+ self,
225
+ exc: Exception,
226
+ request: Request | None = None,
227
+ ) -> Response:
228
+ """Process an exception and return a typed HTTP Response."""
229
+ req_method = request.method if request is not None else "-"
230
+ req_path = request.path if request is not None else "-"
231
+
232
+ # 1. Custom exception handlers selected by MRO specificity
233
+ for cls in type(exc).__mro__:
234
+ if not issubclass(cls, Exception):
235
+ continue
236
+ handler = self._custom_handlers.get(cls)
237
+ if handler is not None:
238
+ try:
239
+ order = _infer_exception_handler_order(handler, exception_type=cls)
240
+ if order[0] == "request_first":
241
+ res = handler(request, exc)
242
+ else:
243
+ res = handler(exc, request)
244
+
245
+ if inspect.isawaitable(res):
246
+ res = await res
247
+ if isinstance(res, Response):
248
+ return res
249
+ return json_response(res)
250
+ except Exception:
251
+ logger.exception(
252
+ "Custom exception handler for %s raised an unhandled error",
253
+ cls.__name__,
254
+ )
255
+ return json_response(
256
+ {"detail": "Internal Server Error"},
257
+ status_code=500,
258
+ )
259
+
260
+ # 2. Database Record / Model Not Found errors automatically map to HTTP 404
261
+ if isinstance(exc, (ModelNotFoundError, RecordNotFoundError)):
262
+ logger.info(
263
+ "HTTP 404 %s %s: %s",
264
+ req_method,
265
+ req_path,
266
+ exc.message,
267
+ )
268
+ return json_response(
269
+ {"detail": exc.message},
270
+ status_code=404,
271
+ )
272
+
273
+ # 3. Framework validation errors ONLY (no duck-typing on .errors)
274
+ if isinstance(exc, (ValidationError, ValidationException, FileValidationError)):
275
+ logger.info(
276
+ "HTTP 422 %s %s: %s",
277
+ req_method,
278
+ req_path,
279
+ exc.message,
280
+ )
281
+ headers = exc.headers or {}
282
+ resp = unprocessable(exc.errors)
283
+ if headers:
284
+ resp.headers.update(headers)
285
+ return resp
286
+
287
+ # 4. HTTP Exceptions (HttpException, NotFoundError, UnauthorizedError, etc.)
288
+ if isinstance(exc, HttpException):
289
+ if exc.status_code >= 500:
290
+ logger.error(
291
+ "HTTP %d %s %s: %s",
292
+ exc.status_code,
293
+ req_method,
294
+ req_path,
295
+ exc.message,
296
+ )
297
+ else:
298
+ logger.info(
299
+ "HTTP %d %s %s: %s",
300
+ exc.status_code,
301
+ req_method,
302
+ req_path,
303
+ exc.message,
304
+ )
305
+ headers = exc.headers or {}
306
+ detail = (
307
+ "Internal Server Error"
308
+ if (not self.debug and exc.status_code >= 500)
309
+ else exc.message
310
+ )
311
+ resp = json_response(
312
+ {"detail": detail},
313
+ status_code=exc.status_code,
314
+ )
315
+ if headers:
316
+ resp.headers.update(headers)
317
+ return resp
318
+
319
+ # 5. Unhandled server exceptions
320
+ logger.exception("Unhandled exception during request processing")
321
+
322
+ if self.debug:
323
+ tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
324
+ return json_response(
325
+ {
326
+ "error": "Internal Server Error",
327
+ "exception": exc.__class__.__name__,
328
+ "message": str(exc),
329
+ "traceback": tb,
330
+ },
331
+ status_code=500,
332
+ )
333
+
334
+ return json_response(
335
+ {"detail": "Internal Server Error"},
336
+ status_code=500,
337
+ )
338
+
339
+
340
+ __all__ = [
341
+ "BadRequestError",
342
+ "ClientDisconnect",
343
+ "ExceptionHandler",
344
+ "ForbiddenError",
345
+ "HttpException",
346
+ "InternalServerError",
347
+ "MethodNotAllowedError",
348
+ "NotFoundError",
349
+ "PayloadTooLargeError",
350
+ "UnauthorizedError",
351
+ "ValidationError",
352
+ ]
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from swiftpy.core.container import Container
6
+
7
+
8
+ class ServiceProvider(ABC):
9
+ """
10
+ Base service provider.
11
+ """
12
+
13
+ def __init__(self, container: Container) -> None:
14
+ self.container = container
15
+
16
+ @abstractmethod
17
+ def register(self) -> None:
18
+ """
19
+ Register services into the container.
20
+ """
21
+
22
+ def boot(self) -> None:
23
+ """
24
+ Optional synchronous boot hook.
25
+ """
26
+
27
+ async def startup(self) -> None:
28
+ """
29
+ Optional asynchronous startup hook invoked during ASGI lifespan startup.
30
+ """
31
+
32
+ async def shutdown(self) -> None:
33
+ """
34
+ Optional asynchronous shutdown hook invoked during ASGI lifespan shutdown.
35
+ """
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.database.exceptions import (
4
+ DatabaseError,
5
+ ModelNotFoundError,
6
+ RecordNotFoundError,
7
+ )
8
+ from swiftpy.database.migrations.blueprint import Blueprint
9
+ from swiftpy.database.migrations.schema import Schema
10
+ from swiftpy.database.pagination import Paginator, SimplePaginator
11
+ from swiftpy.database.pool import DatabasePool
12
+ from swiftpy.database.query_builder import Query, QueryBuilder, QueryError, escape_like
13
+ from swiftpy.database.seeder import Seeder
14
+ from swiftpy.database.transaction import (
15
+ Database,
16
+ TransactionClosedError,
17
+ TransactionConnection,
18
+ db,
19
+ on_commit,
20
+ )
21
+
22
+ __all__ = [
23
+ "Blueprint",
24
+ "Database",
25
+ "DatabaseError",
26
+ "DatabasePool",
27
+ "ModelNotFoundError",
28
+ "Paginator",
29
+ "Query",
30
+ "QueryBuilder",
31
+ "QueryError",
32
+ "RecordNotFoundError",
33
+ "Schema",
34
+ "Seeder",
35
+ "SimplePaginator",
36
+ "TransactionClosedError",
37
+ "TransactionConnection",
38
+ "db",
39
+ "escape_like",
40
+ "on_commit",
41
+ ]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class DatabaseError(Exception):
7
+ """Base exception for all database subsystem errors."""
8
+
9
+
10
+ class RecordNotFoundError(DatabaseError, LookupError):
11
+ """Raised when a specific raw query record is not found."""
12
+
13
+ def __init__(self, message: str = "Record not found.") -> None:
14
+ super().__init__(message)
15
+ self.message = message
16
+
17
+
18
+ class ModelNotFoundError(RecordNotFoundError):
19
+ """Raised when an ORM model query using *_or_fail fails to find a matching instance."""
20
+
21
+ def __init__(
22
+ self,
23
+ model: str | type[Any],
24
+ primary_key: Any = None,
25
+ message: str | None = None,
26
+ ) -> None:
27
+ self.model = model if isinstance(model, str) else model.__name__
28
+ self.primary_key = primary_key
29
+ if message is None:
30
+ if primary_key is not None:
31
+ message = f"No query results for model [{self.model}] with primary key [{primary_key}]."
32
+ else:
33
+ message = f"No query results for model [{self.model}]."
34
+ super().__init__(message)
35
+
36
+
37
+ __all__ = [
38
+ "DatabaseError",
39
+ "ModelNotFoundError",
40
+ "RecordNotFoundError",
41
+ ]
42
+