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,391 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+ import urllib.parse
6
+ from collections.abc import Sequence
7
+ from typing import Any
8
+
9
+ from swiftpy.core.config import ConfigurationError
10
+
11
+ # Strict RFC-compliant email regex patterns
12
+ _LOCAL_PART_REGEX = re.compile(
13
+ r"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*$"
14
+ )
15
+ _DOMAIN_LABEL_REGEX = re.compile(r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$")
16
+
17
+ # ASCII-only integer and finite float regexes
18
+ _INT_REGEX = re.compile(r"^[+-]?[0-9]+$")
19
+ _FLOAT_REGEX = re.compile(
20
+ r"^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$"
21
+ )
22
+
23
+
24
+ class StrRule:
25
+ """Validates that a field is a string."""
26
+
27
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
28
+ return isinstance(value, str) and not isinstance(value, bool)
29
+
30
+ def message(self, field: str) -> str:
31
+ return f"The {field} field must be a string."
32
+
33
+
34
+ class IntRule:
35
+ """Validates that a field is an integer or coercible integer string."""
36
+
37
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
38
+ if isinstance(value, bool):
39
+ return False
40
+ if isinstance(value, int):
41
+ return True
42
+ if isinstance(value, str):
43
+ clean = value.strip()
44
+ return bool(_INT_REGEX.fullmatch(clean))
45
+ return False
46
+
47
+ def message(self, field: str) -> str:
48
+ return f"The {field} field must be an integer."
49
+
50
+
51
+ class FloatRule:
52
+ """Validates that a field is a finite float, integer, or numeric float string."""
53
+
54
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
55
+ if isinstance(value, bool):
56
+ return False
57
+ if isinstance(value, (int, float)):
58
+ return math.isfinite(value)
59
+ if isinstance(value, str):
60
+ clean = value.strip()
61
+ if not _FLOAT_REGEX.fullmatch(clean):
62
+ return False
63
+ try:
64
+ val = float(clean)
65
+ return math.isfinite(val)
66
+ except (ValueError, TypeError):
67
+ return False
68
+ return False
69
+
70
+ def message(self, field: str) -> str:
71
+ return f"The {field} field must be a float."
72
+
73
+
74
+ class BoolRule:
75
+ """Validates that a field is a boolean value or boolean-like token."""
76
+
77
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
78
+ if isinstance(value, bool):
79
+ return True
80
+ if isinstance(value, int) and value in (0, 1):
81
+ return True
82
+ return isinstance(value, str) and value.strip().lower() in (
83
+ "true",
84
+ "false",
85
+ "1",
86
+ "0",
87
+ )
88
+
89
+ def message(self, field: str) -> str:
90
+ return f"The {field} field must be a boolean."
91
+
92
+
93
+ class EmailRule:
94
+ """Validates that a field is a well-formed email address."""
95
+
96
+ def __init__(self, lowercase: bool = False) -> None:
97
+ self.lowercase = lowercase
98
+
99
+ def normalize(self, value: str) -> str:
100
+ val = value.strip()
101
+ if "@" in val:
102
+ local, domain = val.rsplit("@", 1)
103
+ if self.lowercase:
104
+ return f"{local.lower()}@{domain.lower()}"
105
+ return f"{local}@{domain.lower()}"
106
+ return val
107
+
108
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
109
+ if not isinstance(value, str) or isinstance(value, bool):
110
+ return False
111
+ val = value.strip()
112
+ if len(val) > 254 or ".." in val:
113
+ return False
114
+ if val.count("@") != 1:
115
+ return False
116
+ local, domain = val.split("@")
117
+ if not local or len(local) > 64 or not _LOCAL_PART_REGEX.fullmatch(local):
118
+ return False
119
+ if not domain or len(domain) > 253:
120
+ return False
121
+ labels = domain.split(".")
122
+ if len(labels) < 2:
123
+ return False
124
+ for label in labels:
125
+ if not _DOMAIN_LABEL_REGEX.fullmatch(label):
126
+ return False
127
+ return True
128
+
129
+ def message(self, field: str) -> str:
130
+ return f"The {field} field must be a valid email address."
131
+
132
+
133
+ class UrlRule:
134
+ """Validates that a field is a valid HTTP or HTTPS URL."""
135
+
136
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
137
+ if not isinstance(value, str) or isinstance(value, bool):
138
+ return False
139
+ val = value.strip()
140
+ if len(val) > 2048:
141
+ return False
142
+ try:
143
+ parsed = urllib.parse.urlparse(val)
144
+ return bool(parsed.scheme in ("http", "https") and parsed.netloc)
145
+ except (ValueError, TypeError):
146
+ return False
147
+
148
+ def message(self, field: str) -> str:
149
+ return f"The {field} field must be a valid URL."
150
+
151
+
152
+ class MinRule:
153
+ """Validates minimum length for strings/collections or minimum numeric value."""
154
+
155
+ def __init__(self, min_val: float) -> None:
156
+ self.min_val = min_val
157
+
158
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
159
+ if isinstance(value, bool):
160
+ return False
161
+ if isinstance(value, (int, float)):
162
+ return value >= self.min_val
163
+ if isinstance(value, (str, list, dict, set, tuple)):
164
+ return len(value) >= self.min_val
165
+ return False
166
+
167
+ def message(self, field: str) -> str:
168
+ return f"The {field} field must be at least {self.min_val}."
169
+
170
+
171
+ class MaxRule:
172
+ """Validates maximum length for strings/collections or maximum numeric value."""
173
+
174
+ def __init__(self, max_val: float) -> None:
175
+ self.max_val = max_val
176
+
177
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
178
+ if isinstance(value, bool):
179
+ return False
180
+ if isinstance(value, (int, float)):
181
+ return value <= self.max_val
182
+ if isinstance(value, (str, list, dict, set, tuple)):
183
+ return len(value) <= self.max_val
184
+ return False
185
+
186
+ def message(self, field: str) -> str:
187
+ return f"The {field} field must not be greater than {self.max_val}."
188
+
189
+
190
+ class BetweenRule:
191
+ """Validates that a value or length is within an inclusive numeric range."""
192
+
193
+ def __init__(self, min_val: float, max_val: float) -> None:
194
+ if min_val > max_val:
195
+ raise ValueError(
196
+ f"min_val ({min_val}) cannot be greater than max_val ({max_val})"
197
+ )
198
+ self.min_val = min_val
199
+ self.max_val = max_val
200
+
201
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
202
+ if isinstance(value, bool):
203
+ return False
204
+ if isinstance(value, (int, float)):
205
+ return self.min_val <= value <= self.max_val
206
+ if isinstance(value, (str, list, dict, set, tuple)):
207
+ return self.min_val <= len(value) <= self.max_val
208
+ return False
209
+
210
+ def message(self, field: str) -> str:
211
+ return f"The {field} field must be between {self.min_val} and {self.max_val}."
212
+
213
+
214
+ class InRule:
215
+ """Validates that a field value exists within an allowed list of values."""
216
+
217
+ def __init__(self, values: Sequence[Any] | set[Any]) -> None:
218
+ self.values = list(values)
219
+ self._str_values = {str(v) for v in self.values if not isinstance(v, bool)}
220
+ self._bool_values = {v for v in self.values if isinstance(v, bool)}
221
+
222
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
223
+ if isinstance(value, bool):
224
+ return value in self._bool_values
225
+ for v in self.values:
226
+ if not isinstance(v, bool) and value == v:
227
+ return True
228
+ if isinstance(value, (str, int)):
229
+ return str(value) in self._str_values
230
+ return False
231
+
232
+ def message(self, field: str) -> str:
233
+ return f"The selected {field} is invalid."
234
+
235
+
236
+ class NotInRule:
237
+ """Validates that a field value does not exist within a forbidden list of values."""
238
+
239
+ def __init__(self, values: Sequence[Any] | set[Any]) -> None:
240
+ self.values = list(values)
241
+ self._str_values = {str(v) for v in self.values if not isinstance(v, bool)}
242
+ self._bool_values = {v for v in self.values if isinstance(v, bool)}
243
+
244
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
245
+ if isinstance(value, bool):
246
+ return value not in self._bool_values
247
+ for v in self.values:
248
+ if not isinstance(v, bool) and value == v:
249
+ return False
250
+ if isinstance(value, (str, int)):
251
+ return str(value) not in self._str_values
252
+ return True
253
+
254
+ def message(self, field: str) -> str:
255
+ return f"The selected {field} is invalid."
256
+
257
+
258
+ class RegexRule:
259
+ """Validates that a field value matches a specific regular expression."""
260
+
261
+ def __init__(self, pattern: str | re.Pattern[str]) -> None:
262
+ if isinstance(pattern, re.Pattern):
263
+ self.pattern: re.Pattern[str] = pattern
264
+ else:
265
+ try:
266
+ self.pattern = re.compile(pattern)
267
+ except re.error as exc:
268
+ raise ConfigurationError(
269
+ f"Invalid regular expression pattern '{pattern}': {exc}"
270
+ ) from exc
271
+
272
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
273
+ if not isinstance(value, (str, int, float)) or isinstance(value, bool):
274
+ return False
275
+ try:
276
+ return bool(self.pattern.search(str(value)))
277
+ except re.error:
278
+ return False
279
+
280
+ def message(self, field: str) -> str:
281
+ return f"The {field} field format is invalid."
282
+
283
+
284
+ class BailRule:
285
+ """Marker rule instructing Validator to stop validating a field after its first failure."""
286
+
287
+ is_bail_rule: bool = True
288
+
289
+ def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
290
+ return True
291
+
292
+ def message(self, field: str) -> str:
293
+ return ""
294
+
295
+
296
+ # Rule factories
297
+ def str_() -> StrRule:
298
+ return StrRule()
299
+
300
+
301
+ def string() -> StrRule:
302
+ return StrRule()
303
+
304
+
305
+ def int_() -> IntRule:
306
+ return IntRule()
307
+
308
+
309
+ def integer() -> IntRule:
310
+ return IntRule()
311
+
312
+
313
+ def float_() -> FloatRule:
314
+ return FloatRule()
315
+
316
+
317
+ def bool_() -> BoolRule:
318
+ return BoolRule()
319
+
320
+
321
+ def boolean() -> BoolRule:
322
+ return BoolRule()
323
+
324
+
325
+ def email(lowercase: bool = False) -> EmailRule:
326
+ return EmailRule(lowercase=lowercase)
327
+
328
+
329
+ def url() -> UrlRule:
330
+ return UrlRule()
331
+
332
+
333
+ def min_(val: float) -> MinRule:
334
+ return MinRule(val)
335
+
336
+
337
+ def max_(val: float) -> MaxRule:
338
+ return MaxRule(val)
339
+
340
+
341
+ def between(min_val: float, max_val: float) -> BetweenRule:
342
+ return BetweenRule(min_val, max_val)
343
+
344
+
345
+ def in_(values: Sequence[Any] | set[Any]) -> InRule:
346
+ return InRule(values)
347
+
348
+
349
+ def not_in(values: Sequence[Any] | set[Any]) -> NotInRule:
350
+ return NotInRule(values)
351
+
352
+
353
+ def regex(pattern: str | re.Pattern[str]) -> RegexRule:
354
+ return RegexRule(pattern)
355
+
356
+
357
+ def bail() -> BailRule:
358
+ return BailRule()
359
+
360
+
361
+ __all__ = [
362
+ "BailRule",
363
+ "BetweenRule",
364
+ "BoolRule",
365
+ "EmailRule",
366
+ "FloatRule",
367
+ "InRule",
368
+ "IntRule",
369
+ "MaxRule",
370
+ "MinRule",
371
+ "NotInRule",
372
+ "RegexRule",
373
+ "StrRule",
374
+ "UrlRule",
375
+ "bail",
376
+ "between",
377
+ "bool_",
378
+ "boolean",
379
+ "email",
380
+ "float_",
381
+ "in_",
382
+ "int_",
383
+ "integer",
384
+ "max_",
385
+ "min_",
386
+ "not_in",
387
+ "regex",
388
+ "str_",
389
+ "string",
390
+ "url",
391
+ ]
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import asyncpg
6
+
7
+ from swiftpy.core.container import get_container
8
+ from swiftpy.database.identifiers import validate_identifier
9
+ from swiftpy.database.pool import DatabasePool
10
+ from swiftpy.database.transaction import get_active_connection
11
+
12
+
13
+ class ExistsRule:
14
+ """
15
+ Validates that a field value exists in a given database table and column.
16
+ Non-blocking async query executed against the active transaction or DatabasePool.
17
+ """
18
+
19
+ requires_db: bool = True
20
+
21
+ def __init__(
22
+ self,
23
+ table: str,
24
+ column: str,
25
+ pool: DatabasePool | None = None,
26
+ ) -> None:
27
+ self.table = validate_identifier(table, param_name="table")
28
+ self.column = validate_identifier(column, param_name="column")
29
+ self.pool = pool
30
+
31
+ async def validate(self, field: str, value: Any, data: dict[str, Any]) -> bool:
32
+ if value is None or value == "":
33
+ return True
34
+
35
+ query = f'SELECT 1 FROM "{self.table}" WHERE "{self.column}" = $1 LIMIT 1'
36
+
37
+ try:
38
+ active_conn = get_active_connection()
39
+ if active_conn is not None:
40
+ if hasattr(active_conn, "transaction"):
41
+ async with active_conn.transaction():
42
+ row = await active_conn.fetchval(query, value)
43
+ else:
44
+ row = await active_conn.fetchval(query, value)
45
+ return row is not None
46
+
47
+ active_pool = self.pool
48
+ if active_pool is None:
49
+ try:
50
+ container = get_container()
51
+ if container.has(DatabasePool):
52
+ active_pool = container.resolve(DatabasePool)
53
+ except RuntimeError:
54
+ pass
55
+
56
+ if active_pool is None:
57
+ raise RuntimeError(
58
+ "DatabasePool is required for ExistsRule validation. Ensure it is registered in the container or passed explicitly."
59
+ )
60
+
61
+ db = active_pool.get_pool()
62
+ async with db.acquire() as conn:
63
+ row = await conn.fetchval(query, value)
64
+
65
+ return row is not None
66
+ except (asyncpg.DataError, ValueError, TypeError):
67
+ return False
68
+
69
+ def message(self, field: str) -> str:
70
+ return f"The selected {field} is invalid."
71
+
72
+
73
+ def exists(
74
+ table: str,
75
+ column: str,
76
+ pool: DatabasePool | None = None,
77
+ ) -> ExistsRule:
78
+ """Helper to instantiate ExistsRule."""
79
+ return ExistsRule(
80
+ table=table,
81
+ column=column,
82
+ pool=pool,
83
+ )
84
+
85
+
86
+ __all__ = ["ExistsRule", "exists"]