core-framework 0.12.2__py3-none-any.whl → 0.12.4__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 CHANGED
@@ -278,7 +278,7 @@ from core.exceptions import (
278
278
  MissingDependency,
279
279
  )
280
280
 
281
- __version__ = "0.12.2"
281
+ __version__ = "0.12.4"
282
282
  __all__ = [
283
283
  # Models
284
284
  "Model",
core/auth/middleware.py CHANGED
@@ -182,26 +182,65 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
182
182
  if User is None:
183
183
  return None
184
184
 
185
- # Get database session
186
- from core.database import get_read_session
185
+ # Bug #3 & #4 Fix: Get database session correctly
186
+ # The middleware runs outside FastAPI DI context, so we need to
187
+ # handle database session creation carefully
188
+ db = await self._get_db_session()
189
+ if db is None:
190
+ return None
187
191
 
188
- async for db in get_read_session():
189
- try:
190
- # Convert user_id to correct type
191
- user_id_converted = self._convert_user_id(user_id, User)
192
-
193
- user = await User.objects.using(db).filter(id=user_id_converted).first()
194
-
195
- if user is None:
196
- return None
197
-
198
- # Check if user is active
199
- if hasattr(user, "is_active") and not user.is_active:
200
- return None
201
-
202
- return user
203
- except Exception:
192
+ try:
193
+ # Convert user_id to correct type
194
+ user_id_converted = self._convert_user_id(user_id, User)
195
+
196
+ user = await User.objects.using(db).filter(id=user_id_converted).first()
197
+
198
+ if user is None:
204
199
  return None
200
+
201
+ # Check if user is active
202
+ if hasattr(user, "is_active") and not user.is_active:
203
+ return None
204
+
205
+ return user
206
+ except Exception:
207
+ return None
208
+ finally:
209
+ await db.close()
210
+
211
+ async def _get_db_session(self) -> Any | None:
212
+ """
213
+ Get a database session for authentication.
214
+
215
+ Bug #3 & #4 Fix: Handles both initialized and uninitialized database states.
216
+ Creates session directly from engine if normal path fails.
217
+
218
+ Returns:
219
+ AsyncSession or None if database not available
220
+ """
221
+ # Try 1: Use the standard get_read_session (if database is initialized)
222
+ try:
223
+ from core.database import get_read_session, _read_session_factory
224
+
225
+ if _read_session_factory is not None:
226
+ return _read_session_factory()
227
+ except (RuntimeError, ImportError):
228
+ pass
229
+
230
+ # Try 2: Create session from settings (lazy initialization)
231
+ try:
232
+ from core.config import get_settings
233
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
234
+
235
+ settings = get_settings()
236
+ db_url = getattr(settings, 'database_read_url', None) or getattr(settings, 'database_url', None)
237
+
238
+ if db_url:
239
+ engine = create_async_engine(db_url, echo=False)
240
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
241
+ return session_factory()
242
+ except Exception:
243
+ pass
205
244
 
206
245
  return None
207
246
 
core/auth/models.py CHANGED
@@ -27,12 +27,14 @@ Uso:
27
27
  from __future__ import annotations
28
28
 
29
29
  from typing import Any, ClassVar, TYPE_CHECKING
30
+ from uuid import UUID
30
31
 
31
32
  from sqlalchemy import Table, Column, Integer, ForeignKey, inspect
32
33
  from sqlalchemy.orm import Mapped, relationship, declared_attr
33
34
  from sqlalchemy.dialects.postgresql import UUID as PG_UUID
34
35
 
35
36
  from core.models import Model, Field
37
+ from core.fields import AdvancedField
36
38
  from core.auth.base import get_password_hasher, get_auth_config
37
39
  from core.datetime import timezone, DateTime
38
40
 
@@ -56,6 +58,7 @@ def _get_pk_column_type(model_class: type) -> type:
56
58
  Detecta o tipo da coluna PK de um modelo.
57
59
 
58
60
  Bug #3 Fix: Detecção robusta do tipo de PK para FKs.
61
+ Verifica toda a cadeia de herança (MRO) para detectar corretamente.
59
62
 
60
63
  Suporta:
61
64
  - Integer (int)
@@ -69,7 +72,7 @@ def _get_pk_column_type(model_class: type) -> type:
69
72
  from sqlalchemy import Integer, BigInteger, String
70
73
  from sqlalchemy.dialects.postgresql import UUID as PG_UUID
71
74
  from sqlalchemy import Uuid
72
- import uuid
75
+ import uuid as uuid_module
73
76
 
74
77
  # Verifica cache primeiro
75
78
  cache_key = f"{model_class.__module__}.{model_class.__name__}"
@@ -78,7 +81,30 @@ def _get_pk_column_type(model_class: type) -> type:
78
81
 
79
82
  detected_type = Integer # Default
80
83
 
81
- # Método 1: Tenta obter da tabela mapeada
84
+ # Método 0 (MAIS IMPORTANTE): Verifica herança de AbstractUUIDUser
85
+ # Isso é verificado PRIMEIRO porque funciona mesmo durante o mapeamento
86
+ for base in model_class.__mro__:
87
+ base_name = base.__name__
88
+ # Verifica se herda de AbstractUUIDUser ou qualquer classe com UUID no nome
89
+ if base_name == "AbstractUUIDUser":
90
+ detected_type = PG_UUID
91
+ _pk_type_cache[cache_key] = detected_type
92
+ return detected_type
93
+
94
+ # Método 1: Verifica annotations em TODA a cadeia de herança (MRO)
95
+ for base in model_class.__mro__:
96
+ annotations = getattr(base, "__annotations__", {})
97
+ if "id" in annotations:
98
+ ann = annotations["id"]
99
+ ann_str = str(ann)
100
+
101
+ # Detecta UUID (vários formatos)
102
+ if "UUID" in ann_str or "uuid" in ann_str or "Uuid" in ann_str:
103
+ detected_type = PG_UUID
104
+ _pk_type_cache[cache_key] = detected_type
105
+ return detected_type
106
+
107
+ # Método 2: Tenta obter da tabela já mapeada (se existir)
82
108
  if hasattr(model_class, "__table__"):
83
109
  pk_columns = [c for c in model_class.__table__.columns if c.primary_key]
84
110
  if pk_columns:
@@ -101,40 +127,41 @@ def _get_pk_column_type(model_class: type) -> type:
101
127
  _pk_type_cache[cache_key] = detected_type
102
128
  return detected_type
103
129
 
104
- # Método 2: Tenta via annotations
105
- annotations = getattr(model_class, "__annotations__", {})
106
-
107
- if "id" in annotations:
108
- ann = annotations["id"]
109
- ann_str = str(ann)
110
-
111
- # Detecta UUID (vários formatos)
112
- if "UUID" in ann_str or "uuid" in ann_str or "Uuid" in ann_str:
113
- detected_type = PG_UUID
114
- # Detecta int
115
- elif "int" in ann_str.lower() and "uuid" not in ann_str.lower():
116
- detected_type = Integer
117
- # Detecta str
118
- elif "str" in ann_str.lower() and "uuid" not in ann_str.lower():
119
- detected_type = String
120
-
121
- # Método 3: Verifica campos na classe (declared_attr ou column_property)
122
- for attr_name in dir(model_class):
123
- if attr_name == "id":
124
- attr = getattr(model_class, attr_name, None)
130
+ # Método 3: Verifica atributo 'id' diretamente na classe e bases
131
+ for base in model_class.__mro__:
132
+ if hasattr(base, "id"):
133
+ attr = getattr(base, "id", None)
125
134
  if attr is not None:
126
- # Pode ser um InstrumentedAttribute
135
+ # Pode ser um InstrumentedAttribute ou MappedColumn
127
136
  if hasattr(attr, "type"):
128
137
  attr_type = attr.type
129
138
  if isinstance(attr_type, (PG_UUID, Uuid)):
130
139
  detected_type = PG_UUID
131
140
  break
141
+ type_name = type(attr_type).__name__.upper()
142
+ if "UUID" in type_name:
143
+ detected_type = PG_UUID
144
+ break
132
145
  # Pode ser um mapped_column
133
146
  if hasattr(attr, "property") and hasattr(attr.property, "columns"):
134
147
  for col in attr.property.columns:
135
148
  if isinstance(col.type, (PG_UUID, Uuid)):
136
149
  detected_type = PG_UUID
137
150
  break
151
+ type_name = type(col.type).__name__.upper()
152
+ if "UUID" in type_name:
153
+ detected_type = PG_UUID
154
+ break
155
+ # Verifica se é um MappedColumn com tipo definido
156
+ if hasattr(attr, "column") and hasattr(attr.column, "type"):
157
+ col_type = attr.column.type
158
+ if isinstance(col_type, (PG_UUID, Uuid)):
159
+ detected_type = PG_UUID
160
+ break
161
+ type_name = type(col_type).__name__.upper()
162
+ if "UUID" in type_name:
163
+ detected_type = PG_UUID
164
+ break
138
165
 
139
166
  _pk_type_cache[cache_key] = detected_type
140
167
  return detected_type
@@ -790,12 +817,9 @@ class AbstractUUIDUser(AbstractUser):
790
817
 
791
818
  __abstract__ = True
792
819
 
793
- # Importa aqui para evitar circular import
794
- from core.fields import AdvancedField
795
- from uuid import UUID as UUIDType
796
-
797
820
  # Override: usa UUID como PK
798
- id: Mapped[UUIDType] = AdvancedField.uuid_pk()
821
+ # UUID importado no topo do módulo para resolução correta de tipos
822
+ id: Mapped[UUID] = AdvancedField.uuid_pk()
799
823
 
800
824
 
801
825
  # =============================================================================
core/middleware.py CHANGED
@@ -736,11 +736,16 @@ class SecurityHeadersMiddleware(BaseMiddleware):
736
736
  # =============================================================================
737
737
 
738
738
  _builtin_middlewares.update({
739
+ # Pre-built middlewares from this module
739
740
  "timing": "core.middleware.TimingMiddleware",
740
741
  "request_id": "core.middleware.RequestIDMiddleware",
741
742
  "logging": "core.middleware.LoggingMiddleware",
742
743
  "maintenance": "core.middleware.MaintenanceModeMiddleware",
743
744
  "security_headers": "core.middleware.SecurityHeadersMiddleware",
745
+ # Auth middlewares (ensure they're registered even if initial dict failed)
746
+ "auth": "core.auth.middleware.AuthenticationMiddleware",
747
+ "authentication": "core.auth.middleware.AuthenticationMiddleware",
748
+ "optional_auth": "core.auth.middleware.OptionalAuthenticationMiddleware",
744
749
  })
745
750
 
746
751
 
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
- for table in diff.tables_to_create:
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-framework
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema.
5
5
  Project-URL: Homepage, https://github.com/SorPuti/core-framework
6
6
  Project-URL: Documentation, https://github.com/SorPuti/core-framework#readme
@@ -1,4 +1,4 @@
1
- core/__init__.py,sha256=Ka6DK8M7VOnG9iIsEhx17UFdtVRaZGZD7EIttp1-GGE,12058
1
+ core/__init__.py,sha256=LpMfN4s5QgM0suYMegcYK7uf2m8dIH90qOsbM5IWSNs,12058
2
2
  core/app.py,sha256=sCA3mJI696i7MIjrPxfOr5zEYt0njarQfHHy3EAajk4,21071
3
3
  core/choices.py,sha256=rhcL3p2dB7RK99zIilpmoTFVcibQEIaRpz0CY0kImCE,10502
4
4
  core/config.py,sha256=2-MVF9nLoYmxpYYH_Gzn4-Sa3MU87YZskRPtlNyhg6Q,14049
@@ -7,7 +7,7 @@ core/datetime.py,sha256=bzqlAj3foA-lzbhXjlEiDNR2D-nwXu9mpxpdcUb-Pmw,32730
7
7
  core/dependencies.py,sha256=LrNLbmQhXoCPRvoRPRMGNo0w6_l4NLGWeeTHzpUU36M,11582
8
8
  core/exceptions.py,sha256=cdcffeYnMzCbS4hApOYNmPVNbPUpKcrgJbi3nKhqTuI,22702
9
9
  core/fields.py,sha256=F2NdToowkJ_LFvPN9KVyxIFES1AlVDy7WkEp-8UiBpA,9327
10
- core/middleware.py,sha256=kQ8APXerC9fzKHkGhTjrTYfjAvfCpOtQE-IjCPurvxo,23172
10
+ core/middleware.py,sha256=MZVw7smJ2MiycYvkaYIC2cNpyqYGk3m-eeoDandqZU4,23506
11
11
  core/models.py,sha256=jdNdjRPKBZiOBOgg8CmDYBwmuWdD7twCIpqINLGY4Q4,33788
12
12
  core/permissions.py,sha256=HQu_eNBEodJyR50CYcFvdCw9LlEfhI5vJbljV5VIs7M,10162
13
13
  core/querysets.py,sha256=Z87-U06Un_xA9GKwcjXx0yzw6F_xf_tvG_rBT5UGL9c,22678
@@ -22,8 +22,8 @@ core/auth/backends.py,sha256=R-siIE8TrNqDHkCx42zXN1WVvvuWOun1nj8D5elrC9g,10425
22
22
  core/auth/base.py,sha256=Q7vXgwTmgdmyW7G8eJmDket2bKB_8YFnraZ_kK9_gTs,21425
23
23
  core/auth/decorators.py,sha256=tmC7prKUvHuzQ3J872nM6r83DR9d82dCLXKLvUB1Os8,12288
24
24
  core/auth/hashers.py,sha256=0gIf67TU0k5H744FADpyh9_ugxA7m3mhYPZxLh_lEtc,12808
25
- core/auth/middleware.py,sha256=pei2C1uy2WqyJeSGMYBwh0jj4Ong2I0_vmSi2y6F6jA,9425
26
- core/auth/models.py,sha256=qT31MJoHOTbYJPr_KPKn4wCpSVC4vkgBHWFEfL-meG0,32654
25
+ core/auth/middleware.py,sha256=r4F3AIb4k9Z7gbTwcyG-MVWCyGikQqP54IHNKmyNtdc,10963
26
+ core/auth/models.py,sha256=aEE7deQKPS1aH0Btzzh3Z1Bwuqy8zvLZwu4JFEmiUNk,34058
27
27
  core/auth/permissions.py,sha256=v3ykAgNpq5wJ0NkuC_FuveMctOkDfM9Xp11XEnUAuBg,12461
28
28
  core/auth/schemas.py,sha256=L0W96dOD348rJDGeu1K5Rz3aJj-GdwMr2vbwwsYfo2g,3469
29
29
  core/auth/tokens.py,sha256=jk-TnMRdVGPhy6pWqSF2Ef8RTqLrP6Mkuo5GvRQh9no,8489
@@ -61,7 +61,7 @@ core/messaging/redis/producer.py,sha256=F9NA1GpYvN-wdW5Ilzi49rrAmxfBmicXX3l6sABW
61
61
  core/migrations/__init__.py,sha256=OF_7XQ9x9V_BWr3d8vDZk8W5QYT0RO3ZXNFnOg8UgDI,1908
62
62
  core/migrations/analyzer.py,sha256=QiwG_Xf_-Mb-Kp4hstkF8xNJD0Tvxgz20vqvYZ6xEXM,27287
63
63
  core/migrations/cli.py,sha256=mR3lIFTlXSvupFOPVlfuC-urJyDfNFR9nqYZn4TjIco,12019
64
- core/migrations/engine.py,sha256=tggCEV1FuFFyNkcGOlDZSryepUYHWfZ4irb0sbsWWZo,28821
64
+ core/migrations/engine.py,sha256=jk8-wX8aKNBidUGyQ7ckHcUsukNJYpgSva-Sp-Iu-L4,31590
65
65
  core/migrations/migration.py,sha256=Xv5MSNLvGAR9wnuMc4GRwciUSuU22AxWlWZP-hsVliI,2748
66
66
  core/migrations/operations.py,sha256=wZLui76zU-MDiJfyn3l3NBRGJw1V4XF8tViSV3kvN6A,28651
67
67
  core/migrations/state.py,sha256=eb_EYTE1tG-xQIwliS_-QTgr0y8-Jj0Va4C3nfpMrd4,15324
@@ -78,7 +78,7 @@ example/auth.py,sha256=zBpLutb8lVKnGfQqQ2wnyygsSutHYZzeJBuhnFhxBaQ,4971
78
78
  example/models.py,sha256=xKdx0kJ9n0tZ7sCce3KhV3BTvKvsh6m7G69eFm3ukf0,4549
79
79
  example/schemas.py,sha256=wJ9QofnuHp4PjtM_IuMMBLVFVDJ4YlwcF6uQm1ooKiY,6139
80
80
  example/views.py,sha256=GQwgQcW6yoeUIDbF7-lsaZV7cs8G1S1vGVtiwVpZIQE,14338
81
- core_framework-0.12.2.dist-info/METADATA,sha256=ulG99_nW-of7nHPyV8Ybq4QEAVSyeHh2lqwmvy42XiE,12791
82
- core_framework-0.12.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
83
- core_framework-0.12.2.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
84
- core_framework-0.12.2.dist-info/RECORD,,
81
+ core_framework-0.12.4.dist-info/METADATA,sha256=YJACIGfMLNMWJNHuGVX2ltcOvKdZDajmp6GLc_GFzNA,12791
82
+ core_framework-0.12.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
83
+ core_framework-0.12.4.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
84
+ core_framework-0.12.4.dist-info/RECORD,,