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