sucuri-framework 0.0.1__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.
- sucuri_framework-0.0.1/PKG-INFO +297 -0
- sucuri_framework-0.0.1/README.md +281 -0
- sucuri_framework-0.0.1/pyproject.toml +26 -0
- sucuri_framework-0.0.1/src/sucuri_framework/__init__.py +3 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/__init__.py +0 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/ast_utils.py +41 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/cli.py +47 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/commands_db.py +34 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/commands_generate.py +548 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/commands_new.py +102 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/commands_run.py +21 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/commands_task.py +13 -0
- sucuri_framework-0.0.1/src/sucuri_framework/cli/core.py +0 -0
- sucuri_framework-0.0.1/src/sucuri_framework/config.py +16 -0
- sucuri_framework-0.0.1/src/sucuri_framework/core.py +127 -0
- sucuri_framework-0.0.1/src/sucuri_framework/database.py +21 -0
- sucuri_framework-0.0.1/src/sucuri_framework/di.py +113 -0
- sucuri_framework-0.0.1/src/sucuri_framework/exceptions.py +50 -0
- sucuri_framework-0.0.1/src/sucuri_framework/guards.py +48 -0
- sucuri_framework-0.0.1/src/sucuri_framework/models.py +7 -0
- sucuri_framework-0.0.1/src/sucuri_framework/module.py +25 -0
- sucuri_framework-0.0.1/src/sucuri_framework/py.typed +0 -0
- sucuri_framework-0.0.1/src/sucuri_framework/routing.py +42 -0
- sucuri_framework-0.0.1/src/sucuri_framework/schema.py +3 -0
- sucuri_framework-0.0.1/src/sucuri_framework/tasks.py +3 -0
- sucuri_framework-0.0.1/src/sucuri_framework/testing.py +72 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sucuri-framework
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Framework Web API - Sucuri Project
|
|
5
|
+
Author: Antonio Henrique Machado
|
|
6
|
+
Author-email: Antonio Henrique Machado <machadoah@proton.me>
|
|
7
|
+
Requires-Dist: aerich>=0.8.1
|
|
8
|
+
Requires-Dist: fastapi>=0.137.1
|
|
9
|
+
Requires-Dist: pydantic>=2.13.4
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.14.1
|
|
11
|
+
Requires-Dist: tortoise-orm>=1.1.7
|
|
12
|
+
Requires-Dist: typer>=0.26.7
|
|
13
|
+
Requires-Dist: taskipy>=1.14.1
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# 🐍 Sucuri Framework
|
|
18
|
+
|
|
19
|
+
O **Sucuri** é um framework Web Python assíncrono e opinativo, criado para simplificar o desenvolvimento de APIs. Ele atua como uma fachada elegante sobre **FastAPI**, **Tortoise ORM** e **Pydantic**, abstraindo a complexidade dessas bibliotecas e focando em uma arquitetura limpa, rápida e direta, com "baterias inclusas".
|
|
20
|
+
|
|
21
|
+
## Filosofia
|
|
22
|
+
|
|
23
|
+
O desenvolvedor final do Sucuri **não** lida diretamente com os pacotes ASGI originais. O isolamento garante uma experiência padronizada, onde você interage unicamente através dos pacotes `sucuri.*`. O framework dita como as pastas são estruturadas e como a injeção de dependências e roteamento são gerenciados.
|
|
24
|
+
|
|
25
|
+
## Instalação
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Instale a partir do repositório/diretório local usando o uv ou pip
|
|
29
|
+
uv pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🛠️ O Guia Prático (Como usar)
|
|
35
|
+
|
|
36
|
+
A única forma oficial de gerir seu projeto Sucuri é através da CLI `sucuri`.
|
|
37
|
+
|
|
38
|
+
### 1. Criar um Novo Projeto
|
|
39
|
+
O gerador oficial montará o esqueleto do projeto com as pastas necessárias (controllers, models, schemas).
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
sucuri new meu_projeto
|
|
43
|
+
cd meu_projeto
|
|
44
|
+
```
|
|
45
|
+
Isso criará uma pasta `app/` contendo `main.py` e os submódulos, já pré-configurado com a base de dados.
|
|
46
|
+
|
|
47
|
+
### 2. Gerar Recursos
|
|
48
|
+
Para evitar código repetitivo, use o gerador de recursos (Resource). Ele criará o Model, o Schema, o Service, o Controller e o Módulo correspondente, amarrando tudo automaticamente.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sucuri generate resource Usuario
|
|
52
|
+
```
|
|
53
|
+
Você verá os seguintes arquivos gerados:
|
|
54
|
+
- `app/models/usuario.py` (Modelo Active Record)
|
|
55
|
+
- `app/schemas/usuario.py` (Validação de Dados)
|
|
56
|
+
- `app/services/usuario.py` (Lógica de negócios via DI)
|
|
57
|
+
- `app/controllers/usuario.py` (Rotas RESTful)
|
|
58
|
+
- `app/modules/usuario.py` (Módulo autônomo aglomerando Controllers e Services)
|
|
59
|
+
|
|
60
|
+
✨ **Magia do Framework**: O arquivo `app/app_module.py` será lido via AST e modificado silenciosamente, injetando e conectando o novo `UsuarioModule` direto na árvore principal de dependências! Sem a necessidade de importações manuais de rotas.
|
|
61
|
+
|
|
62
|
+
**Nota:** Você também pode gerar componentes individuais com: `sucuri generate module`, `sucuri generate controller`, `sucuri generate service`, `sucuri generate guard`, `sucuri generate filter`, `sucuri generate task`, `sucuri generate model` ou `sucuri generate schema`.
|
|
63
|
+
|
|
64
|
+
### 3. Migrações de Banco de Dados
|
|
65
|
+
O Sucuri já vem pré-configurado para rastrear alterações no banco de dados.
|
|
66
|
+
|
|
67
|
+
*Sempre que alterar um Model:* Crie e aplique uma migração.
|
|
68
|
+
```bash
|
|
69
|
+
sucuri db migrate "Adicionado campo idade no Usuario"
|
|
70
|
+
sucuri db upgrade
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. Rodar o Servidor
|
|
74
|
+
O framework utiliza a CLI moderna do FastAPI por debaixo dos panos. Você tem duas opções:
|
|
75
|
+
|
|
76
|
+
**Modo de Desenvolvimento (Com Hot Reloading):**
|
|
77
|
+
```bash
|
|
78
|
+
sucuri dev
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Modo de Produção (Otimizado, sem Hot Reloading):**
|
|
82
|
+
```bash
|
|
83
|
+
sucuri run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Acesse `http://127.0.0.1:8000/docs` para ver a sua documentação automática interativa Swagger gerada pela nossa abstração de rotas!
|
|
87
|
+
|
|
88
|
+
### 5. Scripts Customizados (Taskipy)
|
|
89
|
+
Você pode rodar comandos curtos de automação com o executor integrado de scripts! Os scripts devem ser definidos na seção `[tool.taskipy.tasks]` do arquivo `pyproject.toml` gerado pelo framework.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Executando um script customizado com o nome "format"
|
|
93
|
+
sucuri task format
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 🧩 Referência de Componentes
|
|
99
|
+
|
|
100
|
+
### Aplicação (Core) e Tratamento de Erros
|
|
101
|
+
O `app/main.py` é minimalista. Ele inicializa a Fachada, varre a árvore de módulos e conecta o banco. Opcionalmente, permite interceptadores globais de erros.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from sucuri_framework.core import SucuriApp
|
|
105
|
+
from app.app_module import AppModule
|
|
106
|
+
from app.filters.database_filter import DatabaseNotFoundFilter
|
|
107
|
+
|
|
108
|
+
app = SucuriApp(title="Minha API", version="1.0.0")
|
|
109
|
+
|
|
110
|
+
# Registra um Exception Filter global para erros não tratados
|
|
111
|
+
app.use_global_filters(DatabaseNotFoundFilter())
|
|
112
|
+
|
|
113
|
+
# Inicializa os controllers e dependências varrendo a árvore de módulos
|
|
114
|
+
app.bootstrap(AppModule)
|
|
115
|
+
|
|
116
|
+
# Conecta o banco de dados magicamente (lê as configurações internas do projeto)
|
|
117
|
+
app.connect_database()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Organização em Módulos (@Module)
|
|
121
|
+
Projetos crescem. Para evitar bagunça global, agrupamos responsabilidades e importações em Módulos, criando uma hierarquia limpa a partir do `AppModule`.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from sucuri_framework.module import Module
|
|
125
|
+
from app.controllers.produtos import produtos_controller
|
|
126
|
+
|
|
127
|
+
produtos_module = Module("produtos")
|
|
128
|
+
produtos_module.include_controller(produtos_controller)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Modelagem (Active Record)
|
|
132
|
+
Utilizamos o Tortoise ORM nativamente. Operações como `.create()`, `.filter()`, e `.get()` são providas de forma assíncrona.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from sucuri_framework.models import Model, fields
|
|
136
|
+
|
|
137
|
+
class Produto(Model):
|
|
138
|
+
nome = fields.CharField(max_length=255)
|
|
139
|
+
|
|
140
|
+
class Meta:
|
|
141
|
+
table = "produtos"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Validação (Schemas)
|
|
145
|
+
O framework abstrai as rotas através de `Controllers` com uma interface fluida.
|
|
146
|
+
Os Schemas do Pydantic interagem perfeitamente com o ORM. Para retornar dados do Banco diretamente e esconder campos sensíveis, basta usar o `response_model` no Controller e garantir que o Schema tenha a flag `from_attributes=True`.
|
|
147
|
+
|
|
148
|
+
### 1. Criando os Schemas
|
|
149
|
+
```python
|
|
150
|
+
# app/schemas/produtos.py
|
|
151
|
+
from sucuri_framework.schema import Schema
|
|
152
|
+
|
|
153
|
+
class ProdutoSchema(Schema):
|
|
154
|
+
nome: str
|
|
155
|
+
preco: float
|
|
156
|
+
|
|
157
|
+
class ProdutoPublicoSchema(Schema):
|
|
158
|
+
id: int
|
|
159
|
+
nome: str
|
|
160
|
+
preco: float
|
|
161
|
+
|
|
162
|
+
class Config:
|
|
163
|
+
from_attributes = True # Permite leitura direta de objetos Tortoise ORM
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 2. O Roteador e a Ocultação Mágica
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# app/controllers/produtos.py
|
|
170
|
+
from sucuri_framework.routing import Controller
|
|
171
|
+
from app.models.produtos import Produto
|
|
172
|
+
from app.schemas.produtos import ProdutoPublicoSchema
|
|
173
|
+
|
|
174
|
+
produtos_controller = Controller(prefix="/api/produtos")
|
|
175
|
+
|
|
176
|
+
@produtos_controller.get("/", response_model=list[ProdutoPublicoSchema])
|
|
177
|
+
async def listar():
|
|
178
|
+
# O Pydantic oculta automaticamente qualquer campo extra do banco de dados
|
|
179
|
+
# (ex: log_auditoria, senha, flags internas) que não estiver no Schema!
|
|
180
|
+
return await Produto.all()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Injeção de Dependências Explícita (DI)
|
|
184
|
+
Regras de negócios devem residir em classes separadas. Seguindo a premissa de que "explícito é melhor que implícito", o Sucuri utiliza um contêiner nativo super-rápido para injetar dependências nas suas rotas.
|
|
185
|
+
|
|
186
|
+
Basta marcar o seu Service com `@Injectable()`. Por padrão, eles são tratados como **Singletons**. O contêiner do Sucuri suporta **Injeção em Cascata** (serviços injetando outros serviços no construtor) e previne falhas com **Detecção de Dependência Circular**.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# app/services/email.py
|
|
190
|
+
from sucuri_framework.di import Injectable
|
|
191
|
+
|
|
192
|
+
@Injectable()
|
|
193
|
+
class EmailService:
|
|
194
|
+
async def enviar(self): pass
|
|
195
|
+
|
|
196
|
+
# app/services/produtos.py
|
|
197
|
+
from sucuri_framework.di import Injectable, Inject
|
|
198
|
+
from app.services.email import EmailService
|
|
199
|
+
|
|
200
|
+
@Injectable()
|
|
201
|
+
class ProdutoService:
|
|
202
|
+
def __init__(self, email: EmailService = Inject()):
|
|
203
|
+
self.email = email
|
|
204
|
+
|
|
205
|
+
async def validar_estoque(self, id: int):
|
|
206
|
+
await self.email.enviar()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3. Controller Dinâmico
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# app/controllers/produtos.py
|
|
213
|
+
from sucuri_framework.routing import Controller
|
|
214
|
+
from sucuri_framework.di import Inject
|
|
215
|
+
from app.services.produtos import ProdutoService
|
|
216
|
+
|
|
217
|
+
produtos_controller = Controller(prefix="/api/produtos")
|
|
218
|
+
|
|
219
|
+
@produtos_controller.post("/")
|
|
220
|
+
async def criar(payload: dict, service: ProdutoService = Inject()):
|
|
221
|
+
# O framework usa a dica de tipo (ProdutoService) e injeta a
|
|
222
|
+
# instância Singleton correspondente dinamicamente pelo FastAPI.
|
|
223
|
+
await service.validar_estoque(payload["id"])
|
|
224
|
+
return {"status": "sucesso"}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Autorização e Proteção de Rotas (Guards)
|
|
228
|
+
Guards verificam se a requisição pode acessar a rota antes que o Controller seja executado.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from sucuri_framework.core import Request
|
|
232
|
+
from sucuri_framework.guards import BaseGuard, UseGuards
|
|
233
|
+
from sucuri_framework.exceptions import HTTPException
|
|
234
|
+
|
|
235
|
+
class AuthGuard(BaseGuard):
|
|
236
|
+
async def can_activate(self, request: Request) -> bool:
|
|
237
|
+
if not request.headers.get("Authorization"):
|
|
238
|
+
raise HTTPException(status_code=401, detail="Token ausente")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
@produtos_controller.get("/vip")
|
|
242
|
+
@UseGuards(AuthGuard)
|
|
243
|
+
async def rota_protegida():
|
|
244
|
+
return {"mensagem": "Protegido"}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Tarefas em Segundo Plano (Background Worker)
|
|
248
|
+
O framework injeta gerenciadores de tarefas para você realizar operações lentas sem bloquear o retorno HTTP.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from sucuri_framework.tasks import BackgroundWorker
|
|
252
|
+
|
|
253
|
+
async def enviar_email(email: str):
|
|
254
|
+
... # lógica demorada
|
|
255
|
+
|
|
256
|
+
@produtos_controller.post("/notificar")
|
|
257
|
+
async def notificar(email: str, worker: BackgroundWorker):
|
|
258
|
+
worker.add_task(enviar_email, email)
|
|
259
|
+
return {"status": "enfileirado"}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Filtros de Exceção (Exception Filters)
|
|
263
|
+
Traduza exceções difíceis ou de terceiros em payloads HTTP controlados.
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from sucuri_framework.exceptions import ExceptionFilter, Catch
|
|
267
|
+
from tortoise.exceptions import DoesNotExist
|
|
268
|
+
from fastapi.responses import JSONResponse
|
|
269
|
+
|
|
270
|
+
@Catch(DoesNotExist)
|
|
271
|
+
class DatabaseNotFoundFilter(ExceptionFilter):
|
|
272
|
+
async def catch(self, exception: Exception, request):
|
|
273
|
+
return JSONResponse(status_code=404, content={"error": "Não encontrado"})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Utilitários de Teste E2E (Test Client)
|
|
277
|
+
O framework oferece o `SucuriTestClient` para que você não precise gerenciar o Event Loop ou as conexões do banco de dados na mão. Ele faz o setup do Tortoise (em memória) e já lida com chamadas HTTP 100% assíncronas internamente!
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import pytest
|
|
281
|
+
from sucuri_framework.testing import SucuriTestClient
|
|
282
|
+
from app.main import app
|
|
283
|
+
from app.services.produtos import ProdutoService
|
|
284
|
+
|
|
285
|
+
@pytest.mark.asyncio
|
|
286
|
+
async def test_envio(monkeypatch):
|
|
287
|
+
# Sobrescreve o método da classe nativamente (Mock)
|
|
288
|
+
async def mock_validar(*args):
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
monkeypatch.setattr(ProdutoService, "validar_estoque", mock_validar)
|
|
292
|
+
|
|
293
|
+
# SucuriTestClient inicia o BD e serve a aplicação de forma assíncrona
|
|
294
|
+
async with SucuriTestClient(app, db_url="sqlite://:memory:") as client:
|
|
295
|
+
response = await client.post("/api/produtos/", json={"id": 10})
|
|
296
|
+
assert response.status_code == 200
|
|
297
|
+
```
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# 🐍 Sucuri Framework
|
|
2
|
+
|
|
3
|
+
O **Sucuri** é um framework Web Python assíncrono e opinativo, criado para simplificar o desenvolvimento de APIs. Ele atua como uma fachada elegante sobre **FastAPI**, **Tortoise ORM** e **Pydantic**, abstraindo a complexidade dessas bibliotecas e focando em uma arquitetura limpa, rápida e direta, com "baterias inclusas".
|
|
4
|
+
|
|
5
|
+
## Filosofia
|
|
6
|
+
|
|
7
|
+
O desenvolvedor final do Sucuri **não** lida diretamente com os pacotes ASGI originais. O isolamento garante uma experiência padronizada, onde você interage unicamente através dos pacotes `sucuri.*`. O framework dita como as pastas são estruturadas e como a injeção de dependências e roteamento são gerenciados.
|
|
8
|
+
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Instale a partir do repositório/diretório local usando o uv ou pip
|
|
13
|
+
uv pip install -e .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🛠️ O Guia Prático (Como usar)
|
|
19
|
+
|
|
20
|
+
A única forma oficial de gerir seu projeto Sucuri é através da CLI `sucuri`.
|
|
21
|
+
|
|
22
|
+
### 1. Criar um Novo Projeto
|
|
23
|
+
O gerador oficial montará o esqueleto do projeto com as pastas necessárias (controllers, models, schemas).
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
sucuri new meu_projeto
|
|
27
|
+
cd meu_projeto
|
|
28
|
+
```
|
|
29
|
+
Isso criará uma pasta `app/` contendo `main.py` e os submódulos, já pré-configurado com a base de dados.
|
|
30
|
+
|
|
31
|
+
### 2. Gerar Recursos
|
|
32
|
+
Para evitar código repetitivo, use o gerador de recursos (Resource). Ele criará o Model, o Schema, o Service, o Controller e o Módulo correspondente, amarrando tudo automaticamente.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sucuri generate resource Usuario
|
|
36
|
+
```
|
|
37
|
+
Você verá os seguintes arquivos gerados:
|
|
38
|
+
- `app/models/usuario.py` (Modelo Active Record)
|
|
39
|
+
- `app/schemas/usuario.py` (Validação de Dados)
|
|
40
|
+
- `app/services/usuario.py` (Lógica de negócios via DI)
|
|
41
|
+
- `app/controllers/usuario.py` (Rotas RESTful)
|
|
42
|
+
- `app/modules/usuario.py` (Módulo autônomo aglomerando Controllers e Services)
|
|
43
|
+
|
|
44
|
+
✨ **Magia do Framework**: O arquivo `app/app_module.py` será lido via AST e modificado silenciosamente, injetando e conectando o novo `UsuarioModule` direto na árvore principal de dependências! Sem a necessidade de importações manuais de rotas.
|
|
45
|
+
|
|
46
|
+
**Nota:** Você também pode gerar componentes individuais com: `sucuri generate module`, `sucuri generate controller`, `sucuri generate service`, `sucuri generate guard`, `sucuri generate filter`, `sucuri generate task`, `sucuri generate model` ou `sucuri generate schema`.
|
|
47
|
+
|
|
48
|
+
### 3. Migrações de Banco de Dados
|
|
49
|
+
O Sucuri já vem pré-configurado para rastrear alterações no banco de dados.
|
|
50
|
+
|
|
51
|
+
*Sempre que alterar um Model:* Crie e aplique uma migração.
|
|
52
|
+
```bash
|
|
53
|
+
sucuri db migrate "Adicionado campo idade no Usuario"
|
|
54
|
+
sucuri db upgrade
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 4. Rodar o Servidor
|
|
58
|
+
O framework utiliza a CLI moderna do FastAPI por debaixo dos panos. Você tem duas opções:
|
|
59
|
+
|
|
60
|
+
**Modo de Desenvolvimento (Com Hot Reloading):**
|
|
61
|
+
```bash
|
|
62
|
+
sucuri dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Modo de Produção (Otimizado, sem Hot Reloading):**
|
|
66
|
+
```bash
|
|
67
|
+
sucuri run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Acesse `http://127.0.0.1:8000/docs` para ver a sua documentação automática interativa Swagger gerada pela nossa abstração de rotas!
|
|
71
|
+
|
|
72
|
+
### 5. Scripts Customizados (Taskipy)
|
|
73
|
+
Você pode rodar comandos curtos de automação com o executor integrado de scripts! Os scripts devem ser definidos na seção `[tool.taskipy.tasks]` do arquivo `pyproject.toml` gerado pelo framework.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Executando um script customizado com o nome "format"
|
|
77
|
+
sucuri task format
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🧩 Referência de Componentes
|
|
83
|
+
|
|
84
|
+
### Aplicação (Core) e Tratamento de Erros
|
|
85
|
+
O `app/main.py` é minimalista. Ele inicializa a Fachada, varre a árvore de módulos e conecta o banco. Opcionalmente, permite interceptadores globais de erros.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from sucuri_framework.core import SucuriApp
|
|
89
|
+
from app.app_module import AppModule
|
|
90
|
+
from app.filters.database_filter import DatabaseNotFoundFilter
|
|
91
|
+
|
|
92
|
+
app = SucuriApp(title="Minha API", version="1.0.0")
|
|
93
|
+
|
|
94
|
+
# Registra um Exception Filter global para erros não tratados
|
|
95
|
+
app.use_global_filters(DatabaseNotFoundFilter())
|
|
96
|
+
|
|
97
|
+
# Inicializa os controllers e dependências varrendo a árvore de módulos
|
|
98
|
+
app.bootstrap(AppModule)
|
|
99
|
+
|
|
100
|
+
# Conecta o banco de dados magicamente (lê as configurações internas do projeto)
|
|
101
|
+
app.connect_database()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Organização em Módulos (@Module)
|
|
105
|
+
Projetos crescem. Para evitar bagunça global, agrupamos responsabilidades e importações em Módulos, criando uma hierarquia limpa a partir do `AppModule`.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from sucuri_framework.module import Module
|
|
109
|
+
from app.controllers.produtos import produtos_controller
|
|
110
|
+
|
|
111
|
+
produtos_module = Module("produtos")
|
|
112
|
+
produtos_module.include_controller(produtos_controller)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Modelagem (Active Record)
|
|
116
|
+
Utilizamos o Tortoise ORM nativamente. Operações como `.create()`, `.filter()`, e `.get()` são providas de forma assíncrona.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from sucuri_framework.models import Model, fields
|
|
120
|
+
|
|
121
|
+
class Produto(Model):
|
|
122
|
+
nome = fields.CharField(max_length=255)
|
|
123
|
+
|
|
124
|
+
class Meta:
|
|
125
|
+
table = "produtos"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Validação (Schemas)
|
|
129
|
+
O framework abstrai as rotas através de `Controllers` com uma interface fluida.
|
|
130
|
+
Os Schemas do Pydantic interagem perfeitamente com o ORM. Para retornar dados do Banco diretamente e esconder campos sensíveis, basta usar o `response_model` no Controller e garantir que o Schema tenha a flag `from_attributes=True`.
|
|
131
|
+
|
|
132
|
+
### 1. Criando os Schemas
|
|
133
|
+
```python
|
|
134
|
+
# app/schemas/produtos.py
|
|
135
|
+
from sucuri_framework.schema import Schema
|
|
136
|
+
|
|
137
|
+
class ProdutoSchema(Schema):
|
|
138
|
+
nome: str
|
|
139
|
+
preco: float
|
|
140
|
+
|
|
141
|
+
class ProdutoPublicoSchema(Schema):
|
|
142
|
+
id: int
|
|
143
|
+
nome: str
|
|
144
|
+
preco: float
|
|
145
|
+
|
|
146
|
+
class Config:
|
|
147
|
+
from_attributes = True # Permite leitura direta de objetos Tortoise ORM
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2. O Roteador e a Ocultação Mágica
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# app/controllers/produtos.py
|
|
154
|
+
from sucuri_framework.routing import Controller
|
|
155
|
+
from app.models.produtos import Produto
|
|
156
|
+
from app.schemas.produtos import ProdutoPublicoSchema
|
|
157
|
+
|
|
158
|
+
produtos_controller = Controller(prefix="/api/produtos")
|
|
159
|
+
|
|
160
|
+
@produtos_controller.get("/", response_model=list[ProdutoPublicoSchema])
|
|
161
|
+
async def listar():
|
|
162
|
+
# O Pydantic oculta automaticamente qualquer campo extra do banco de dados
|
|
163
|
+
# (ex: log_auditoria, senha, flags internas) que não estiver no Schema!
|
|
164
|
+
return await Produto.all()
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Injeção de Dependências Explícita (DI)
|
|
168
|
+
Regras de negócios devem residir em classes separadas. Seguindo a premissa de que "explícito é melhor que implícito", o Sucuri utiliza um contêiner nativo super-rápido para injetar dependências nas suas rotas.
|
|
169
|
+
|
|
170
|
+
Basta marcar o seu Service com `@Injectable()`. Por padrão, eles são tratados como **Singletons**. O contêiner do Sucuri suporta **Injeção em Cascata** (serviços injetando outros serviços no construtor) e previne falhas com **Detecção de Dependência Circular**.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# app/services/email.py
|
|
174
|
+
from sucuri_framework.di import Injectable
|
|
175
|
+
|
|
176
|
+
@Injectable()
|
|
177
|
+
class EmailService:
|
|
178
|
+
async def enviar(self): pass
|
|
179
|
+
|
|
180
|
+
# app/services/produtos.py
|
|
181
|
+
from sucuri_framework.di import Injectable, Inject
|
|
182
|
+
from app.services.email import EmailService
|
|
183
|
+
|
|
184
|
+
@Injectable()
|
|
185
|
+
class ProdutoService:
|
|
186
|
+
def __init__(self, email: EmailService = Inject()):
|
|
187
|
+
self.email = email
|
|
188
|
+
|
|
189
|
+
async def validar_estoque(self, id: int):
|
|
190
|
+
await self.email.enviar()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 3. Controller Dinâmico
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# app/controllers/produtos.py
|
|
197
|
+
from sucuri_framework.routing import Controller
|
|
198
|
+
from sucuri_framework.di import Inject
|
|
199
|
+
from app.services.produtos import ProdutoService
|
|
200
|
+
|
|
201
|
+
produtos_controller = Controller(prefix="/api/produtos")
|
|
202
|
+
|
|
203
|
+
@produtos_controller.post("/")
|
|
204
|
+
async def criar(payload: dict, service: ProdutoService = Inject()):
|
|
205
|
+
# O framework usa a dica de tipo (ProdutoService) e injeta a
|
|
206
|
+
# instância Singleton correspondente dinamicamente pelo FastAPI.
|
|
207
|
+
await service.validar_estoque(payload["id"])
|
|
208
|
+
return {"status": "sucesso"}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Autorização e Proteção de Rotas (Guards)
|
|
212
|
+
Guards verificam se a requisição pode acessar a rota antes que o Controller seja executado.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from sucuri_framework.core import Request
|
|
216
|
+
from sucuri_framework.guards import BaseGuard, UseGuards
|
|
217
|
+
from sucuri_framework.exceptions import HTTPException
|
|
218
|
+
|
|
219
|
+
class AuthGuard(BaseGuard):
|
|
220
|
+
async def can_activate(self, request: Request) -> bool:
|
|
221
|
+
if not request.headers.get("Authorization"):
|
|
222
|
+
raise HTTPException(status_code=401, detail="Token ausente")
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
@produtos_controller.get("/vip")
|
|
226
|
+
@UseGuards(AuthGuard)
|
|
227
|
+
async def rota_protegida():
|
|
228
|
+
return {"mensagem": "Protegido"}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Tarefas em Segundo Plano (Background Worker)
|
|
232
|
+
O framework injeta gerenciadores de tarefas para você realizar operações lentas sem bloquear o retorno HTTP.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from sucuri_framework.tasks import BackgroundWorker
|
|
236
|
+
|
|
237
|
+
async def enviar_email(email: str):
|
|
238
|
+
... # lógica demorada
|
|
239
|
+
|
|
240
|
+
@produtos_controller.post("/notificar")
|
|
241
|
+
async def notificar(email: str, worker: BackgroundWorker):
|
|
242
|
+
worker.add_task(enviar_email, email)
|
|
243
|
+
return {"status": "enfileirado"}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Filtros de Exceção (Exception Filters)
|
|
247
|
+
Traduza exceções difíceis ou de terceiros em payloads HTTP controlados.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from sucuri_framework.exceptions import ExceptionFilter, Catch
|
|
251
|
+
from tortoise.exceptions import DoesNotExist
|
|
252
|
+
from fastapi.responses import JSONResponse
|
|
253
|
+
|
|
254
|
+
@Catch(DoesNotExist)
|
|
255
|
+
class DatabaseNotFoundFilter(ExceptionFilter):
|
|
256
|
+
async def catch(self, exception: Exception, request):
|
|
257
|
+
return JSONResponse(status_code=404, content={"error": "Não encontrado"})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Utilitários de Teste E2E (Test Client)
|
|
261
|
+
O framework oferece o `SucuriTestClient` para que você não precise gerenciar o Event Loop ou as conexões do banco de dados na mão. Ele faz o setup do Tortoise (em memória) e já lida com chamadas HTTP 100% assíncronas internamente!
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
import pytest
|
|
265
|
+
from sucuri_framework.testing import SucuriTestClient
|
|
266
|
+
from app.main import app
|
|
267
|
+
from app.services.produtos import ProdutoService
|
|
268
|
+
|
|
269
|
+
@pytest.mark.asyncio
|
|
270
|
+
async def test_envio(monkeypatch):
|
|
271
|
+
# Sobrescreve o método da classe nativamente (Mock)
|
|
272
|
+
async def mock_validar(*args):
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
monkeypatch.setattr(ProdutoService, "validar_estoque", mock_validar)
|
|
276
|
+
|
|
277
|
+
# SucuriTestClient inicia o BD e serve a aplicação de forma assíncrona
|
|
278
|
+
async with SucuriTestClient(app, db_url="sqlite://:memory:") as client:
|
|
279
|
+
response = await client.post("/api/produtos/", json={"id": 10})
|
|
280
|
+
assert response.status_code == 200
|
|
281
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sucuri-framework"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Framework Web API - Sucuri Project"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Antonio Henrique Machado", email = "machadoah@proton.me" }]
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"aerich>=0.8.1",
|
|
10
|
+
"fastapi>=0.137.1",
|
|
11
|
+
"pydantic>=2.13.4",
|
|
12
|
+
"pydantic-settings>=2.14.1",
|
|
13
|
+
"tortoise-orm>=1.1.7",
|
|
14
|
+
"typer>=0.26.7",
|
|
15
|
+
"taskipy>=1.14.1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
sucuri = "sucuri.cli.cli:app"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.11.13,<0.12.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = ["httpx>=0.28.1", "pytest>=9.1.0", "pytest-asyncio>=1.4.0"]
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def inject_module_into_app(app_module_path: Path, import_path: str, module_var: str) -> bool:
|
|
5
|
+
"""
|
|
6
|
+
Injeta cirurgicamente a importação e a declaração de um módulo no arquivo
|
|
7
|
+
app_module.py usando a sintaxe de instância (ex: app_module.include_module(...))
|
|
8
|
+
|
|
9
|
+
Retorna True se alterou, False se já existia.
|
|
10
|
+
"""
|
|
11
|
+
if not app_module_path.exists():
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
content = app_module_path.read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
# Previne duplicidade
|
|
17
|
+
if f"import {module_var}" in content or module_var in content:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
# 1. Injetar o Import no topo do arquivo (após o último import)
|
|
21
|
+
import_statement = f"from {import_path} import {module_var}\n"
|
|
22
|
+
|
|
23
|
+
# Achar o último import para colocar logo depois
|
|
24
|
+
import_matches = list(re.finditer(r'^import .*|^from .* import .*', content, flags=re.MULTILINE))
|
|
25
|
+
if import_matches:
|
|
26
|
+
last_import = import_matches[-1]
|
|
27
|
+
insert_pos = last_import.end() + 1
|
|
28
|
+
content = content[:insert_pos] + import_statement + content[insert_pos:]
|
|
29
|
+
else:
|
|
30
|
+
# Se não houver nenhum import, joga no topo
|
|
31
|
+
content = import_statement + "\n" + content
|
|
32
|
+
|
|
33
|
+
# 2. Injetar a chamada .include_module no final do arquivo
|
|
34
|
+
call_statement = f"app_module.include_module({module_var})\n"
|
|
35
|
+
|
|
36
|
+
if not content.endswith('\n'):
|
|
37
|
+
content += '\n'
|
|
38
|
+
content += call_statement
|
|
39
|
+
|
|
40
|
+
app_module_path.write_text(new_content := content, encoding="utf-8")
|
|
41
|
+
return True
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from importlib import metadata
|
|
3
|
+
|
|
4
|
+
from sucuri_framework.cli.commands_run import run_server_dev, run_server_prod
|
|
5
|
+
from sucuri_framework.cli.commands_new import new_project
|
|
6
|
+
from sucuri_framework.cli.commands_generate import generate_app
|
|
7
|
+
from sucuri_framework.cli.commands_db import db_app
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="CLI oficial do Sucuri Framework")
|
|
10
|
+
|
|
11
|
+
# Adiciona grupos de comando
|
|
12
|
+
app.add_typer(generate_app, name="generate")
|
|
13
|
+
app.add_typer(db_app, name="db")
|
|
14
|
+
|
|
15
|
+
@app.command(name="version")
|
|
16
|
+
def _version():
|
|
17
|
+
"""Mostra a versão atual do Sucuri."""
|
|
18
|
+
try:
|
|
19
|
+
versao = metadata.version("sucuri")
|
|
20
|
+
print(f"🐍 Sucuri Framework v{versao}")
|
|
21
|
+
except metadata.PackageNotFoundError:
|
|
22
|
+
print("🐍 Sucuri Framework (Versão não encontrada)")
|
|
23
|
+
|
|
24
|
+
@app.command(name="dev")
|
|
25
|
+
def _dev():
|
|
26
|
+
"""Iniciar o Servidor de Desenvolvimento (Hot Reload)."""
|
|
27
|
+
run_server_dev()
|
|
28
|
+
|
|
29
|
+
@app.command(name="run")
|
|
30
|
+
def _run():
|
|
31
|
+
"""Iniciar o Servidor de Produção."""
|
|
32
|
+
run_server_prod()
|
|
33
|
+
|
|
34
|
+
from sucuri_framework.cli.commands_task import run_task
|
|
35
|
+
|
|
36
|
+
@app.command(name="task")
|
|
37
|
+
def _task(nome_da_tarefa: str):
|
|
38
|
+
"""Rodar scripts de automação customizados definidos no pyproject.toml."""
|
|
39
|
+
run_task(nome_da_tarefa)
|
|
40
|
+
|
|
41
|
+
@app.command(name="new")
|
|
42
|
+
def _new(nome_do_projeto: str):
|
|
43
|
+
"""Criar um projeto novo."""
|
|
44
|
+
new_project(nome_do_projeto)
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
app()
|