core-framework 0.12.1__py3-none-any.whl → 0.12.3__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.
- core/__init__.py +66 -2
- core/app.py +65 -3
- core/auth/__init__.py +27 -2
- core/auth/base.py +146 -0
- core/auth/middleware.py +355 -0
- core/auth/models.py +138 -24
- core/auth/schemas.py +5 -1
- core/auth/views.py +168 -50
- core/config.py +27 -0
- core/middleware.py +779 -0
- core/migrations/engine.py +68 -2
- core/migrations/operations.py +88 -10
- core/views.py +453 -28
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/METADATA +1 -1
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/RECORD +17 -15
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/WHEEL +0 -0
- {core_framework-0.12.1.dist-info → core_framework-0.12.3.dist-info}/entry_points.txt +0 -0
core/migrations/engine.py
CHANGED
|
@@ -255,6 +255,71 @@ class MigrationEngine:
|
|
|
255
255
|
index=col.index,
|
|
256
256
|
)
|
|
257
257
|
|
|
258
|
+
def _topological_sort_tables(self, tables: list[TableState]) -> list[TableState]:
|
|
259
|
+
"""
|
|
260
|
+
Bug #5 Fix: Ordena tabelas em ordem topológica baseada em FKs.
|
|
261
|
+
|
|
262
|
+
Tabelas referenciadas por FKs são criadas ANTES das que as referenciam.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
tables: Lista de TableState a ordenar
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Lista ordenada topologicamente
|
|
269
|
+
"""
|
|
270
|
+
if not tables:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
# Mapeia nome -> tabela
|
|
274
|
+
table_map = {t.name: t for t in tables}
|
|
275
|
+
|
|
276
|
+
# Constrói grafo de dependências
|
|
277
|
+
# dependencias[A] = {B, C} significa A depende de B e C (A tem FK para B e C)
|
|
278
|
+
dependencies: dict[str, set[str]] = {t.name: set() for t in tables}
|
|
279
|
+
|
|
280
|
+
for table in tables:
|
|
281
|
+
for fk in table.foreign_keys:
|
|
282
|
+
ref_table = fk.references_table
|
|
283
|
+
# Só adiciona dependência se a tabela referenciada também está sendo criada
|
|
284
|
+
# (tabelas já existentes não precisam ser consideradas)
|
|
285
|
+
if ref_table in table_map:
|
|
286
|
+
dependencies[table.name].add(ref_table)
|
|
287
|
+
|
|
288
|
+
# Algoritmo de Kahn para ordenação topológica
|
|
289
|
+
result = []
|
|
290
|
+
|
|
291
|
+
# Encontra tabelas sem dependências (grau de entrada 0)
|
|
292
|
+
no_deps = [name for name, deps in dependencies.items() if not deps]
|
|
293
|
+
|
|
294
|
+
while no_deps:
|
|
295
|
+
# Remove uma tabela sem dependências
|
|
296
|
+
name = no_deps.pop(0)
|
|
297
|
+
result.append(table_map[name])
|
|
298
|
+
|
|
299
|
+
# Remove esta tabela das dependências de outras
|
|
300
|
+
for other_name, deps in dependencies.items():
|
|
301
|
+
if name in deps:
|
|
302
|
+
deps.remove(name)
|
|
303
|
+
# Se não tem mais dependências, adiciona à fila
|
|
304
|
+
if not deps and other_name not in [t.name for t in result]:
|
|
305
|
+
no_deps.append(other_name)
|
|
306
|
+
|
|
307
|
+
# Verifica ciclo (se sobrou alguma tabela com dependências)
|
|
308
|
+
remaining = [t for t in tables if t not in result]
|
|
309
|
+
if remaining:
|
|
310
|
+
# Há um ciclo - adiciona as tabelas restantes no final
|
|
311
|
+
# (o banco vai falhar, mas pelo menos o erro será claro)
|
|
312
|
+
import warnings
|
|
313
|
+
cycle_tables = [t.name for t in remaining]
|
|
314
|
+
warnings.warn(
|
|
315
|
+
f"Circular FK dependency detected involving tables: {cycle_tables}. "
|
|
316
|
+
"Migration may fail. Consider breaking the cycle with nullable FKs.",
|
|
317
|
+
RuntimeWarning,
|
|
318
|
+
)
|
|
319
|
+
result.extend(remaining)
|
|
320
|
+
|
|
321
|
+
return result
|
|
322
|
+
|
|
258
323
|
def _diff_to_operations(self, diff: SchemaDiff) -> list[Operation]:
|
|
259
324
|
"""Converte SchemaDiff em lista de operações."""
|
|
260
325
|
from core.migrations.operations import CreateEnum, DropEnum, AlterEnum
|
|
@@ -284,8 +349,9 @@ class MigrationEngine:
|
|
|
284
349
|
new_values=new_enum.values,
|
|
285
350
|
))
|
|
286
351
|
|
|
287
|
-
# 3. Criar tabelas
|
|
288
|
-
|
|
352
|
+
# 3. Criar tabelas (BUG #5 FIX: em ordem topológica)
|
|
353
|
+
sorted_tables = self._topological_sort_tables(diff.tables_to_create)
|
|
354
|
+
for table in sorted_tables:
|
|
289
355
|
columns = [self._column_state_to_def(col) for col in table.columns.values()]
|
|
290
356
|
foreign_keys = [
|
|
291
357
|
ForeignKeyDef(
|
core/migrations/operations.py
CHANGED
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from abc import ABC, abstractmethod
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Any, Callable, TYPE_CHECKING
|
|
12
|
+
from typing import Any, Callable, ClassVar, TYPE_CHECKING
|
|
13
13
|
from collections.abc import Awaitable
|
|
14
14
|
|
|
15
15
|
from sqlalchemy import text
|
|
@@ -31,9 +31,90 @@ class ColumnDef:
|
|
|
31
31
|
unique: bool = False
|
|
32
32
|
index: bool = False
|
|
33
33
|
|
|
34
|
+
# Mapeamento de tipos genéricos para tipos específicos de cada dialeto
|
|
35
|
+
# Bug #1: DATETIME → TIMESTAMP para PostgreSQL
|
|
36
|
+
TYPE_MAPPING: ClassVar[dict[str, dict[str, str]]] = {
|
|
37
|
+
"postgresql": {
|
|
38
|
+
"DATETIME": "TIMESTAMP WITH TIME ZONE",
|
|
39
|
+
"TIMESTAMP": "TIMESTAMP WITH TIME ZONE",
|
|
40
|
+
"BOOLEAN": "BOOLEAN",
|
|
41
|
+
"TINYINT": "SMALLINT",
|
|
42
|
+
"LONGTEXT": "TEXT",
|
|
43
|
+
"DOUBLE": "DOUBLE PRECISION",
|
|
44
|
+
},
|
|
45
|
+
"mysql": {
|
|
46
|
+
"DATETIME": "DATETIME",
|
|
47
|
+
"TIMESTAMP": "TIMESTAMP",
|
|
48
|
+
"BOOLEAN": "TINYINT(1)",
|
|
49
|
+
"TEXT": "LONGTEXT",
|
|
50
|
+
"UUID": "CHAR(36)",
|
|
51
|
+
},
|
|
52
|
+
"sqlite": {
|
|
53
|
+
"DATETIME": "DATETIME",
|
|
54
|
+
"TIMESTAMP": "DATETIME",
|
|
55
|
+
"BOOLEAN": "BOOLEAN",
|
|
56
|
+
"UUID": "TEXT",
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _get_dialect_type(self, dialect: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Converte tipo genérico para tipo específico do dialeto.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
dialect: Nome do dialeto (postgresql, mysql, sqlite)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tipo SQL específico para o dialeto
|
|
69
|
+
"""
|
|
70
|
+
# Extrai tipo base (sem parâmetros como VARCHAR(255))
|
|
71
|
+
base_type = self.type.split("(")[0].upper()
|
|
72
|
+
|
|
73
|
+
# Verifica se há mapeamento específico
|
|
74
|
+
dialect_mapping = self.TYPE_MAPPING.get(dialect, {})
|
|
75
|
+
mapped_type = dialect_mapping.get(base_type)
|
|
76
|
+
|
|
77
|
+
if mapped_type:
|
|
78
|
+
# Se o tipo original tinha parâmetros, tenta preservá-los
|
|
79
|
+
if "(" in self.type and "(" not in mapped_type:
|
|
80
|
+
# Tipo como VARCHAR(255) mantém os parâmetros
|
|
81
|
+
return self.type
|
|
82
|
+
return mapped_type
|
|
83
|
+
|
|
84
|
+
return self.type
|
|
85
|
+
|
|
86
|
+
def _get_default_sql(self, dialect: str) -> str | None:
|
|
87
|
+
"""
|
|
88
|
+
Gera SQL para valor default considerando o dialeto.
|
|
89
|
+
|
|
90
|
+
Bug #2: Boolean defaults usando TRUE/FALSE para PostgreSQL
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
dialect: Nome do dialeto
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
String SQL para o default ou None
|
|
97
|
+
"""
|
|
98
|
+
if self.default is None:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
if isinstance(self.default, str):
|
|
102
|
+
return f"DEFAULT '{self.default}'"
|
|
103
|
+
|
|
104
|
+
if isinstance(self.default, bool):
|
|
105
|
+
# PostgreSQL exige TRUE/FALSE em vez de 1/0
|
|
106
|
+
if dialect == "postgresql":
|
|
107
|
+
return f"DEFAULT {'TRUE' if self.default else 'FALSE'}"
|
|
108
|
+
else:
|
|
109
|
+
return f"DEFAULT {1 if self.default else 0}"
|
|
110
|
+
|
|
111
|
+
return f"DEFAULT {self.default}"
|
|
112
|
+
|
|
34
113
|
def to_sql(self, dialect: str = "sqlite") -> str:
|
|
35
|
-
"""Gera SQL para a coluna."""
|
|
36
|
-
|
|
114
|
+
"""Gera SQL para a coluna, adaptado ao dialeto do banco."""
|
|
115
|
+
# Obtém tipo adaptado ao dialeto
|
|
116
|
+
col_type = self._get_dialect_type(dialect)
|
|
117
|
+
parts = [f'"{self.name}"', col_type]
|
|
37
118
|
|
|
38
119
|
if self.primary_key:
|
|
39
120
|
parts.append("PRIMARY KEY")
|
|
@@ -49,13 +130,10 @@ class ColumnDef:
|
|
|
49
130
|
if not self.nullable and not self.primary_key:
|
|
50
131
|
parts.append("NOT NULL")
|
|
51
132
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
parts.append(f"DEFAULT {1 if self.default else 0}")
|
|
57
|
-
else:
|
|
58
|
-
parts.append(f"DEFAULT {self.default}")
|
|
133
|
+
# Gera default adaptado ao dialeto
|
|
134
|
+
default_sql = self._get_default_sql(dialect)
|
|
135
|
+
if default_sql:
|
|
136
|
+
parts.append(default_sql)
|
|
59
137
|
|
|
60
138
|
if self.unique and not self.primary_key:
|
|
61
139
|
parts.append("UNIQUE")
|