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,435 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import asyncpg
|
|
11
|
+
|
|
12
|
+
from swiftpy.core.container import get_container
|
|
13
|
+
from swiftpy.database.identifiers import validate_identifier
|
|
14
|
+
from swiftpy.database.migrations.blueprint import (
|
|
15
|
+
_UNSET,
|
|
16
|
+
ALLOWED_COLUMN_TYPES,
|
|
17
|
+
Blueprint,
|
|
18
|
+
ColumnDefinition,
|
|
19
|
+
DefaultRaw,
|
|
20
|
+
_validate_column_length,
|
|
21
|
+
_validate_decimal_params,
|
|
22
|
+
raw,
|
|
23
|
+
)
|
|
24
|
+
from swiftpy.database.pool import DatabasePool
|
|
25
|
+
from swiftpy.database.transaction import get_active_connection
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SchemaGrammar:
|
|
29
|
+
"""Translates a Blueprint into PostgreSQL Data Definition Language (DDL) statements."""
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def compile_create(cls, blueprint: Blueprint) -> list[str]:
|
|
33
|
+
table = validate_identifier(blueprint.table)
|
|
34
|
+
columns_sql: list[str] = []
|
|
35
|
+
constraints_sql: list[str] = []
|
|
36
|
+
post_statements: list[str] = []
|
|
37
|
+
|
|
38
|
+
# 1. Compile columns
|
|
39
|
+
for col in blueprint.columns:
|
|
40
|
+
col_sql = cls._compile_column(col)
|
|
41
|
+
columns_sql.append(col_sql)
|
|
42
|
+
|
|
43
|
+
# Standalone index on column
|
|
44
|
+
if col.is_index:
|
|
45
|
+
idx_name = validate_identifier(
|
|
46
|
+
col.index_name or f"idx_{table}_{col.name}"
|
|
47
|
+
)
|
|
48
|
+
post_statements.append(
|
|
49
|
+
f'CREATE INDEX "{idx_name}" ON "{table}" ("{col.name}");'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Standalone named unique constraint on column
|
|
53
|
+
if col.is_unique and col.index_name:
|
|
54
|
+
uq_col_name = validate_identifier(col.index_name)
|
|
55
|
+
constraints_sql.append(
|
|
56
|
+
f'CONSTRAINT "{uq_col_name}" UNIQUE ("{col.name}")'
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Column comments
|
|
60
|
+
post_statements.extend(cls._compile_comment(table, col))
|
|
61
|
+
|
|
62
|
+
# 2. Compile table-level primary key (e.g. composite primary key)
|
|
63
|
+
if blueprint.primary_columns:
|
|
64
|
+
pk_name = validate_identifier(blueprint.primary_key_name or f"pk_{table}")
|
|
65
|
+
cols_joined = ", ".join(f'"{c}"' for c in blueprint.primary_columns)
|
|
66
|
+
constraints_sql.append(
|
|
67
|
+
f'CONSTRAINT "{pk_name}" PRIMARY KEY ({cols_joined})'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# 3. Compile table-level unique constraints and indexes
|
|
71
|
+
for idx in blueprint.indexes:
|
|
72
|
+
cols_joined = ", ".join(f'"{c}"' for c in idx.columns)
|
|
73
|
+
name_suffix = "_".join(idx.columns)
|
|
74
|
+
|
|
75
|
+
if idx.is_unique:
|
|
76
|
+
uq_name = validate_identifier(idx.name or f"uq_{table}_{name_suffix}")
|
|
77
|
+
constraints_sql.append(f'CONSTRAINT "{uq_name}" UNIQUE ({cols_joined})')
|
|
78
|
+
else:
|
|
79
|
+
idx_name = validate_identifier(idx.name or f"idx_{table}_{name_suffix}")
|
|
80
|
+
post_statements.append(
|
|
81
|
+
f'CREATE INDEX "{idx_name}" ON "{table}" ({cols_joined});'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 4. Compile foreign key constraints
|
|
85
|
+
for fk in blueprint.foreign_keys:
|
|
86
|
+
if not fk.ref_table:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Foreign key on {table}.{fk.column} must specify a referenced table via .on('table_name')"
|
|
89
|
+
)
|
|
90
|
+
fk_name = validate_identifier(
|
|
91
|
+
fk.name or f"fk_{table}_{fk.column}_{fk.ref_table}"
|
|
92
|
+
)
|
|
93
|
+
constraints_sql.append(
|
|
94
|
+
f'CONSTRAINT "{fk_name}" FOREIGN KEY ("{fk.column}") '
|
|
95
|
+
f'REFERENCES "{fk.ref_table}" ("{fk.ref_column}") '
|
|
96
|
+
f"ON DELETE {fk.on_delete_action} ON UPDATE {fk.on_update_action}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
definitions = columns_sql + constraints_sql
|
|
100
|
+
create_sql = f'CREATE TABLE "{table}" (\n '
|
|
101
|
+
create_sql += ",\n ".join(definitions)
|
|
102
|
+
create_sql += "\n);"
|
|
103
|
+
|
|
104
|
+
return [create_sql, *post_statements]
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def compile_table(cls, blueprint: Blueprint) -> list[str]:
|
|
108
|
+
table = validate_identifier(blueprint.table)
|
|
109
|
+
statements: list[str] = []
|
|
110
|
+
|
|
111
|
+
# 1. Drops first, so one blueprint can replace what it removes
|
|
112
|
+
for name in blueprint.drop_foreign_list:
|
|
113
|
+
safe_name = validate_identifier(name)
|
|
114
|
+
statements.append(
|
|
115
|
+
f'ALTER TABLE "{table}" DROP CONSTRAINT IF EXISTS "{safe_name}";'
|
|
116
|
+
)
|
|
117
|
+
for name in blueprint.drop_unique_list:
|
|
118
|
+
safe_name = validate_identifier(name)
|
|
119
|
+
statements.append(
|
|
120
|
+
f'ALTER TABLE "{table}" DROP CONSTRAINT IF EXISTS "{safe_name}";'
|
|
121
|
+
)
|
|
122
|
+
for name in blueprint.drop_indexes_list:
|
|
123
|
+
safe_name = validate_identifier(name)
|
|
124
|
+
statements.append(f'DROP INDEX IF EXISTS "{safe_name}";')
|
|
125
|
+
for name in blueprint.drop_columns_list:
|
|
126
|
+
safe_name = validate_identifier(name)
|
|
127
|
+
statements.append(
|
|
128
|
+
f'ALTER TABLE "{table}" DROP COLUMN IF EXISTS "{safe_name}";'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# 2. Renames
|
|
132
|
+
for old_name, new_name in blueprint.rename_columns_map.items():
|
|
133
|
+
safe_old = validate_identifier(old_name)
|
|
134
|
+
safe_new = validate_identifier(new_name)
|
|
135
|
+
statements.append(
|
|
136
|
+
f'ALTER TABLE "{table}" RENAME COLUMN "{safe_old}" TO "{safe_new}";'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# 3. Adds
|
|
140
|
+
for col in blueprint.columns:
|
|
141
|
+
statements.append(
|
|
142
|
+
f'ALTER TABLE "{table}" ADD COLUMN {cls._compile_column(col)};'
|
|
143
|
+
)
|
|
144
|
+
if col.is_index:
|
|
145
|
+
idx_name = validate_identifier(
|
|
146
|
+
col.index_name or f"idx_{table}_{col.name}"
|
|
147
|
+
)
|
|
148
|
+
statements.append(
|
|
149
|
+
f'CREATE INDEX "{idx_name}" ON "{table}" ("{col.name}");'
|
|
150
|
+
)
|
|
151
|
+
statements.extend(cls._compile_comment(table, col))
|
|
152
|
+
|
|
153
|
+
for idx in blueprint.indexes:
|
|
154
|
+
cols_joined = ", ".join(f'"{c}"' for c in idx.columns)
|
|
155
|
+
suffix = "_".join(idx.columns)
|
|
156
|
+
if idx.is_unique:
|
|
157
|
+
name = validate_identifier(idx.name or f"uq_{table}_{suffix}")
|
|
158
|
+
statements.append(
|
|
159
|
+
f'ALTER TABLE "{table}" ADD CONSTRAINT "{name}" UNIQUE ({cols_joined});'
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
name = validate_identifier(idx.name or f"idx_{table}_{suffix}")
|
|
163
|
+
statements.append(
|
|
164
|
+
f'CREATE INDEX "{name}" ON "{table}" ({cols_joined});'
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
for fk in blueprint.foreign_keys:
|
|
168
|
+
if not fk.ref_table:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"Foreign key on {table}.{fk.column} must specify a referenced table via .on('table_name')"
|
|
171
|
+
)
|
|
172
|
+
fk_name = validate_identifier(
|
|
173
|
+
fk.name or f"fk_{table}_{fk.column}_{fk.ref_table}"
|
|
174
|
+
)
|
|
175
|
+
statements.append(
|
|
176
|
+
f'ALTER TABLE "{table}" ADD CONSTRAINT "{fk_name}" '
|
|
177
|
+
f'FOREIGN KEY ("{fk.column}") REFERENCES "{fk.ref_table}" ("{fk.ref_column}") '
|
|
178
|
+
f"ON DELETE {fk.on_delete_action} ON UPDATE {fk.on_update_action};"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return statements
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def compile_drop(cls, table: str) -> str:
|
|
185
|
+
return f'DROP TABLE "{validate_identifier(table)}";'
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def compile_drop_if_exists(cls, table: str) -> str:
|
|
189
|
+
return f'DROP TABLE IF EXISTS "{validate_identifier(table)}";'
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def _compile_column(cls, col: ColumnDefinition) -> str:
|
|
193
|
+
col_name = validate_identifier(col.name)
|
|
194
|
+
parts: list[str] = [f'"{col_name}"']
|
|
195
|
+
t = col.type_name.strip().lower() if isinstance(col.type_name, str) else ""
|
|
196
|
+
if t not in ALLOWED_COLUMN_TYPES:
|
|
197
|
+
raise ValueError(f"Unsupported column type: {col.type_name!r}")
|
|
198
|
+
|
|
199
|
+
# Type mapping
|
|
200
|
+
if t == "id" or t == "big_increments":
|
|
201
|
+
parts.append("BIGSERIAL PRIMARY KEY")
|
|
202
|
+
return " ".join(parts)
|
|
203
|
+
elif t == "increments":
|
|
204
|
+
parts.append("SERIAL PRIMARY KEY")
|
|
205
|
+
return " ".join(parts)
|
|
206
|
+
elif t == "uuid":
|
|
207
|
+
parts.append("UUID")
|
|
208
|
+
elif t == "string":
|
|
209
|
+
validated_length = _validate_column_length(col.length)
|
|
210
|
+
parts.append(
|
|
211
|
+
f"VARCHAR({validated_length})"
|
|
212
|
+
if validated_length is not None
|
|
213
|
+
else "VARCHAR"
|
|
214
|
+
)
|
|
215
|
+
elif t == "text":
|
|
216
|
+
parts.append("TEXT")
|
|
217
|
+
elif t == "integer":
|
|
218
|
+
parts.append("INTEGER")
|
|
219
|
+
elif t == "big_integer":
|
|
220
|
+
parts.append("BIGINT")
|
|
221
|
+
elif t == "small_integer":
|
|
222
|
+
parts.append("SMALLINT")
|
|
223
|
+
elif t == "boolean":
|
|
224
|
+
parts.append("BOOLEAN")
|
|
225
|
+
elif t == "decimal":
|
|
226
|
+
raw_precision = 8 if col.precision is None else col.precision
|
|
227
|
+
raw_scale = 2 if col.scale is None else col.scale
|
|
228
|
+
precision, scale = _validate_decimal_params(raw_precision, raw_scale)
|
|
229
|
+
parts.append(f"NUMERIC({precision}, {scale})")
|
|
230
|
+
elif t == "float":
|
|
231
|
+
parts.append("DOUBLE PRECISION")
|
|
232
|
+
elif t == "json":
|
|
233
|
+
parts.append("JSON")
|
|
234
|
+
elif t == "jsonb":
|
|
235
|
+
parts.append("JSONB")
|
|
236
|
+
elif t == "date":
|
|
237
|
+
parts.append("DATE")
|
|
238
|
+
elif t == "time":
|
|
239
|
+
parts.append("TIME")
|
|
240
|
+
elif t == "timestamp":
|
|
241
|
+
parts.append("TIMESTAMP")
|
|
242
|
+
elif t == "timestamptz":
|
|
243
|
+
parts.append("TIMESTAMPTZ")
|
|
244
|
+
elif t == "vector":
|
|
245
|
+
raw_dim = getattr(col, "dimensions", None)
|
|
246
|
+
dim = 1536 if raw_dim is None else raw_dim
|
|
247
|
+
if isinstance(dim, bool) or not isinstance(dim, int) or dim <= 0:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"Vector dimensions must be a positive integer, got {raw_dim!r}"
|
|
250
|
+
)
|
|
251
|
+
parts.append(f"vector({dim})")
|
|
252
|
+
|
|
253
|
+
# Primary key
|
|
254
|
+
if col.is_primary:
|
|
255
|
+
parts.append("PRIMARY KEY")
|
|
256
|
+
|
|
257
|
+
# Nullability
|
|
258
|
+
if col.is_nullable:
|
|
259
|
+
parts.append("NULL")
|
|
260
|
+
elif not col.is_primary:
|
|
261
|
+
parts.append("NOT NULL")
|
|
262
|
+
|
|
263
|
+
# Default value
|
|
264
|
+
if col.default_val is not _UNSET:
|
|
265
|
+
default_expr = cls._format_default_value(col.default_val)
|
|
266
|
+
parts.append(f"DEFAULT {default_expr}")
|
|
267
|
+
|
|
268
|
+
# Unique (if not named; named unique is emitted in constraints_sql)
|
|
269
|
+
if col.is_unique and not col.index_name:
|
|
270
|
+
parts.append("UNIQUE")
|
|
271
|
+
|
|
272
|
+
# Unsigned check
|
|
273
|
+
if col.is_unsigned:
|
|
274
|
+
parts.append(f'CHECK ("{col_name}" >= 0)')
|
|
275
|
+
|
|
276
|
+
return " ".join(parts)
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _compile_comment(table: str, col: ColumnDefinition) -> list[str]:
|
|
280
|
+
if col.comment_text is None:
|
|
281
|
+
return []
|
|
282
|
+
escaped = col.comment_text.replace("'", "''")
|
|
283
|
+
return [f'COMMENT ON COLUMN "{table}"."{col.name}" IS \'{escaped}\';']
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def _format_default_value(cls, val: Any) -> str:
|
|
287
|
+
if val is None:
|
|
288
|
+
return "NULL"
|
|
289
|
+
if isinstance(val, bool):
|
|
290
|
+
return "TRUE" if val else "FALSE"
|
|
291
|
+
if isinstance(val, int):
|
|
292
|
+
return str(val)
|
|
293
|
+
if isinstance(val, float):
|
|
294
|
+
if not math.isfinite(val):
|
|
295
|
+
raise ValueError(f"Default float value must be finite, got {val!r}")
|
|
296
|
+
return str(val)
|
|
297
|
+
if isinstance(val, DefaultRaw):
|
|
298
|
+
return val.sql
|
|
299
|
+
if hasattr(val, "sql") and isinstance(val.sql, str):
|
|
300
|
+
return str(val.sql).strip()
|
|
301
|
+
if isinstance(val, (dict, list)):
|
|
302
|
+
json_escaped = json.dumps(val).replace("'", "''")
|
|
303
|
+
return f"'{json_escaped}'"
|
|
304
|
+
if isinstance(val, datetime):
|
|
305
|
+
iso_escaped = val.isoformat().replace("'", "''")
|
|
306
|
+
return f"'{iso_escaped}'"
|
|
307
|
+
if isinstance(val, str):
|
|
308
|
+
# Legacy magic PostgreSQL functions/keywords (deprecated in favor of raw(...))
|
|
309
|
+
upper = val.strip().upper()
|
|
310
|
+
if upper in {
|
|
311
|
+
"CURRENT_TIMESTAMP",
|
|
312
|
+
"CURRENT_DATE",
|
|
313
|
+
"CURRENT_TIME",
|
|
314
|
+
"NOW()",
|
|
315
|
+
"GEN_RANDOM_UUID()",
|
|
316
|
+
"TRUE",
|
|
317
|
+
"FALSE",
|
|
318
|
+
"NULL",
|
|
319
|
+
}:
|
|
320
|
+
warnings.warn(
|
|
321
|
+
f"Passing SQL expression {val!r} as a string to .default() is deprecated; "
|
|
322
|
+
f"use raw({val!r}) instead.",
|
|
323
|
+
DeprecationWarning,
|
|
324
|
+
stacklevel=2,
|
|
325
|
+
)
|
|
326
|
+
return val.strip()
|
|
327
|
+
# String literal: escape single quotes
|
|
328
|
+
escaped = val.replace("'", "''")
|
|
329
|
+
return f"'{escaped}'"
|
|
330
|
+
escaped = str(val).replace("'", "''")
|
|
331
|
+
return f"'{escaped}'"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class Schema:
|
|
335
|
+
"""Schema builder facade for defining and altering database tables."""
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
async def _get_connection(cls) -> tuple[asyncpg.Connection, bool]:
|
|
339
|
+
"""Returns the active transaction connection or acquires a new one from the pool."""
|
|
340
|
+
tx_conn = get_active_connection()
|
|
341
|
+
if tx_conn is not None:
|
|
342
|
+
return tx_conn, False
|
|
343
|
+
|
|
344
|
+
container = get_container()
|
|
345
|
+
pool = container.resolve(DatabasePool).get_pool()
|
|
346
|
+
conn = await pool.acquire()
|
|
347
|
+
return conn, True
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
async def _execute_statements(cls, statements: list[str]) -> None:
|
|
351
|
+
conn, should_release = await cls._get_connection()
|
|
352
|
+
try:
|
|
353
|
+
async with conn.transaction():
|
|
354
|
+
for sql in statements:
|
|
355
|
+
if sql.strip():
|
|
356
|
+
await conn.execute(sql)
|
|
357
|
+
finally:
|
|
358
|
+
if should_release:
|
|
359
|
+
container = get_container()
|
|
360
|
+
await container.resolve(DatabasePool).get_pool().release(conn)
|
|
361
|
+
|
|
362
|
+
@classmethod
|
|
363
|
+
async def create(cls, table: str, callback: Callable[[Blueprint], Any]) -> None:
|
|
364
|
+
"""Create a new database table using a fluent Blueprint."""
|
|
365
|
+
blueprint = Blueprint(table, is_create=True)
|
|
366
|
+
callback(blueprint)
|
|
367
|
+
statements = SchemaGrammar.compile_create(blueprint)
|
|
368
|
+
await cls._execute_statements(statements)
|
|
369
|
+
|
|
370
|
+
@classmethod
|
|
371
|
+
async def table(cls, table: str, callback: Callable[[Blueprint], Any]) -> None:
|
|
372
|
+
"""Modify an existing database table using a fluent Blueprint."""
|
|
373
|
+
blueprint = Blueprint(table, is_create=False)
|
|
374
|
+
callback(blueprint)
|
|
375
|
+
statements = SchemaGrammar.compile_table(blueprint)
|
|
376
|
+
await cls._execute_statements(statements)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
async def drop(cls, table: str) -> None:
|
|
380
|
+
"""Drop a database table."""
|
|
381
|
+
sql = SchemaGrammar.compile_drop(table)
|
|
382
|
+
await cls._execute_statements([sql])
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
async def drop_if_exists(cls, table: str) -> None:
|
|
386
|
+
"""Drop a database table if it exists."""
|
|
387
|
+
sql = SchemaGrammar.compile_drop_if_exists(table)
|
|
388
|
+
await cls._execute_statements([sql])
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
async def has_table(cls, table: str) -> bool:
|
|
392
|
+
"""Determine whether the given table exists in the database."""
|
|
393
|
+
safe_table = validate_identifier(table)
|
|
394
|
+
conn, should_release = await cls._get_connection()
|
|
395
|
+
try:
|
|
396
|
+
query = """
|
|
397
|
+
SELECT EXISTS (
|
|
398
|
+
SELECT 1
|
|
399
|
+
FROM information_schema.tables
|
|
400
|
+
WHERE table_name = $1
|
|
401
|
+
AND table_schema = current_schema()
|
|
402
|
+
);
|
|
403
|
+
"""
|
|
404
|
+
result = await conn.fetchval(query, safe_table)
|
|
405
|
+
return bool(result)
|
|
406
|
+
finally:
|
|
407
|
+
if should_release:
|
|
408
|
+
container = get_container()
|
|
409
|
+
await container.resolve(DatabasePool).get_pool().release(conn)
|
|
410
|
+
|
|
411
|
+
@classmethod
|
|
412
|
+
async def has_column(cls, table: str, column: str) -> bool:
|
|
413
|
+
"""Determine whether the given column exists on the specified table."""
|
|
414
|
+
safe_table = validate_identifier(table)
|
|
415
|
+
safe_column = validate_identifier(column)
|
|
416
|
+
conn, should_release = await cls._get_connection()
|
|
417
|
+
try:
|
|
418
|
+
query = """
|
|
419
|
+
SELECT EXISTS (
|
|
420
|
+
SELECT 1
|
|
421
|
+
FROM information_schema.columns
|
|
422
|
+
WHERE table_name = $1
|
|
423
|
+
AND column_name = $2
|
|
424
|
+
AND table_schema = current_schema()
|
|
425
|
+
);
|
|
426
|
+
"""
|
|
427
|
+
result = await conn.fetchval(query, safe_table, safe_column)
|
|
428
|
+
return bool(result)
|
|
429
|
+
finally:
|
|
430
|
+
if should_release:
|
|
431
|
+
container = get_container()
|
|
432
|
+
await container.resolve(DatabasePool).get_pool().release(conn)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
__all__ = ["Schema", "SchemaGrammar", "raw"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from swiftpy.database.orm.fields import Field, FieldInfo, Vector, VectorFieldInfo
|
|
4
|
+
from swiftpy.database.orm.model import Model, ModelQueryBuilder
|
|
5
|
+
from swiftpy.database.orm.relationships import (
|
|
6
|
+
BelongsTo,
|
|
7
|
+
BelongsToMany,
|
|
8
|
+
HasMany,
|
|
9
|
+
HasOne,
|
|
10
|
+
LazyLoadingDisabledError,
|
|
11
|
+
Relation,
|
|
12
|
+
RelationDescriptor,
|
|
13
|
+
RelationNotLoadedError,
|
|
14
|
+
RelationshipQueryBuilder,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BelongsTo",
|
|
19
|
+
"BelongsToMany",
|
|
20
|
+
"Field",
|
|
21
|
+
"FieldInfo",
|
|
22
|
+
"HasMany",
|
|
23
|
+
"HasOne",
|
|
24
|
+
"LazyLoadingDisabledError",
|
|
25
|
+
"Model",
|
|
26
|
+
"ModelQueryBuilder",
|
|
27
|
+
"Relation",
|
|
28
|
+
"RelationDescriptor",
|
|
29
|
+
"RelationNotLoadedError",
|
|
30
|
+
"RelationshipQueryBuilder",
|
|
31
|
+
"Vector",
|
|
32
|
+
"VectorFieldInfo",
|
|
33
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
_MISSING = object()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FieldInfo:
|
|
10
|
+
"""Represents column metadata and configuration on an Active Record Model."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
default: Any = _MISSING,
|
|
15
|
+
*,
|
|
16
|
+
default_factory: Callable[[], Any] | None = None,
|
|
17
|
+
primary_key: bool = False,
|
|
18
|
+
nullable: bool = True,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.default = default
|
|
21
|
+
self.default_factory = default_factory
|
|
22
|
+
self.primary_key = primary_key
|
|
23
|
+
self.nullable = nullable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VectorFieldInfo(FieldInfo):
|
|
27
|
+
"""Represents a vector column for semantic search and embeddings on an Active Record Model."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
dimensions: int,
|
|
32
|
+
default: Any = _MISSING,
|
|
33
|
+
*,
|
|
34
|
+
metric: str = "cosine",
|
|
35
|
+
default_factory: Callable[[], Any] | None = None,
|
|
36
|
+
primary_key: bool = False,
|
|
37
|
+
nullable: bool = True,
|
|
38
|
+
) -> None:
|
|
39
|
+
if isinstance(dimensions, bool) or not isinstance(dimensions, int) or dimensions <= 0:
|
|
40
|
+
raise ValueError(f"Vector dimensions must be a positive integer, got {dimensions}")
|
|
41
|
+
allowed_metrics = {"cosine", "l2", "inner_product"}
|
|
42
|
+
if metric not in allowed_metrics:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Unsupported vector distance metric '{metric}'. Expected one of {allowed_metrics}."
|
|
45
|
+
)
|
|
46
|
+
super().__init__(
|
|
47
|
+
default=default,
|
|
48
|
+
default_factory=default_factory,
|
|
49
|
+
primary_key=primary_key,
|
|
50
|
+
nullable=nullable,
|
|
51
|
+
)
|
|
52
|
+
self.dimensions = dimensions
|
|
53
|
+
self.metric = metric
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def Field(
|
|
57
|
+
default: Any = _MISSING,
|
|
58
|
+
*,
|
|
59
|
+
default_factory: Callable[[], Any] | None = None,
|
|
60
|
+
primary_key: bool = False,
|
|
61
|
+
nullable: bool = True,
|
|
62
|
+
) -> Any:
|
|
63
|
+
"""Declare column metadata, default value, or default_factory on an Active Record Model."""
|
|
64
|
+
return FieldInfo(
|
|
65
|
+
default=default,
|
|
66
|
+
default_factory=default_factory,
|
|
67
|
+
primary_key=primary_key,
|
|
68
|
+
nullable=nullable,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def Vector(
|
|
73
|
+
dimensions: int,
|
|
74
|
+
default: Any = _MISSING,
|
|
75
|
+
*,
|
|
76
|
+
metric: str = "cosine",
|
|
77
|
+
default_factory: Callable[[], Any] | None = None,
|
|
78
|
+
primary_key: bool = False,
|
|
79
|
+
nullable: bool = True,
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Declare a vector embedding column with fixed dimensionality on an Active Record Model."""
|
|
82
|
+
return VectorFieldInfo(
|
|
83
|
+
dimensions=dimensions,
|
|
84
|
+
default=default,
|
|
85
|
+
metric=metric,
|
|
86
|
+
default_factory=default_factory,
|
|
87
|
+
primary_key=primary_key,
|
|
88
|
+
nullable=nullable,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = ["Field", "FieldInfo", "Vector", "VectorFieldInfo"]
|