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.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- 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"]
|