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,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
|
+
]
|