core-framework 0.12.1__py3-none-any.whl → 0.12.2__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 +316 -0
- core/auth/models.py +139 -23
- core/auth/schemas.py +5 -1
- core/auth/views.py +168 -50
- core/config.py +27 -0
- core/middleware.py +774 -0
- core/migrations/operations.py +88 -10
- core/views.py +453 -28
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/METADATA +1 -1
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/RECORD +16 -14
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/WHEEL +0 -0
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/entry_points.txt +0 -0
core/middleware.py
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sistema de Middleware Plugável - Django-style.
|
|
3
|
+
|
|
4
|
+
Permite configurar middlewares de forma declarativa, similar ao MIDDLEWARE do Django.
|
|
5
|
+
|
|
6
|
+
Uso:
|
|
7
|
+
# Em settings ou core.toml
|
|
8
|
+
MIDDLEWARE = [
|
|
9
|
+
"core.auth.AuthenticationMiddleware",
|
|
10
|
+
"core.tenancy.TenantMiddleware",
|
|
11
|
+
"myapp.middleware.CustomMiddleware",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# Ou via configuração
|
|
15
|
+
from core.middleware import configure_middleware
|
|
16
|
+
|
|
17
|
+
configure_middleware([
|
|
18
|
+
"core.auth.AuthenticationMiddleware",
|
|
19
|
+
("myapp.middleware.RateLimitMiddleware", {"requests_per_minute": 60}),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
O framework carrega e aplica os middlewares na ordem especificada.
|
|
23
|
+
|
|
24
|
+
Criando middlewares customizados:
|
|
25
|
+
from core.middleware import BaseMiddleware
|
|
26
|
+
|
|
27
|
+
class MyMiddleware(BaseMiddleware):
|
|
28
|
+
async def before_request(self, request):
|
|
29
|
+
# Executado antes da view
|
|
30
|
+
request.state.custom_data = "hello"
|
|
31
|
+
|
|
32
|
+
async def after_request(self, request, response):
|
|
33
|
+
# Executado depois da view
|
|
34
|
+
response.headers["X-Custom"] = "value"
|
|
35
|
+
return response
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import importlib
|
|
41
|
+
import warnings
|
|
42
|
+
from abc import ABC
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from typing import Any, TYPE_CHECKING
|
|
45
|
+
|
|
46
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
47
|
+
from starlette.requests import Request
|
|
48
|
+
from starlette.responses import Response
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from collections.abc import Callable, Awaitable
|
|
52
|
+
from fastapi import FastAPI
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Base Middleware Class
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
class BaseMiddleware(BaseHTTPMiddleware):
|
|
60
|
+
"""
|
|
61
|
+
Classe base para criar middlewares de forma simplificada.
|
|
62
|
+
|
|
63
|
+
Herde desta classe e implemente os métodos que precisar:
|
|
64
|
+
|
|
65
|
+
- before_request(request): Executado antes da view
|
|
66
|
+
- after_request(request, response): Executado depois da view
|
|
67
|
+
- on_error(request, exc): Executado quando ocorre exceção
|
|
68
|
+
|
|
69
|
+
Exemplo:
|
|
70
|
+
from core.middleware import BaseMiddleware
|
|
71
|
+
|
|
72
|
+
class TimingMiddleware(BaseMiddleware):
|
|
73
|
+
'''Mede tempo de execução das requests.'''
|
|
74
|
+
|
|
75
|
+
async def before_request(self, request):
|
|
76
|
+
import time
|
|
77
|
+
request.state.start_time = time.time()
|
|
78
|
+
|
|
79
|
+
async def after_request(self, request, response):
|
|
80
|
+
import time
|
|
81
|
+
duration = time.time() - request.state.start_time
|
|
82
|
+
response.headers["X-Response-Time"] = f"{duration:.3f}s"
|
|
83
|
+
return response
|
|
84
|
+
|
|
85
|
+
class AuthMiddleware(BaseMiddleware):
|
|
86
|
+
'''Autentica usuários.'''
|
|
87
|
+
|
|
88
|
+
def __init__(self, app, user_model=None):
|
|
89
|
+
super().__init__(app)
|
|
90
|
+
self.user_model = user_model
|
|
91
|
+
|
|
92
|
+
async def before_request(self, request):
|
|
93
|
+
request.state.user = await self.authenticate(request)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Nome legível do middleware (para logs e debug)
|
|
97
|
+
name: str = "BaseMiddleware"
|
|
98
|
+
|
|
99
|
+
# Ordem de execução (menor = executa primeiro)
|
|
100
|
+
# Middlewares com mesma ordem executam na ordem de registro
|
|
101
|
+
order: int = 100
|
|
102
|
+
|
|
103
|
+
# Paths para ignorar (não executar middleware)
|
|
104
|
+
exclude_paths: list[str] = []
|
|
105
|
+
|
|
106
|
+
# Paths para incluir (executar apenas nesses)
|
|
107
|
+
# Se vazio, executa em todos (exceto exclude_paths)
|
|
108
|
+
include_paths: list[str] = []
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
app: "Callable[[Request], Awaitable[Response]]",
|
|
113
|
+
**kwargs: Any,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Inicializa o middleware.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
app: Próximo app/middleware na cadeia
|
|
120
|
+
**kwargs: Configurações customizadas
|
|
121
|
+
"""
|
|
122
|
+
super().__init__(app)
|
|
123
|
+
|
|
124
|
+
# Aplica kwargs como atributos
|
|
125
|
+
for key, value in kwargs.items():
|
|
126
|
+
setattr(self, key, value)
|
|
127
|
+
|
|
128
|
+
async def dispatch(
|
|
129
|
+
self,
|
|
130
|
+
request: Request,
|
|
131
|
+
call_next: "Callable[[Request], Awaitable[Response]]",
|
|
132
|
+
) -> Response:
|
|
133
|
+
"""
|
|
134
|
+
Processa a request através do middleware.
|
|
135
|
+
|
|
136
|
+
Não sobrescreva este método diretamente.
|
|
137
|
+
Use before_request, after_request e on_error.
|
|
138
|
+
"""
|
|
139
|
+
# Verifica se deve processar esta request
|
|
140
|
+
if not self._should_process(request.url.path):
|
|
141
|
+
return await call_next(request)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Before request hook
|
|
145
|
+
result = await self.before_request(request)
|
|
146
|
+
|
|
147
|
+
# Se before_request retornar Response, use-a diretamente
|
|
148
|
+
if isinstance(result, Response):
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
# Chama próximo middleware/view
|
|
152
|
+
response = await call_next(request)
|
|
153
|
+
|
|
154
|
+
# After request hook
|
|
155
|
+
response = await self.after_request(request, response)
|
|
156
|
+
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
# Error hook
|
|
161
|
+
error_response = await self.on_error(request, exc)
|
|
162
|
+
if error_response is not None:
|
|
163
|
+
return error_response
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
def _should_process(self, path: str) -> bool:
|
|
167
|
+
"""Verifica se deve processar esta path."""
|
|
168
|
+
# Se include_paths definido, só processa esses
|
|
169
|
+
if self.include_paths:
|
|
170
|
+
return any(path.startswith(p) for p in self.include_paths)
|
|
171
|
+
|
|
172
|
+
# Verifica exclude_paths
|
|
173
|
+
if self.exclude_paths:
|
|
174
|
+
return not any(path.startswith(p) for p in self.exclude_paths)
|
|
175
|
+
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
async def before_request(self, request: Request) -> Response | None:
|
|
179
|
+
"""
|
|
180
|
+
Hook executado antes da view.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
request: Request objeto
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
None para continuar, ou Response para retornar diretamente
|
|
187
|
+
"""
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
async def after_request(self, request: Request, response: Response) -> Response:
|
|
191
|
+
"""
|
|
192
|
+
Hook executado depois da view.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
request: Request objeto
|
|
196
|
+
response: Response da view
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Response (pode ser modificada)
|
|
200
|
+
"""
|
|
201
|
+
return response
|
|
202
|
+
|
|
203
|
+
async def on_error(self, request: Request, exc: Exception) -> Response | None:
|
|
204
|
+
"""
|
|
205
|
+
Hook executado quando ocorre exceção.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
request: Request objeto
|
|
209
|
+
exc: Exceção que ocorreu
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Response para retornar, ou None para re-raise
|
|
213
|
+
"""
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# =============================================================================
|
|
218
|
+
# Middleware Registry
|
|
219
|
+
# =============================================================================
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class MiddlewareConfig:
|
|
223
|
+
"""Configuração de um middleware."""
|
|
224
|
+
|
|
225
|
+
# Classe ou path string do middleware
|
|
226
|
+
middleware: str | type
|
|
227
|
+
|
|
228
|
+
# Kwargs para passar ao middleware
|
|
229
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
230
|
+
|
|
231
|
+
# Se está habilitado
|
|
232
|
+
enabled: bool = True
|
|
233
|
+
|
|
234
|
+
# Nome para identificação (auto-gerado se None)
|
|
235
|
+
name: str | None = None
|
|
236
|
+
|
|
237
|
+
def __post_init__(self):
|
|
238
|
+
if self.name is None:
|
|
239
|
+
if isinstance(self.middleware, str):
|
|
240
|
+
self.name = self.middleware.split(".")[-1]
|
|
241
|
+
else:
|
|
242
|
+
self.name = self.middleware.__name__
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Registry global de middlewares configurados
|
|
246
|
+
_middleware_registry: list[MiddlewareConfig] = []
|
|
247
|
+
|
|
248
|
+
# Middlewares built-in disponíveis (atalhos)
|
|
249
|
+
_builtin_middlewares: dict[str, str] = {
|
|
250
|
+
# Auth
|
|
251
|
+
"auth": "core.auth.middleware.AuthenticationMiddleware",
|
|
252
|
+
"authentication": "core.auth.middleware.AuthenticationMiddleware",
|
|
253
|
+
"optional_auth": "core.auth.middleware.OptionalAuthenticationMiddleware",
|
|
254
|
+
|
|
255
|
+
# Tenancy
|
|
256
|
+
"tenant": "core.tenancy.TenantMiddleware",
|
|
257
|
+
"tenancy": "core.tenancy.TenantMiddleware",
|
|
258
|
+
|
|
259
|
+
# Common
|
|
260
|
+
"cors": "starlette.middleware.cors.CORSMiddleware",
|
|
261
|
+
"gzip": "starlette.middleware.gzip.GZipMiddleware",
|
|
262
|
+
"https_redirect": "starlette.middleware.httpsredirect.HTTPSRedirectMiddleware",
|
|
263
|
+
"trusted_host": "starlette.middleware.trustedhost.TrustedHostMiddleware",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _resolve_middleware_class(middleware: str | type) -> type:
|
|
268
|
+
"""
|
|
269
|
+
Resolve middleware string para classe.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
middleware: String path ou classe direta
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Classe do middleware
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ImportError: Se não encontrar
|
|
279
|
+
"""
|
|
280
|
+
if isinstance(middleware, type):
|
|
281
|
+
return middleware
|
|
282
|
+
|
|
283
|
+
# Verifica se é atalho built-in
|
|
284
|
+
if middleware in _builtin_middlewares:
|
|
285
|
+
middleware = _builtin_middlewares[middleware]
|
|
286
|
+
|
|
287
|
+
# Importa dinamicamente
|
|
288
|
+
try:
|
|
289
|
+
module_path, class_name = middleware.rsplit(".", 1)
|
|
290
|
+
module = importlib.import_module(module_path)
|
|
291
|
+
return getattr(module, class_name)
|
|
292
|
+
except (ValueError, ImportError, AttributeError) as e:
|
|
293
|
+
raise ImportError(
|
|
294
|
+
f"Could not import middleware '{middleware}'. "
|
|
295
|
+
f"Make sure the module and class exist. Error: {e}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def register_middleware(
|
|
300
|
+
middleware: str | type,
|
|
301
|
+
kwargs: dict[str, Any] | None = None,
|
|
302
|
+
enabled: bool = True,
|
|
303
|
+
name: str | None = None,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Registra um middleware no registry global.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
middleware: Classe ou path string do middleware
|
|
310
|
+
kwargs: Argumentos para o middleware
|
|
311
|
+
enabled: Se está habilitado
|
|
312
|
+
name: Nome opcional
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
register_middleware("core.auth.AuthenticationMiddleware")
|
|
316
|
+
register_middleware(MyMiddleware, {"option": "value"})
|
|
317
|
+
"""
|
|
318
|
+
config = MiddlewareConfig(
|
|
319
|
+
middleware=middleware,
|
|
320
|
+
kwargs=kwargs or {},
|
|
321
|
+
enabled=enabled,
|
|
322
|
+
name=name,
|
|
323
|
+
)
|
|
324
|
+
_middleware_registry.append(config)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def unregister_middleware(name_or_class: str | type) -> bool:
|
|
328
|
+
"""
|
|
329
|
+
Remove um middleware do registry.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
name_or_class: Nome ou classe do middleware
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
True se removido, False se não encontrado
|
|
336
|
+
"""
|
|
337
|
+
global _middleware_registry
|
|
338
|
+
|
|
339
|
+
for i, config in enumerate(_middleware_registry):
|
|
340
|
+
if config.name == name_or_class or config.middleware == name_or_class:
|
|
341
|
+
_middleware_registry.pop(i)
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def get_registered_middlewares() -> list[MiddlewareConfig]:
|
|
348
|
+
"""Retorna lista de middlewares registrados."""
|
|
349
|
+
return _middleware_registry.copy()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def clear_middleware_registry() -> None:
|
|
353
|
+
"""Limpa o registry de middlewares."""
|
|
354
|
+
global _middleware_registry
|
|
355
|
+
_middleware_registry = []
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# =============================================================================
|
|
359
|
+
# Configuration Functions
|
|
360
|
+
# =============================================================================
|
|
361
|
+
|
|
362
|
+
def configure_middleware(
|
|
363
|
+
middlewares: list[str | type | tuple[str | type, dict[str, Any]]],
|
|
364
|
+
clear_existing: bool = True,
|
|
365
|
+
) -> None:
|
|
366
|
+
"""
|
|
367
|
+
Configura middlewares de forma declarativa, estilo Django.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
middlewares: Lista de middlewares para registrar
|
|
371
|
+
clear_existing: Se True, limpa registry antes
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
configure_middleware([
|
|
375
|
+
# String path
|
|
376
|
+
"core.auth.AuthenticationMiddleware",
|
|
377
|
+
|
|
378
|
+
# Com kwargs
|
|
379
|
+
("myapp.RateLimitMiddleware", {"requests_per_minute": 60}),
|
|
380
|
+
|
|
381
|
+
# Classe direta
|
|
382
|
+
MyCustomMiddleware,
|
|
383
|
+
|
|
384
|
+
# Built-in shortcut
|
|
385
|
+
"auth", # = core.auth.AuthenticationMiddleware
|
|
386
|
+
|
|
387
|
+
# Built-in com kwargs
|
|
388
|
+
("gzip", {"minimum_size": 500}),
|
|
389
|
+
])
|
|
390
|
+
"""
|
|
391
|
+
if clear_existing:
|
|
392
|
+
clear_middleware_registry()
|
|
393
|
+
|
|
394
|
+
for item in middlewares:
|
|
395
|
+
if isinstance(item, tuple):
|
|
396
|
+
middleware, kwargs = item
|
|
397
|
+
register_middleware(middleware, kwargs)
|
|
398
|
+
else:
|
|
399
|
+
register_middleware(item)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def apply_middlewares(app: "FastAPI") -> "FastAPI":
|
|
403
|
+
"""
|
|
404
|
+
Aplica todos os middlewares registrados ao app.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
app: FastAPI app
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
App com middlewares aplicados
|
|
411
|
+
"""
|
|
412
|
+
# Ordena por prioridade (se BaseMiddleware)
|
|
413
|
+
configs = get_registered_middlewares()
|
|
414
|
+
|
|
415
|
+
# Aplica em ordem reversa (primeiro registrado = mais externo)
|
|
416
|
+
for config in reversed(configs):
|
|
417
|
+
if not config.enabled:
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
middleware_class = _resolve_middleware_class(config.middleware)
|
|
422
|
+
app.add_middleware(middleware_class, **config.kwargs)
|
|
423
|
+
except ImportError as e:
|
|
424
|
+
warnings.warn(f"Failed to load middleware: {e}", RuntimeWarning)
|
|
425
|
+
|
|
426
|
+
return app
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# =============================================================================
|
|
430
|
+
# Middleware Stack Info (Debug/Introspection)
|
|
431
|
+
# =============================================================================
|
|
432
|
+
|
|
433
|
+
def get_middleware_stack_info(app: Any) -> list[dict[str, Any]]:
|
|
434
|
+
"""
|
|
435
|
+
Retorna informações sobre a stack de middlewares.
|
|
436
|
+
|
|
437
|
+
Útil para debug e introspection.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
app: FastAPI ou CoreApp
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Lista com info de cada middleware
|
|
444
|
+
"""
|
|
445
|
+
info = []
|
|
446
|
+
|
|
447
|
+
# Obtém FastAPI app se CoreApp
|
|
448
|
+
if hasattr(app, "app"):
|
|
449
|
+
app = app.app
|
|
450
|
+
|
|
451
|
+
# Percorre middleware stack
|
|
452
|
+
current = getattr(app, "middleware_stack", None)
|
|
453
|
+
|
|
454
|
+
while current is not None:
|
|
455
|
+
middleware_info = {
|
|
456
|
+
"class": type(current).__name__,
|
|
457
|
+
"module": type(current).__module__,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Tenta obter atributos úteis
|
|
461
|
+
if hasattr(current, "name"):
|
|
462
|
+
middleware_info["name"] = current.name
|
|
463
|
+
if hasattr(current, "order"):
|
|
464
|
+
middleware_info["order"] = current.order
|
|
465
|
+
if hasattr(current, "exclude_paths"):
|
|
466
|
+
middleware_info["exclude_paths"] = current.exclude_paths
|
|
467
|
+
|
|
468
|
+
info.append(middleware_info)
|
|
469
|
+
|
|
470
|
+
# Próximo na stack
|
|
471
|
+
current = getattr(current, "app", None)
|
|
472
|
+
|
|
473
|
+
# Para quando chegar ao app final
|
|
474
|
+
if not hasattr(current, "middleware_stack") and not isinstance(current, BaseHTTPMiddleware):
|
|
475
|
+
if current is not None:
|
|
476
|
+
info.append({
|
|
477
|
+
"class": type(current).__name__,
|
|
478
|
+
"module": type(current).__module__,
|
|
479
|
+
"is_app": True,
|
|
480
|
+
})
|
|
481
|
+
break
|
|
482
|
+
|
|
483
|
+
return info
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def print_middleware_stack(app: Any) -> None:
|
|
487
|
+
"""
|
|
488
|
+
Imprime a stack de middlewares formatada.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
app: FastAPI ou CoreApp
|
|
492
|
+
"""
|
|
493
|
+
info = get_middleware_stack_info(app)
|
|
494
|
+
|
|
495
|
+
print("\n📦 Middleware Stack:")
|
|
496
|
+
print("=" * 50)
|
|
497
|
+
|
|
498
|
+
for i, mw in enumerate(info):
|
|
499
|
+
is_app = mw.get("is_app", False)
|
|
500
|
+
prefix = " └─ " if is_app else f" {i+1}. "
|
|
501
|
+
name = mw.get("name", mw["class"])
|
|
502
|
+
|
|
503
|
+
if is_app:
|
|
504
|
+
print(f"{prefix}[APP] {name}")
|
|
505
|
+
else:
|
|
506
|
+
print(f"{prefix}{name}")
|
|
507
|
+
if "exclude_paths" in mw and mw["exclude_paths"]:
|
|
508
|
+
print(f" exclude: {mw['exclude_paths']}")
|
|
509
|
+
|
|
510
|
+
print("=" * 50)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# =============================================================================
|
|
514
|
+
# Pre-built Middleware Classes
|
|
515
|
+
# =============================================================================
|
|
516
|
+
|
|
517
|
+
class TimingMiddleware(BaseMiddleware):
|
|
518
|
+
"""
|
|
519
|
+
Middleware que mede tempo de resposta.
|
|
520
|
+
|
|
521
|
+
Adiciona header X-Response-Time com duração em segundos.
|
|
522
|
+
|
|
523
|
+
Usage:
|
|
524
|
+
configure_middleware([
|
|
525
|
+
"core.middleware.TimingMiddleware",
|
|
526
|
+
])
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
name = "TimingMiddleware"
|
|
530
|
+
order = 10 # Executa cedo para medir tempo total
|
|
531
|
+
|
|
532
|
+
async def before_request(self, request: Request) -> None:
|
|
533
|
+
import time
|
|
534
|
+
request.state._timing_start = time.perf_counter()
|
|
535
|
+
|
|
536
|
+
async def after_request(self, request: Request, response: Response) -> Response:
|
|
537
|
+
import time
|
|
538
|
+
start = getattr(request.state, "_timing_start", None)
|
|
539
|
+
if start is not None:
|
|
540
|
+
duration = time.perf_counter() - start
|
|
541
|
+
response.headers["X-Response-Time"] = f"{duration:.4f}s"
|
|
542
|
+
return response
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class RequestIDMiddleware(BaseMiddleware):
|
|
546
|
+
"""
|
|
547
|
+
Middleware que adiciona ID único a cada request.
|
|
548
|
+
|
|
549
|
+
Útil para tracing e logs.
|
|
550
|
+
|
|
551
|
+
Usage:
|
|
552
|
+
configure_middleware([
|
|
553
|
+
"core.middleware.RequestIDMiddleware",
|
|
554
|
+
])
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
name = "RequestIDMiddleware"
|
|
558
|
+
order = 5 # Executa muito cedo
|
|
559
|
+
|
|
560
|
+
# Nome do header para ID
|
|
561
|
+
header_name: str = "X-Request-ID"
|
|
562
|
+
|
|
563
|
+
async def before_request(self, request: Request) -> None:
|
|
564
|
+
import uuid
|
|
565
|
+
|
|
566
|
+
# Usa ID do header se fornecido, senão gera novo
|
|
567
|
+
request_id = request.headers.get(self.header_name)
|
|
568
|
+
if not request_id:
|
|
569
|
+
request_id = str(uuid.uuid4())
|
|
570
|
+
|
|
571
|
+
request.state.request_id = request_id
|
|
572
|
+
|
|
573
|
+
async def after_request(self, request: Request, response: Response) -> Response:
|
|
574
|
+
request_id = getattr(request.state, "request_id", None)
|
|
575
|
+
if request_id:
|
|
576
|
+
response.headers[self.header_name] = request_id
|
|
577
|
+
return response
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class LoggingMiddleware(BaseMiddleware):
|
|
581
|
+
"""
|
|
582
|
+
Middleware que loga requests.
|
|
583
|
+
|
|
584
|
+
Usage:
|
|
585
|
+
configure_middleware([
|
|
586
|
+
("core.middleware.LoggingMiddleware", {"log_body": False}),
|
|
587
|
+
])
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
name = "LoggingMiddleware"
|
|
591
|
+
order = 20
|
|
592
|
+
|
|
593
|
+
# Se deve logar body da request
|
|
594
|
+
log_body: bool = False
|
|
595
|
+
|
|
596
|
+
# Se deve logar headers
|
|
597
|
+
log_headers: bool = False
|
|
598
|
+
|
|
599
|
+
# Logger name
|
|
600
|
+
logger_name: str = "core.requests"
|
|
601
|
+
|
|
602
|
+
async def before_request(self, request: Request) -> None:
|
|
603
|
+
import logging
|
|
604
|
+
import time
|
|
605
|
+
|
|
606
|
+
request.state._log_start = time.perf_counter()
|
|
607
|
+
|
|
608
|
+
logger = logging.getLogger(self.logger_name)
|
|
609
|
+
|
|
610
|
+
msg = f"→ {request.method} {request.url.path}"
|
|
611
|
+
if request.query_params:
|
|
612
|
+
msg += f"?{request.query_params}"
|
|
613
|
+
|
|
614
|
+
logger.info(msg)
|
|
615
|
+
|
|
616
|
+
if self.log_headers:
|
|
617
|
+
logger.debug(f" Headers: {dict(request.headers)}")
|
|
618
|
+
|
|
619
|
+
async def after_request(self, request: Request, response: Response) -> Response:
|
|
620
|
+
import logging
|
|
621
|
+
import time
|
|
622
|
+
|
|
623
|
+
logger = logging.getLogger(self.logger_name)
|
|
624
|
+
|
|
625
|
+
start = getattr(request.state, "_log_start", None)
|
|
626
|
+
duration = ""
|
|
627
|
+
if start:
|
|
628
|
+
duration = f" [{time.perf_counter() - start:.3f}s]"
|
|
629
|
+
|
|
630
|
+
logger.info(f"← {response.status_code}{duration}")
|
|
631
|
+
|
|
632
|
+
return response
|
|
633
|
+
|
|
634
|
+
async def on_error(self, request: Request, exc: Exception) -> None:
|
|
635
|
+
import logging
|
|
636
|
+
|
|
637
|
+
logger = logging.getLogger(self.logger_name)
|
|
638
|
+
logger.error(f"✗ Error: {type(exc).__name__}: {exc}")
|
|
639
|
+
|
|
640
|
+
return None # Re-raise
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class MaintenanceModeMiddleware(BaseMiddleware):
|
|
644
|
+
"""
|
|
645
|
+
Middleware para modo de manutenção.
|
|
646
|
+
|
|
647
|
+
Retorna 503 para todas as requests quando ativado.
|
|
648
|
+
|
|
649
|
+
Usage:
|
|
650
|
+
configure_middleware([
|
|
651
|
+
("core.middleware.MaintenanceModeMiddleware", {
|
|
652
|
+
"enabled": False, # Ative quando precisar
|
|
653
|
+
"message": "Site em manutenção",
|
|
654
|
+
"allowed_ips": ["127.0.0.1"],
|
|
655
|
+
}),
|
|
656
|
+
])
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
name = "MaintenanceModeMiddleware"
|
|
660
|
+
order = 1 # Executa primeiro
|
|
661
|
+
|
|
662
|
+
# Se modo manutenção está ativo
|
|
663
|
+
maintenance_enabled: bool = False
|
|
664
|
+
|
|
665
|
+
# Mensagem de manutenção
|
|
666
|
+
message: str = "Service temporarily unavailable for maintenance"
|
|
667
|
+
|
|
668
|
+
# IPs permitidos mesmo em manutenção
|
|
669
|
+
allowed_ips: list[str] = []
|
|
670
|
+
|
|
671
|
+
# Paths permitidos mesmo em manutenção (ex: /health)
|
|
672
|
+
allowed_paths: list[str] = ["/health", "/healthz"]
|
|
673
|
+
|
|
674
|
+
async def before_request(self, request: Request) -> Response | None:
|
|
675
|
+
if not self.maintenance_enabled:
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
# Verifica paths permitidos
|
|
679
|
+
if any(request.url.path.startswith(p) for p in self.allowed_paths):
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
# Verifica IPs permitidos
|
|
683
|
+
client_ip = request.client.host if request.client else None
|
|
684
|
+
if client_ip in self.allowed_ips:
|
|
685
|
+
return None
|
|
686
|
+
|
|
687
|
+
# Retorna 503
|
|
688
|
+
from starlette.responses import JSONResponse
|
|
689
|
+
return JSONResponse(
|
|
690
|
+
status_code=503,
|
|
691
|
+
content={
|
|
692
|
+
"detail": self.message,
|
|
693
|
+
"code": "maintenance_mode",
|
|
694
|
+
},
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
class SecurityHeadersMiddleware(BaseMiddleware):
|
|
699
|
+
"""
|
|
700
|
+
Middleware que adiciona headers de segurança.
|
|
701
|
+
|
|
702
|
+
Usage:
|
|
703
|
+
configure_middleware([
|
|
704
|
+
"core.middleware.SecurityHeadersMiddleware",
|
|
705
|
+
])
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
name = "SecurityHeadersMiddleware"
|
|
709
|
+
order = 15
|
|
710
|
+
|
|
711
|
+
# Headers a adicionar
|
|
712
|
+
headers: dict[str, str] = {
|
|
713
|
+
"X-Content-Type-Options": "nosniff",
|
|
714
|
+
"X-Frame-Options": "DENY",
|
|
715
|
+
"X-XSS-Protection": "1; mode=block",
|
|
716
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
# Se deve adicionar HSTS (apenas para HTTPS)
|
|
720
|
+
enable_hsts: bool = False
|
|
721
|
+
hsts_max_age: int = 31536000 # 1 ano
|
|
722
|
+
|
|
723
|
+
async def after_request(self, request: Request, response: Response) -> Response:
|
|
724
|
+
for header, value in self.headers.items():
|
|
725
|
+
response.headers[header] = value
|
|
726
|
+
|
|
727
|
+
# HSTS apenas para HTTPS
|
|
728
|
+
if self.enable_hsts and request.url.scheme == "https":
|
|
729
|
+
response.headers["Strict-Transport-Security"] = f"max-age={self.hsts_max_age}"
|
|
730
|
+
|
|
731
|
+
return response
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# =============================================================================
|
|
735
|
+
# Built-in middleware shortcuts - update registry
|
|
736
|
+
# =============================================================================
|
|
737
|
+
|
|
738
|
+
_builtin_middlewares.update({
|
|
739
|
+
"timing": "core.middleware.TimingMiddleware",
|
|
740
|
+
"request_id": "core.middleware.RequestIDMiddleware",
|
|
741
|
+
"logging": "core.middleware.LoggingMiddleware",
|
|
742
|
+
"maintenance": "core.middleware.MaintenanceModeMiddleware",
|
|
743
|
+
"security_headers": "core.middleware.SecurityHeadersMiddleware",
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# =============================================================================
|
|
748
|
+
# Exports
|
|
749
|
+
# =============================================================================
|
|
750
|
+
|
|
751
|
+
__all__ = [
|
|
752
|
+
# Base class
|
|
753
|
+
"BaseMiddleware",
|
|
754
|
+
|
|
755
|
+
# Configuration
|
|
756
|
+
"MiddlewareConfig",
|
|
757
|
+
"configure_middleware",
|
|
758
|
+
"register_middleware",
|
|
759
|
+
"unregister_middleware",
|
|
760
|
+
"get_registered_middlewares",
|
|
761
|
+
"clear_middleware_registry",
|
|
762
|
+
"apply_middlewares",
|
|
763
|
+
|
|
764
|
+
# Debug
|
|
765
|
+
"get_middleware_stack_info",
|
|
766
|
+
"print_middleware_stack",
|
|
767
|
+
|
|
768
|
+
# Pre-built middlewares
|
|
769
|
+
"TimingMiddleware",
|
|
770
|
+
"RequestIDMiddleware",
|
|
771
|
+
"LoggingMiddleware",
|
|
772
|
+
"MaintenanceModeMiddleware",
|
|
773
|
+
"SecurityHeadersMiddleware",
|
|
774
|
+
]
|