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,126 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from types import TracebackType
5
+ from typing import Any, Self
6
+
7
+ from swiftpy.core.application import Application
8
+ from swiftpy.testing.http_client import TestClient, TestResponse
9
+
10
+
11
+ class TestCase:
12
+ """
13
+ Base test case for SwiftPY applications.
14
+ Provides an in-process TestClient, lifecycle helpers, and HTTP convenience methods.
15
+ """
16
+
17
+ __test__ = False
18
+ app: Application | None = None
19
+ _client: TestClient | None = None
20
+
21
+ def create_app(self) -> Application:
22
+ """Create and return an Application instance for testing."""
23
+ return Application()
24
+
25
+ def get_app(self) -> Application:
26
+ """Retrieve the test application instance."""
27
+ if self.app is None:
28
+ self.app = self.create_app()
29
+ return self.app
30
+
31
+ @property
32
+ def client(self) -> TestClient:
33
+ """Retrieve or create the in-process TestClient."""
34
+ if self._client is None:
35
+ self._client = TestClient(self.get_app())
36
+ return self._client
37
+
38
+ async def startup(self) -> None:
39
+ """Start the underlying ASGI lifespan on the test client."""
40
+ await self.client.startup()
41
+
42
+ async def shutdown(self) -> None:
43
+ """Shut down the underlying ASGI lifespan on the test client if created."""
44
+ if self._client is not None:
45
+ await self._client.shutdown()
46
+
47
+ def reset(self) -> None:
48
+ """Reset cached application and TestClient state between test runs."""
49
+ self._client = None
50
+ self.app = None
51
+
52
+ async def __aenter__(self) -> Self:
53
+ await self.startup()
54
+ return self
55
+
56
+ async def __aexit__(
57
+ self,
58
+ exc_type: type[BaseException] | None,
59
+ exc_val: BaseException | None,
60
+ exc_tb: TracebackType | None,
61
+ ) -> None:
62
+ await self.shutdown()
63
+
64
+ async def get(
65
+ self,
66
+ path: str,
67
+ *,
68
+ headers: dict[str, str] | None = None,
69
+ query: dict[str, Any] | None = None,
70
+ ) -> TestResponse:
71
+ return await self.client.get(path, headers=headers, query=query)
72
+
73
+ async def post(
74
+ self,
75
+ path: str,
76
+ *,
77
+ json: Any = None,
78
+ data: bytes | str | Mapping[str, Any] | None = None,
79
+ headers: dict[str, str] | None = None,
80
+ query: dict[str, Any] | None = None,
81
+ ) -> TestResponse:
82
+ return await self.client.post(
83
+ path, json=json, data=data, headers=headers, query=query
84
+ )
85
+
86
+ async def put(
87
+ self,
88
+ path: str,
89
+ *,
90
+ json: Any = None,
91
+ data: bytes | str | Mapping[str, Any] | None = None,
92
+ headers: dict[str, str] | None = None,
93
+ query: dict[str, Any] | None = None,
94
+ ) -> TestResponse:
95
+ return await self.client.put(
96
+ path, json=json, data=data, headers=headers, query=query
97
+ )
98
+
99
+ async def patch(
100
+ self,
101
+ path: str,
102
+ *,
103
+ json: Any = None,
104
+ data: bytes | str | Mapping[str, Any] | None = None,
105
+ headers: dict[str, str] | None = None,
106
+ query: dict[str, Any] | None = None,
107
+ ) -> TestResponse:
108
+ return await self.client.patch(
109
+ path, json=json, data=data, headers=headers, query=query
110
+ )
111
+
112
+ async def delete(
113
+ self,
114
+ path: str,
115
+ *,
116
+ json: Any = None,
117
+ data: bytes | str | Mapping[str, Any] | None = None,
118
+ headers: dict[str, str] | None = None,
119
+ query: dict[str, Any] | None = None,
120
+ ) -> TestResponse:
121
+ return await self.client.delete(
122
+ path, json=json, data=data, headers=headers, query=query
123
+ )
124
+
125
+
126
+ __all__ = ["TestCase"]
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.validation.exceptions import ValidationException
4
+ from swiftpy.validation.form_request import FormRequest
5
+ from swiftpy.validation.protocols import ValidatorRule
6
+ from swiftpy.validation.rules import (
7
+ BailRule,
8
+ BetweenRule,
9
+ BoolRule,
10
+ EmailRule,
11
+ ExistsRule,
12
+ FloatRule,
13
+ InRule,
14
+ IntRule,
15
+ MaxRule,
16
+ MinRule,
17
+ NotInRule,
18
+ NullableRule,
19
+ RegexRule,
20
+ RequiredRule,
21
+ RuleRegistry,
22
+ StrRule,
23
+ UniqueRule,
24
+ UrlRule,
25
+ bail,
26
+ between,
27
+ bool_,
28
+ boolean,
29
+ default_registry,
30
+ email,
31
+ exists,
32
+ float_,
33
+ in_,
34
+ int_,
35
+ integer,
36
+ max_,
37
+ min_,
38
+ not_in,
39
+ nullable,
40
+ regex,
41
+ register_rule,
42
+ required,
43
+ str_,
44
+ string,
45
+ unique,
46
+ url,
47
+ )
48
+ from swiftpy.validation.validator import ValidationResult, Validator, validate
49
+
50
+ __all__ = [
51
+ "BailRule",
52
+ "BetweenRule",
53
+ "BoolRule",
54
+ "EmailRule",
55
+ "ExistsRule",
56
+ "FloatRule",
57
+ "FormRequest",
58
+ "InRule",
59
+ "IntRule",
60
+ "MaxRule",
61
+ "MinRule",
62
+ "NotInRule",
63
+ "NullableRule",
64
+ "RegexRule",
65
+ "RequiredRule",
66
+ "RuleRegistry",
67
+ "StrRule",
68
+ "UniqueRule",
69
+ "UrlRule",
70
+ "ValidationException",
71
+ "ValidationResult",
72
+ "Validator",
73
+ "ValidatorRule",
74
+ "bail",
75
+ "between",
76
+ "bool_",
77
+ "boolean",
78
+ "default_registry",
79
+ "email",
80
+ "exists",
81
+ "float_",
82
+ "in_",
83
+ "int_",
84
+ "integer",
85
+ "max_",
86
+ "min_",
87
+ "not_in",
88
+ "nullable",
89
+ "regex",
90
+ "register_rule",
91
+ "required",
92
+ "str_",
93
+ "string",
94
+ "unique",
95
+ "url",
96
+ "validate",
97
+ ]
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.http.exceptions import HttpException
4
+
5
+
6
+ class ValidationException(HttpException):
7
+ """
8
+ Exception raised when validation fails.
9
+ Carries structured field error messages and defaults to HTTP status 422.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ errors: dict[str, list[str]],
15
+ message: str = "The given data was invalid.",
16
+ headers: dict[str, str] | None = None,
17
+ ) -> None:
18
+ self.errors = errors
19
+ super().__init__(
20
+ message=message,
21
+ status_code=422,
22
+ headers=headers,
23
+ )
24
+
25
+
26
+ __all__ = ["ValidationException"]
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any, ClassVar, Self
5
+ from weakref import WeakKeyDictionary
6
+
7
+ from swiftpy.core.config import ConfigurationError
8
+ from swiftpy.core.container import Container, get_container
9
+ from swiftpy.http.request import Request
10
+ from swiftpy.validation.rules.registry import RuleRegistry, default_registry
11
+ from swiftpy.validation.validator import ValidationResult
12
+
13
+
14
+ class FormRequest:
15
+ """
16
+ Base class for typed request validation. Handlers can declare a parameter
17
+ typed with a FormRequest subclass; SwiftPY automatically validates incoming
18
+ request payloads and returns HTTP 422 if validation fails.
19
+ """
20
+
21
+ schema: ClassVar[dict[str, Any]] = {}
22
+ _cached_parsed_rules: ClassVar[dict[str, list[Any]] | None] = None
23
+ _cached_parsed_by_registry: ClassVar[
24
+ WeakKeyDictionary[RuleRegistry, dict[str, list[Any]]] | None
25
+ ] = None
26
+
27
+ def __init_subclass__(cls, **kwargs: Any) -> None:
28
+ super().__init_subclass__(**kwargs)
29
+ cls._cached_parsed_rules = None
30
+ cls._cached_parsed_by_registry = None
31
+
32
+ def __init__(
33
+ self,
34
+ data: dict[str, Any] | None = None,
35
+ raw_data: dict[str, Any] | None = None,
36
+ request: Request | None = None,
37
+ validation_result: ValidationResult | None = None,
38
+ ) -> None:
39
+ self.data: dict[str, Any] = data or {}
40
+ self.raw_data: dict[str, Any] = (
41
+ raw_data if raw_data is not None else dict(self.data)
42
+ )
43
+ self.request = request
44
+ self.validation_result = validation_result
45
+
46
+ def rules(self) -> dict[str, Any]:
47
+ """
48
+ Define validation rules. Defaults to returning the class 'schema' attribute.
49
+ Can be overridden in subclasses for dynamic rules.
50
+ """
51
+ return self.schema
52
+
53
+ def messages(self) -> dict[str, str]:
54
+ """
55
+ Define custom validation error messages keyed by 'field.rule', 'field', or 'rule'.
56
+ Can be overridden in subclasses.
57
+ """
58
+ return {}
59
+
60
+ @classmethod
61
+ def _requires_di_resolution(cls) -> bool:
62
+ """Return True only when cls.__init__ declares required dependency parameters."""
63
+ if cls.__init__ is FormRequest.__init__:
64
+ return False
65
+ sig = inspect.signature(cls.__init__)
66
+ for idx, param in enumerate(sig.parameters.values()):
67
+ if idx == 0:
68
+ continue
69
+ if param.kind in (
70
+ inspect.Parameter.VAR_POSITIONAL,
71
+ inspect.Parameter.VAR_KEYWORD,
72
+ ):
73
+ continue
74
+ if param.default is inspect.Parameter.empty:
75
+ return True
76
+ return False
77
+
78
+ @classmethod
79
+ def resolve_instance(
80
+ cls,
81
+ *,
82
+ data: dict[str, Any] | None = None,
83
+ raw_data: dict[str, Any] | None = None,
84
+ request: Request | None = None,
85
+ validation_result: ValidationResult | None = None,
86
+ container: Container | None = None,
87
+ ) -> Self:
88
+ """
89
+ Instantiate a FormRequest using explicit DI resolution only when required,
90
+ avoiding broad TypeError fallbacks that mask constructor bugs.
91
+ """
92
+ cont = container
93
+ if cont is None:
94
+ try:
95
+ cont = get_container()
96
+ except RuntimeError:
97
+ pass
98
+
99
+ if cont is not None and cont.has(cls):
100
+ instance = cont.resolve(cls)
101
+ elif cls._requires_di_resolution():
102
+ if cont is not None and cont.can_resolve(cls):
103
+ instance = cont.resolve(cls)
104
+ else:
105
+ raise ConfigurationError(
106
+ f"Failed to resolve dependencies for {cls.__name__}"
107
+ )
108
+ elif cls.__init__ is FormRequest.__init__:
109
+ instance = cls(
110
+ data=data,
111
+ raw_data=raw_data,
112
+ request=request,
113
+ validation_result=validation_result,
114
+ )
115
+ else:
116
+ sig = inspect.signature(cls.__init__)
117
+ params = sig.parameters
118
+ has_varkw = any(
119
+ p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
120
+ )
121
+ init_kwargs: dict[str, Any] = {}
122
+ candidate_kwargs: dict[str, Any] = {
123
+ "data": data,
124
+ "raw_data": raw_data,
125
+ "request": request,
126
+ "validation_result": validation_result,
127
+ }
128
+ for key, val in candidate_kwargs.items():
129
+ if val is not None and (has_varkw or key in params):
130
+ init_kwargs[key] = val
131
+ instance = cls(**init_kwargs)
132
+
133
+ if data is not None:
134
+ instance.data = data
135
+ elif not hasattr(instance, "data"):
136
+ instance.data = {}
137
+ if raw_data is not None:
138
+ instance.raw_data = raw_data
139
+ elif not hasattr(instance, "raw_data"):
140
+ instance.raw_data = dict(instance.data)
141
+ if request is not None or not hasattr(instance, "request"):
142
+ instance.request = request
143
+ if validation_result is not None or not hasattr(
144
+ instance, "validation_result"
145
+ ):
146
+ instance.validation_result = validation_result
147
+ return instance
148
+
149
+ @classmethod
150
+ def get_rules(
151
+ cls,
152
+ container: Container | None = None,
153
+ request: Request | None = None,
154
+ ) -> dict[str, Any]:
155
+ """
156
+ Resolve validation rules for this class.
157
+ Fails closed with ConfigurationError if rule resolution fails (D-9).
158
+ """
159
+ instance = cls.resolve_instance(request=request, container=container)
160
+ try:
161
+ rules_def = instance.rules()
162
+ if not isinstance(rules_def, dict):
163
+ raise ConfigurationError(
164
+ f"'{cls.__name__}.rules()' must return a dict, got {type(rules_def).__name__}"
165
+ )
166
+ return rules_def
167
+ except ConfigurationError:
168
+ raise
169
+ except Exception as exc:
170
+ raise ConfigurationError(
171
+ f"Failed to resolve validation rules for {cls.__name__}: {exc}"
172
+ ) from exc
173
+
174
+ @classmethod
175
+ def prepare_validation(
176
+ cls,
177
+ *,
178
+ registry: RuleRegistry | None = None,
179
+ container: Container | None = None,
180
+ request: Request | None = None,
181
+ ) -> tuple[Self, dict[str, list[Any]], dict[str, str]]:
182
+ """
183
+ Resolve FormRequest instance with current request context and return
184
+ (instance, parsed_rules, custom_messages).
185
+ Dynamic rules() overrides are evaluated per request rather than frozen on the class.
186
+ """
187
+ instance = cls.resolve_instance(request=request, container=container)
188
+ try:
189
+ rules_def = instance.rules()
190
+ if not isinstance(rules_def, dict):
191
+ raise ConfigurationError(
192
+ f"'{cls.__name__}.rules()' must return a dict, got {type(rules_def).__name__}"
193
+ )
194
+ messages_def = instance.messages()
195
+ if not isinstance(messages_def, dict):
196
+ raise ConfigurationError(
197
+ f"'{cls.__name__}.messages()' must return a dict, got {type(messages_def).__name__}"
198
+ )
199
+ except ConfigurationError:
200
+ raise
201
+ except Exception as exc:
202
+ raise ConfigurationError(
203
+ f"Failed to resolve validation metadata for {cls.__name__}: {exc}"
204
+ ) from exc
205
+
206
+ reg = registry or default_registry
207
+ is_static_schema = cls.rules is FormRequest.rules
208
+ if is_static_schema:
209
+ cached = cls._get_cached_static_rules(reg)
210
+ if cached is not None:
211
+ return instance, cached, messages_def
212
+
213
+ parsed: dict[str, list[Any]] = {}
214
+ for field_name, rule_def in rules_def.items():
215
+ parsed[field_name] = reg.parse_rules(rule_def)
216
+
217
+ if is_static_schema:
218
+ cls._set_cached_static_rules(reg, parsed)
219
+
220
+ return instance, parsed, messages_def
221
+
222
+ @classmethod
223
+ def _get_cached_static_rules(
224
+ cls, registry: RuleRegistry
225
+ ) -> dict[str, list[Any]] | None:
226
+ if registry is default_registry:
227
+ return cls._cached_parsed_rules
228
+ if cls._cached_parsed_by_registry is not None:
229
+ return cls._cached_parsed_by_registry.get(registry)
230
+ return None
231
+
232
+ @classmethod
233
+ def _set_cached_static_rules(
234
+ cls, registry: RuleRegistry, parsed: dict[str, list[Any]]
235
+ ) -> None:
236
+ if registry is default_registry:
237
+ cls._cached_parsed_rules = parsed
238
+ return
239
+ if cls._cached_parsed_by_registry is None:
240
+ cls._cached_parsed_by_registry = WeakKeyDictionary()
241
+ cls._cached_parsed_by_registry[registry] = parsed
242
+
243
+ @classmethod
244
+ def get_parsed_rules(
245
+ cls,
246
+ registry: RuleRegistry | None = None,
247
+ container: Container | None = None,
248
+ request: Request | None = None,
249
+ ) -> dict[str, list[Any]]:
250
+ """
251
+ Resolve and parse validation rules. Static schema classes cache per registry;
252
+ dynamic rules() subclasses are evaluated per call with request context.
253
+ """
254
+ reg = registry or default_registry
255
+ is_static_schema = cls.rules is FormRequest.rules
256
+ if is_static_schema:
257
+ cached = cls._get_cached_static_rules(reg)
258
+ if cached is not None:
259
+ return cached
260
+
261
+ rules_dict = cls.get_rules(container=container, request=request)
262
+ parsed: dict[str, list[Any]] = {}
263
+ for field_name, rule_def in rules_dict.items():
264
+ parsed[field_name] = reg.parse_rules(rule_def)
265
+
266
+ if is_static_schema:
267
+ cls._set_cached_static_rules(reg, parsed)
268
+ return parsed
269
+
270
+ @classmethod
271
+ def clear_rule_cache(cls) -> None:
272
+ """Clear the parsed rules cache for this class (useful for tests)."""
273
+ cls._cached_parsed_rules = None
274
+ if cls._cached_parsed_by_registry is not None:
275
+ cls._cached_parsed_by_registry.clear()
276
+
277
+ def validated(self) -> dict[str, Any]:
278
+ """Return the dictionary of validated, cleaned parameters."""
279
+ return dict(self.data)
280
+
281
+ def all(self) -> dict[str, Any]:
282
+ """Return all raw input parameters."""
283
+ return dict(self.raw_data)
284
+
285
+ def get(self, key: str, default: Any = None) -> Any:
286
+ return self.data.get(key, default)
287
+
288
+ def __getitem__(self, item: str) -> Any:
289
+ return self.data[item]
290
+
291
+ def __contains__(self, item: str) -> bool:
292
+ return item in self.data
293
+
294
+ def __getattr__(self, name: str) -> Any:
295
+ if name in self.data:
296
+ return self.data[name]
297
+ raise AttributeError(
298
+ f"'{self.__class__.__name__}' object has no attribute '{name}'"
299
+ )
300
+
301
+
302
+ __all__ = ["FormRequest"]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable
4
+ from typing import Any, Protocol, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class ValidatorRule(Protocol):
9
+ """
10
+ Protocol for validation rules.
11
+ Rules can be synchronous or asynchronous.
12
+ """
13
+
14
+ def validate(
15
+ self,
16
+ field: str,
17
+ value: Any,
18
+ data: dict[str, Any],
19
+ ) -> bool | Awaitable[bool]:
20
+ """
21
+ Validate whether the given field value is valid according to this rule.
22
+ Can be synchronous (returning bool) or asynchronous (returning Awaitable[bool]).
23
+ """
24
+ ...
25
+
26
+ def message(self, field: str) -> str:
27
+ """
28
+ Return the validation error message when this rule fails.
29
+ """
30
+ ...
31
+
32
+
33
+ __all__ = ["ValidatorRule"]
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.validation.rules.builtin import (
4
+ BailRule,
5
+ BetweenRule,
6
+ BoolRule,
7
+ EmailRule,
8
+ FloatRule,
9
+ InRule,
10
+ IntRule,
11
+ MaxRule,
12
+ MinRule,
13
+ NotInRule,
14
+ RegexRule,
15
+ StrRule,
16
+ UrlRule,
17
+ bail,
18
+ between,
19
+ bool_,
20
+ boolean,
21
+ email,
22
+ float_,
23
+ in_,
24
+ int_,
25
+ integer,
26
+ max_,
27
+ min_,
28
+ not_in,
29
+ regex,
30
+ str_,
31
+ string,
32
+ url,
33
+ )
34
+ from swiftpy.validation.rules.exists import ExistsRule, exists
35
+ from swiftpy.validation.rules.registry import (
36
+ RuleRegistry,
37
+ default_registry,
38
+ register_rule,
39
+ )
40
+ from swiftpy.validation.rules.required import (
41
+ NullableRule,
42
+ RequiredRule,
43
+ nullable,
44
+ required,
45
+ )
46
+ from swiftpy.validation.rules.unique import UniqueRule, unique
47
+
48
+ __all__ = [
49
+ "BailRule",
50
+ "BetweenRule",
51
+ "BoolRule",
52
+ "EmailRule",
53
+ "ExistsRule",
54
+ "FloatRule",
55
+ "InRule",
56
+ "IntRule",
57
+ "MaxRule",
58
+ "MinRule",
59
+ "NotInRule",
60
+ "NullableRule",
61
+ "RegexRule",
62
+ "RequiredRule",
63
+ "RuleRegistry",
64
+ "StrRule",
65
+ "UniqueRule",
66
+ "UrlRule",
67
+ "bail",
68
+ "between",
69
+ "bool_",
70
+ "boolean",
71
+ "default_registry",
72
+ "email",
73
+ "exists",
74
+ "float_",
75
+ "in_",
76
+ "int_",
77
+ "integer",
78
+ "max_",
79
+ "min_",
80
+ "not_in",
81
+ "nullable",
82
+ "regex",
83
+ "register_rule",
84
+ "required",
85
+ "str_",
86
+ "string",
87
+ "unique",
88
+ "url",
89
+ ]