core-framework 0.12.11__tar.gz → 0.12.12__tar.gz

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 (142) hide show
  1. {core_framework-0.12.11 → core_framework-0.12.12}/PKG-INFO +1 -1
  2. {core_framework-0.12.11 → core_framework-0.12.12}/core/__init__.py +1 -1
  3. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/views.py +37 -0
  4. {core_framework-0.12.11 → core_framework-0.12.12}/core/cli/main.py +37 -3
  5. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/operations.py +47 -9
  6. {core_framework-0.12.11 → core_framework-0.12.12}/core/routing.py +15 -2
  7. {core_framework-0.12.11 → core_framework-0.12.12}/core/views.py +12 -2
  8. {core_framework-0.12.11 → core_framework-0.12.12}/pyproject.toml +1 -1
  9. core_framework-0.12.12/tests/test_issues_fixes.py +264 -0
  10. {core_framework-0.12.11 → core_framework-0.12.12}/.gitignore +0 -0
  11. {core_framework-0.12.11 → core_framework-0.12.12}/README.md +0 -0
  12. {core_framework-0.12.11 → core_framework-0.12.12}/core/app.py +0 -0
  13. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/__init__.py +0 -0
  14. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/backends.py +0 -0
  15. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/base.py +0 -0
  16. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/decorators.py +0 -0
  17. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/hashers.py +0 -0
  18. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/helpers.py +0 -0
  19. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/middleware.py +0 -0
  20. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/models.py +0 -0
  21. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/permissions.py +0 -0
  22. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/schemas.py +0 -0
  23. {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/tokens.py +0 -0
  24. {core_framework-0.12.11 → core_framework-0.12.12}/core/choices.py +0 -0
  25. {core_framework-0.12.11 → core_framework-0.12.12}/core/cli/__init__.py +0 -0
  26. {core_framework-0.12.11 → core_framework-0.12.12}/core/config.py +0 -0
  27. {core_framework-0.12.11 → core_framework-0.12.12}/core/database.py +0 -0
  28. {core_framework-0.12.11 → core_framework-0.12.12}/core/datetime.py +0 -0
  29. {core_framework-0.12.11 → core_framework-0.12.12}/core/dependencies.py +0 -0
  30. {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/__init__.py +0 -0
  31. {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/docker.py +0 -0
  32. {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/kubernetes.py +0 -0
  33. {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/pm2.py +0 -0
  34. {core_framework-0.12.11 → core_framework-0.12.12}/core/exceptions.py +0 -0
  35. {core_framework-0.12.11 → core_framework-0.12.12}/core/fields.py +0 -0
  36. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/__init__.py +0 -0
  37. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/avro.py +0 -0
  38. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/base.py +0 -0
  39. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/config.py +0 -0
  40. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/confluent/__init__.py +0 -0
  41. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/confluent/consumer.py +0 -0
  42. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/confluent/producer.py +0 -0
  43. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/decorators.py +0 -0
  44. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/__init__.py +0 -0
  45. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/admin.py +0 -0
  46. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/broker.py +0 -0
  47. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/consumer.py +0 -0
  48. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/producer.py +0 -0
  49. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/__init__.py +0 -0
  50. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/broker.py +0 -0
  51. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/consumer.py +0 -0
  52. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/producer.py +0 -0
  53. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/__init__.py +0 -0
  54. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/broker.py +0 -0
  55. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/consumer.py +0 -0
  56. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/producer.py +0 -0
  57. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/registry.py +0 -0
  58. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/topics.py +0 -0
  59. {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/workers.py +0 -0
  60. {core_framework-0.12.11 → core_framework-0.12.12}/core/middleware.py +0 -0
  61. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/__init__.py +0 -0
  62. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/analyzer.py +0 -0
  63. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/cli.py +0 -0
  64. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/engine.py +0 -0
  65. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/migration.py +0 -0
  66. {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/state.py +0 -0
  67. {core_framework-0.12.11 → core_framework-0.12.12}/core/models.py +0 -0
  68. {core_framework-0.12.11 → core_framework-0.12.12}/core/permissions.py +0 -0
  69. {core_framework-0.12.11 → core_framework-0.12.12}/core/querysets.py +0 -0
  70. {core_framework-0.12.11 → core_framework-0.12.12}/core/relations.py +0 -0
  71. {core_framework-0.12.11 → core_framework-0.12.12}/core/serializers.py +0 -0
  72. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/__init__.py +0 -0
  73. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/base.py +0 -0
  74. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/config.py +0 -0
  75. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/decorators.py +0 -0
  76. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/registry.py +0 -0
  77. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/scheduler.py +0 -0
  78. {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/worker.py +0 -0
  79. {core_framework-0.12.11 → core_framework-0.12.12}/core/tenancy.py +0 -0
  80. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/__init__.py +0 -0
  81. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/assertions.py +0 -0
  82. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/client.py +0 -0
  83. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/database.py +0 -0
  84. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/factories.py +0 -0
  85. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/mocks.py +0 -0
  86. {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/plugin.py +0 -0
  87. {core_framework-0.12.11 → core_framework-0.12.12}/core/validation.py +0 -0
  88. {core_framework-0.12.11 → core_framework-0.12.12}/core/validators.py +0 -0
  89. {core_framework-0.12.11 → core_framework-0.12.12}/docs/01-quickstart.md +0 -0
  90. {core_framework-0.12.11 → core_framework-0.12.12}/docs/02-viewsets.md +0 -0
  91. {core_framework-0.12.11 → core_framework-0.12.12}/docs/03-authentication.md +0 -0
  92. {core_framework-0.12.11 → core_framework-0.12.12}/docs/04-messaging.md +0 -0
  93. {core_framework-0.12.11 → core_framework-0.12.12}/docs/05-multi-service.md +0 -0
  94. {core_framework-0.12.11 → core_framework-0.12.12}/docs/06-tasks.md +0 -0
  95. {core_framework-0.12.11 → core_framework-0.12.12}/docs/07-deployment.md +0 -0
  96. {core_framework-0.12.11 → core_framework-0.12.12}/docs/08-complete-example.md +0 -0
  97. {core_framework-0.12.11 → core_framework-0.12.12}/docs/09-settings.md +0 -0
  98. {core_framework-0.12.11 → core_framework-0.12.12}/docs/10-migrations.md +0 -0
  99. {core_framework-0.12.11 → core_framework-0.12.12}/docs/11-permissions.md +0 -0
  100. {core_framework-0.12.11 → core_framework-0.12.12}/docs/12-auth-backends.md +0 -0
  101. {core_framework-0.12.11 → core_framework-0.12.12}/docs/13-validators.md +0 -0
  102. {core_framework-0.12.11 → core_framework-0.12.12}/docs/14-querysets.md +0 -0
  103. {core_framework-0.12.11 → core_framework-0.12.12}/docs/15-routing.md +0 -0
  104. {core_framework-0.12.11 → core_framework-0.12.12}/docs/16-serializers.md +0 -0
  105. {core_framework-0.12.11 → core_framework-0.12.12}/docs/17-datetime.md +0 -0
  106. {core_framework-0.12.11 → core_framework-0.12.12}/docs/18-dependencies.md +0 -0
  107. {core_framework-0.12.11 → core_framework-0.12.12}/docs/19-views.md +0 -0
  108. {core_framework-0.12.11 → core_framework-0.12.12}/docs/20-fields.md +0 -0
  109. {core_framework-0.12.11 → core_framework-0.12.12}/docs/21-tenancy.md +0 -0
  110. {core_framework-0.12.11 → core_framework-0.12.12}/docs/22-replicas.md +0 -0
  111. {core_framework-0.12.11 → core_framework-0.12.12}/docs/23-soft-delete.md +0 -0
  112. {core_framework-0.12.11 → core_framework-0.12.12}/docs/24-relations.md +0 -0
  113. {core_framework-0.12.11 → core_framework-0.12.12}/docs/25-exceptions.md +0 -0
  114. {core_framework-0.12.11 → core_framework-0.12.12}/docs/26-choices.md +0 -0
  115. {core_framework-0.12.11 → core_framework-0.12.12}/docs/27-workers.md +0 -0
  116. {core_framework-0.12.11 → core_framework-0.12.12}/docs/28-avro.md +0 -0
  117. {core_framework-0.12.11 → core_framework-0.12.12}/docs/29-topics.md +0 -0
  118. {core_framework-0.12.11 → core_framework-0.12.12}/docs/30-changelog-0.12.2.md +0 -0
  119. {core_framework-0.12.11 → core_framework-0.12.12}/docs/31-middleware.md +0 -0
  120. {core_framework-0.12.11 → core_framework-0.12.12}/docs/32-migration-guide-0.12.2.md +0 -0
  121. {core_framework-0.12.11 → core_framework-0.12.12}/docs/32-testing.md +0 -0
  122. {core_framework-0.12.11 → core_framework-0.12.12}/docs/33-changelog-0.12.3.md +0 -0
  123. {core_framework-0.12.11 → core_framework-0.12.12}/docs/99-faq-troubleshooting.md +0 -0
  124. {core_framework-0.12.11 → core_framework-0.12.12}/docs/GUIDE.md +0 -0
  125. {core_framework-0.12.11 → core_framework-0.12.12}/docs/README.md +0 -0
  126. {core_framework-0.12.11 → core_framework-0.12.12}/example/__init__.py +0 -0
  127. {core_framework-0.12.11 → core_framework-0.12.12}/example/app.py +0 -0
  128. {core_framework-0.12.11 → core_framework-0.12.12}/example/auth.py +0 -0
  129. {core_framework-0.12.11 → core_framework-0.12.12}/example/models.py +0 -0
  130. {core_framework-0.12.11 → core_framework-0.12.12}/example/schemas.py +0 -0
  131. {core_framework-0.12.11 → core_framework-0.12.12}/example/views.py +0 -0
  132. {core_framework-0.12.11 → core_framework-0.12.12}/libs/__init__.py +0 -0
  133. {core_framework-0.12.11 → core_framework-0.12.12}/main.py +0 -0
  134. {core_framework-0.12.11 → core_framework-0.12.12}/tests/__init__.py +0 -0
  135. {core_framework-0.12.11 → core_framework-0.12.12}/tests/conftest.py +0 -0
  136. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_auth_helpers.py +0 -0
  137. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_imports.py +0 -0
  138. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_models.py +0 -0
  139. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_permissions.py +0 -0
  140. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_querysets.py +0 -0
  141. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_serializers.py +0 -0
  142. {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-framework
3
- Version: 0.12.11
3
+ Version: 0.12.12
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
@@ -287,7 +287,7 @@ from core.exceptions import (
287
287
  MissingDependency,
288
288
  )
289
289
 
290
- __version__ = "0.12.11"
290
+ __version__ = "0.12.12"
291
291
  __all__ = [
292
292
  # Models
293
293
  "Model",
@@ -88,6 +88,43 @@ class AuthViewSet(ViewSet):
88
88
  # ViewSet config
89
89
  tags: list[str] = ["auth"]
90
90
 
91
+ # Explicitly disable CRUD endpoints that don't make sense for auth
92
+ # These would cause 500 errors if called
93
+ async def list(self, *args, **kwargs):
94
+ """List is not available on auth endpoint."""
95
+ raise HTTPException(
96
+ status_code=405,
97
+ detail="Method not allowed. Use /auth/me to get current user."
98
+ )
99
+
100
+ async def retrieve(self, *args, **kwargs):
101
+ """Retrieve is not available on auth endpoint."""
102
+ raise HTTPException(
103
+ status_code=405,
104
+ detail="Method not allowed. Use /auth/me to get current user."
105
+ )
106
+
107
+ async def create(self, *args, **kwargs):
108
+ """Use /auth/register instead."""
109
+ raise HTTPException(
110
+ status_code=405,
111
+ detail="Method not allowed. Use /auth/register to create users."
112
+ )
113
+
114
+ async def update(self, *args, **kwargs):
115
+ """Update is not available on auth endpoint."""
116
+ raise HTTPException(
117
+ status_code=405,
118
+ detail="Method not allowed."
119
+ )
120
+
121
+ async def destroy(self, *args, **kwargs):
122
+ """Destroy is not available on auth endpoint."""
123
+ raise HTTPException(
124
+ status_code=405,
125
+ detail="Method not allowed."
126
+ )
127
+
91
128
  # Cache for dynamic schema
92
129
  _dynamic_register_schema: type | None = None
93
130
 
@@ -2266,7 +2266,32 @@ def cmd_check(args: argparse.Namespace) -> int:
2266
2266
  continue
2267
2267
 
2268
2268
  pending_count += 1
2269
- migration = engine._load_migration(file_path)
2269
+
2270
+ # Try to load migration with error handling for syntax errors
2271
+ try:
2272
+ migration = engine._load_migration(file_path)
2273
+ except SyntaxError as e:
2274
+ print(error(f"\n {migration_name}: SYNTAX ERROR"))
2275
+ print(error(f" File: {file_path}"))
2276
+ print(error(f" Line {e.lineno}: {e.msg}"))
2277
+ if e.text:
2278
+ print(error(f" {e.text.strip()}"))
2279
+ print(warning(f" Fix the syntax error and run check again."))
2280
+ total_issues.append(type('Issue', (), {
2281
+ 'severity': Severity.CRITICAL,
2282
+ 'message': f"Syntax error in {migration_name}: {e.msg}"
2283
+ })())
2284
+ continue
2285
+ except Exception as e:
2286
+ print(error(f"\n {migration_name}: LOAD ERROR"))
2287
+ print(error(f" {type(e).__name__}: {e}"))
2288
+ print(warning(f" Fix the error and run check again."))
2289
+ total_issues.append(type('Issue', (), {
2290
+ 'severity': Severity.CRITICAL,
2291
+ 'message': f"Load error in {migration_name}: {e}"
2292
+ })())
2293
+ continue
2294
+
2270
2295
  result = await analyzer.analyze(
2271
2296
  migration.operations,
2272
2297
  conn,
@@ -2373,13 +2398,22 @@ def cmd_reset_db(args: argparse.Namespace) -> int:
2373
2398
  elif dialect == "mysql":
2374
2399
  await conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
2375
2400
 
2376
- # Drop all tables
2401
+ # Drop all tables with CASCADE for PostgreSQL
2377
2402
  for table in tables:
2378
2403
  print(f" Dropping: {table}")
2379
2404
  try:
2380
- await conn.execute(text(f'DROP TABLE IF EXISTS "{table}"'))
2405
+ if dialect == "postgresql":
2406
+ # Use CASCADE to handle foreign key dependencies
2407
+ await conn.execute(text(f'DROP TABLE IF EXISTS "{table}" CASCADE'))
2408
+ else:
2409
+ await conn.execute(text(f'DROP TABLE IF EXISTS "{table}"'))
2381
2410
  except Exception as e:
2382
2411
  print(warning(f" Warning: {e}"))
2412
+ # Rollback and continue with new transaction
2413
+ try:
2414
+ await conn.rollback()
2415
+ except Exception:
2416
+ pass
2383
2417
 
2384
2418
  # Re-enable foreign key checks
2385
2419
  if dialect == "sqlite":
@@ -18,6 +18,51 @@ if TYPE_CHECKING:
18
18
  from sqlalchemy.ext.asyncio import AsyncConnection
19
19
 
20
20
 
21
+ def _serialize_default(value: Any) -> str:
22
+ """
23
+ Serialize a default value for migration file output.
24
+
25
+ Handles:
26
+ - None -> "None"
27
+ - Strings -> repr()
28
+ - Booleans -> "True"/"False"
29
+ - Callables -> "module.function" (importable reference)
30
+ - Other -> repr()
31
+
32
+ Args:
33
+ value: The default value to serialize
34
+
35
+ Returns:
36
+ String representation suitable for Python code
37
+ """
38
+ if value is None:
39
+ return "None"
40
+
41
+ if callable(value):
42
+ # Get module and function name for proper serialization
43
+ module = getattr(value, "__module__", "")
44
+ name = getattr(value, "__qualname__", "") or getattr(value, "__name__", "")
45
+
46
+ if module and name:
47
+ # Return importable reference
48
+ return f"{module}.{name}"
49
+
50
+ # Fallback for lambdas or unnamed functions
51
+ return "None"
52
+
53
+ if isinstance(value, bool):
54
+ return str(value)
55
+
56
+ if isinstance(value, str):
57
+ return repr(value)
58
+
59
+ if isinstance(value, (int, float)):
60
+ return str(value)
61
+
62
+ # Default: use repr (may not always be valid Python)
63
+ return repr(value)
64
+
65
+
21
66
  @dataclass
22
67
  class ColumnDef:
23
68
  """Definição de uma coluna."""
@@ -300,15 +345,8 @@ class AddColumn(Operation):
300
345
 
301
346
  def to_code(self) -> str:
302
347
  c = self.column
303
- # Serializa default de forma segura
304
- default_val = "None"
305
- if c.default is not None and not callable(c.default):
306
- if isinstance(c.default, str):
307
- default_val = f"'{c.default}'"
308
- elif isinstance(c.default, bool):
309
- default_val = str(c.default)
310
- else:
311
- default_val = repr(c.default)
348
+ # Serializa default de forma segura usando helper function
349
+ default_val = _serialize_default(c.default)
312
350
 
313
351
  return f"""AddColumn(
314
352
  table_name='{self.table_name}',
@@ -256,8 +256,14 @@ class Router(APIRouter):
256
256
  for http_method in action_methods:
257
257
  route_name = f"{basename}-{name}"
258
258
 
259
+ # Get action-specific permission_classes
260
+ action_permission_classes = getattr(method, "permission_classes", None)
261
+
259
262
  # Captura method em closure para evitar late binding
260
- def make_action_endpoint(action_method: Callable) -> Callable:
263
+ def make_action_endpoint(
264
+ action_method: Callable,
265
+ perm_classes: list | None = None,
266
+ ) -> Callable:
261
267
  async def action_endpoint(
262
268
  request: Request,
263
269
  db: AsyncSession = Depends(get_db),
@@ -265,6 +271,13 @@ class Router(APIRouter):
265
271
  data: dict[str, Any] | None = Body(None),
266
272
  ) -> Any:
267
273
  vs = viewset_class()
274
+
275
+ # Apply action-specific permissions if defined
276
+ if perm_classes:
277
+ from core.permissions import check_permissions
278
+ perms = [p() if isinstance(p, type) else p for p in perm_classes]
279
+ await check_permissions(perms, request, vs)
280
+
268
281
  path_params = request.path_params
269
282
  if data is not None:
270
283
  return await action_method(vs, request, db, data=data, **path_params)
@@ -273,7 +286,7 @@ class Router(APIRouter):
273
286
 
274
287
  self.add_api_route(
275
288
  path,
276
- make_action_endpoint(method),
289
+ make_action_endpoint(method, action_permission_classes),
277
290
  methods=[http_method],
278
291
  tags=tags,
279
292
  name=route_name,
@@ -497,15 +497,25 @@ class ViewSet(Generic[ModelT, InputT, OutputT]):
497
497
  """
498
498
  Levanta erro padronizado para valor de lookup inválido.
499
499
 
500
+ Returns 422 Validation Error (not 500 Internal Server Error).
501
+
500
502
  Args:
501
503
  value: Valor recebido
502
504
  expected_type: Tipo esperado (para mensagem de erro)
503
505
  """
504
506
  raise HTTPException(
505
- status_code=400,
507
+ status_code=422,
506
508
  detail={
507
- "error": "invalid_lookup_value",
509
+ "error": "validation_error",
508
510
  "message": f"Invalid {self.lookup_field} format. Expected {expected_type}.",
511
+ "errors": [
512
+ {
513
+ "loc": ["path", self.lookup_field],
514
+ "msg": f"Invalid {expected_type} format",
515
+ "type": f"{expected_type.lower()}_parsing",
516
+ "input": str(value),
517
+ }
518
+ ],
509
519
  "field": self.lookup_field,
510
520
  "value": str(value),
511
521
  "expected_type": expected_type,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "core-framework"
3
- version = "0.12.11"
3
+ version = "0.12.12"
4
4
  description = "Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema."
5
5
  requires-python = ">=3.10,<4.0"
6
6
  readme = "README.md"
@@ -0,0 +1,264 @@
1
+ """
2
+ Tests for issues fixed in v0.12.12.
3
+
4
+ Issue #1: UUID inválido retorna 500 em vez de 422
5
+ Issue #2: permission_classes in @action decorator not applied
6
+ Issue #3: AuthViewSet list/retrieve endpoints return 500
7
+ Issue #4: AuthViewSet retrieve expects integer but system uses UUID
8
+ Issue #5: makemigrations serializes function references incorrectly
9
+ Issue #6: reset_db does not use CASCADE
10
+ Issue #7: check command fails when migration has syntax errors
11
+ """
12
+
13
+ import pytest
14
+ from unittest.mock import MagicMock, AsyncMock, patch
15
+ from typing import Optional
16
+
17
+
18
+ class TestIssue1UUIDValidation:
19
+ """Issue #1: UUID validation should return 422, not 500."""
20
+
21
+ def test_invalid_uuid_raises_422(self):
22
+ """Invalid UUID should raise 422 with proper error format."""
23
+ from core.views import ViewSet
24
+ from fastapi import HTTPException
25
+
26
+ class TestViewSet(ViewSet):
27
+ model = MagicMock()
28
+ lookup_field = "id"
29
+
30
+ viewset = TestViewSet()
31
+
32
+ # Mock _get_lookup_field_type to return UUID type
33
+ mock_uuid_type = MagicMock()
34
+ mock_uuid_type.__class__.__name__ = "UUID"
35
+
36
+ with patch.object(viewset, "_get_lookup_field_type", return_value=mock_uuid_type):
37
+ with pytest.raises(HTTPException) as exc_info:
38
+ viewset._convert_lookup_value("invalid-uuid")
39
+
40
+ assert exc_info.value.status_code == 422
41
+ assert "validation_error" in str(exc_info.value.detail)
42
+
43
+ def test_error_format_matches_pydantic(self):
44
+ """Error format should match Pydantic validation errors."""
45
+ from core.views import ViewSet
46
+ from fastapi import HTTPException
47
+
48
+ class TestViewSet(ViewSet):
49
+ model = MagicMock()
50
+ lookup_field = "workspace_id"
51
+
52
+ viewset = TestViewSet()
53
+
54
+ mock_uuid_type = MagicMock()
55
+ mock_uuid_type.__class__.__name__ = "UUID"
56
+
57
+ with patch.object(viewset, "_get_lookup_field_type", return_value=mock_uuid_type):
58
+ with pytest.raises(HTTPException) as exc_info:
59
+ viewset._convert_lookup_value("not-a-uuid")
60
+
61
+ detail = exc_info.value.detail
62
+ assert "errors" in detail
63
+ assert detail["errors"][0]["loc"] == ["path", "workspace_id"]
64
+ assert "uuid" in detail["errors"][0]["type"].lower()
65
+
66
+
67
+ class TestIssue2ActionPermissions:
68
+ """Issue #2: permission_classes in @action decorator should be applied."""
69
+
70
+ def test_action_stores_permission_classes(self):
71
+ """@action decorator should store permission_classes."""
72
+ from core.views import action
73
+ from core.permissions import IsAuthenticated
74
+
75
+ @action(methods=["POST"], detail=True, permission_classes=[IsAuthenticated])
76
+ async def my_action(self, request, db, **kwargs):
77
+ pass
78
+
79
+ assert my_action.permission_classes == [IsAuthenticated]
80
+
81
+ def test_action_without_permissions(self):
82
+ """@action without permissions should have None."""
83
+ from core.views import action
84
+
85
+ @action(methods=["GET"], detail=False)
86
+ async def my_action(self, request, db, **kwargs):
87
+ pass
88
+
89
+ assert my_action.permission_classes is None
90
+
91
+
92
+ class TestIssue3AuthViewSetEndpoints:
93
+ """Issue #3: AuthViewSet list/retrieve should return 405."""
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_list_returns_405(self):
97
+ """AuthViewSet.list() should return 405."""
98
+ from core.auth.views import AuthViewSet
99
+ from fastapi import HTTPException
100
+
101
+ viewset = AuthViewSet()
102
+
103
+ with pytest.raises(HTTPException) as exc_info:
104
+ await viewset.list()
105
+
106
+ assert exc_info.value.status_code == 405
107
+ assert "not allowed" in exc_info.value.detail.lower()
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_retrieve_returns_405(self):
111
+ """AuthViewSet.retrieve() should return 405."""
112
+ from core.auth.views import AuthViewSet
113
+ from fastapi import HTTPException
114
+
115
+ viewset = AuthViewSet()
116
+
117
+ with pytest.raises(HTTPException) as exc_info:
118
+ await viewset.retrieve()
119
+
120
+ assert exc_info.value.status_code == 405
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_create_returns_405(self):
124
+ """AuthViewSet.create() should return 405 (use /register)."""
125
+ from core.auth.views import AuthViewSet
126
+ from fastapi import HTTPException
127
+
128
+ viewset = AuthViewSet()
129
+
130
+ with pytest.raises(HTTPException) as exc_info:
131
+ await viewset.create()
132
+
133
+ assert exc_info.value.status_code == 405
134
+ assert "register" in exc_info.value.detail.lower()
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_update_returns_405(self):
138
+ """AuthViewSet.update() should return 405."""
139
+ from core.auth.views import AuthViewSet
140
+ from fastapi import HTTPException
141
+
142
+ viewset = AuthViewSet()
143
+
144
+ with pytest.raises(HTTPException) as exc_info:
145
+ await viewset.update()
146
+
147
+ assert exc_info.value.status_code == 405
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_destroy_returns_405(self):
151
+ """AuthViewSet.destroy() should return 405."""
152
+ from core.auth.views import AuthViewSet
153
+ from fastapi import HTTPException
154
+
155
+ viewset = AuthViewSet()
156
+
157
+ with pytest.raises(HTTPException) as exc_info:
158
+ await viewset.destroy()
159
+
160
+ assert exc_info.value.status_code == 405
161
+
162
+
163
+ class TestIssue5MigrationSerialization:
164
+ """Issue #5: Function references should be serialized correctly."""
165
+
166
+ def test_serialize_none(self):
167
+ """None should serialize to 'None'."""
168
+ from core.migrations.operations import _serialize_default
169
+
170
+ assert _serialize_default(None) == "None"
171
+
172
+ def test_serialize_string(self):
173
+ """Strings should use repr()."""
174
+ from core.migrations.operations import _serialize_default
175
+
176
+ result = _serialize_default("hello")
177
+ assert result == "'hello'"
178
+
179
+ def test_serialize_bool(self):
180
+ """Booleans should serialize to 'True'/'False'."""
181
+ from core.migrations.operations import _serialize_default
182
+
183
+ assert _serialize_default(True) == "True"
184
+ assert _serialize_default(False) == "False"
185
+
186
+ def test_serialize_number(self):
187
+ """Numbers should serialize correctly."""
188
+ from core.migrations.operations import _serialize_default
189
+
190
+ assert _serialize_default(42) == "42"
191
+ assert _serialize_default(3.14) == "3.14"
192
+
193
+ def test_serialize_callable(self):
194
+ """Callables should serialize to module.function format."""
195
+ from core.migrations.operations import _serialize_default
196
+ from core.datetime import timezone
197
+
198
+ result = _serialize_default(timezone.now)
199
+
200
+ # Should be a valid Python reference, not <function ... at 0x...>
201
+ assert "<function" not in result
202
+ assert "0x" not in result
203
+ # Should contain the function name
204
+ assert "now" in result
205
+
206
+ def test_serialize_lambda_returns_none(self):
207
+ """Lambdas (no module) should fallback to 'None'."""
208
+ from core.migrations.operations import _serialize_default
209
+
210
+ # Lambdas don't have meaningful __module__ path
211
+ result = _serialize_default(lambda: None)
212
+ # Should not contain <function
213
+ assert "<function" not in result
214
+
215
+
216
+ class TestIssue6ResetDbCascade:
217
+ """Issue #6: reset_db should use CASCADE for PostgreSQL."""
218
+
219
+ def test_cascade_in_drop_statement(self):
220
+ """PostgreSQL should use CASCADE when dropping tables."""
221
+ # This is more of an integration test - we verify the code path exists
222
+ from core.cli.main import cmd_reset_db
223
+
224
+ # The fix is in the code, we just verify the function exists
225
+ assert cmd_reset_db is not None
226
+
227
+
228
+ class TestIssue7CheckCommandSyntaxError:
229
+ """Issue #7: check command should handle syntax errors gracefully."""
230
+
231
+ def test_syntax_error_handling(self):
232
+ """Check command should report syntax errors without crashing."""
233
+ # This test verifies the error handling code exists
234
+ # Full integration test would require mocking the file system
235
+ from core.cli.main import cmd_check
236
+
237
+ assert cmd_check is not None
238
+
239
+
240
+ class TestValidUUIDConversion:
241
+ """Test that valid UUIDs are properly converted."""
242
+
243
+ def test_valid_uuid_is_converted(self):
244
+ """Valid UUID string should be converted to UUID object."""
245
+ from core.views import ViewSet
246
+ import uuid
247
+
248
+ class TestViewSet(ViewSet):
249
+ model = MagicMock()
250
+ lookup_field = "id"
251
+
252
+ viewset = TestViewSet()
253
+
254
+ # Mock _get_lookup_field_type to return UUID type
255
+ mock_uuid_type = MagicMock()
256
+ mock_uuid_type.__class__.__name__ = "UUID"
257
+
258
+ valid_uuid = "019c2476-9cac-76e6-8836-4c031d186b6e"
259
+
260
+ with patch.object(viewset, "_get_lookup_field_type", return_value=mock_uuid_type):
261
+ result = viewset._convert_lookup_value(valid_uuid)
262
+
263
+ assert isinstance(result, uuid.UUID)
264
+ assert str(result) == valid_uuid