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.
- {core_framework-0.12.11 → core_framework-0.12.12}/PKG-INFO +1 -1
- {core_framework-0.12.11 → core_framework-0.12.12}/core/__init__.py +1 -1
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/views.py +37 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/cli/main.py +37 -3
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/operations.py +47 -9
- {core_framework-0.12.11 → core_framework-0.12.12}/core/routing.py +15 -2
- {core_framework-0.12.11 → core_framework-0.12.12}/core/views.py +12 -2
- {core_framework-0.12.11 → core_framework-0.12.12}/pyproject.toml +1 -1
- core_framework-0.12.12/tests/test_issues_fixes.py +264 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/.gitignore +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/README.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/app.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/backends.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/base.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/decorators.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/hashers.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/helpers.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/middleware.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/models.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/permissions.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/schemas.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/auth/tokens.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/choices.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/cli/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/config.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/database.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/datetime.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/dependencies.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/docker.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/kubernetes.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/deployment/pm2.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/exceptions.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/fields.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/avro.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/base.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/config.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/confluent/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/confluent/consumer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/confluent/producer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/decorators.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/admin.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/broker.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/consumer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/kafka/producer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/broker.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/consumer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/rabbitmq/producer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/broker.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/consumer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/redis/producer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/registry.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/topics.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/messaging/workers.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/middleware.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/analyzer.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/cli.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/engine.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/migration.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/migrations/state.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/models.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/permissions.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/querysets.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/relations.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/serializers.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/base.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/config.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/decorators.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/registry.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/scheduler.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tasks/worker.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/tenancy.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/assertions.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/client.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/database.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/factories.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/mocks.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/testing/plugin.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/validation.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/core/validators.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/01-quickstart.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/02-viewsets.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/03-authentication.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/04-messaging.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/05-multi-service.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/06-tasks.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/07-deployment.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/08-complete-example.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/09-settings.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/10-migrations.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/11-permissions.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/12-auth-backends.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/13-validators.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/14-querysets.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/15-routing.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/16-serializers.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/17-datetime.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/18-dependencies.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/19-views.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/20-fields.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/21-tenancy.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/22-replicas.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/23-soft-delete.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/24-relations.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/25-exceptions.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/26-choices.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/27-workers.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/28-avro.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/29-topics.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/30-changelog-0.12.2.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/31-middleware.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/32-migration-guide-0.12.2.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/32-testing.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/33-changelog-0.12.3.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/99-faq-troubleshooting.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/GUIDE.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/docs/README.md +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/example/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/example/app.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/example/auth.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/example/models.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/example/schemas.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/example/views.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/libs/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/main.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/__init__.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/conftest.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_auth_helpers.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_imports.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_models.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_permissions.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_querysets.py +0 -0
- {core_framework-0.12.11 → core_framework-0.12.12}/tests/test_serializers.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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=
|
|
507
|
+
status_code=422,
|
|
506
508
|
detail={
|
|
507
|
-
"error": "
|
|
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,
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|