Uma aplicação FastAPI que realiza simulações de Monte Carlo para prever o número de semanas necessárias para concluir um backlog, utilizando dados históricos de throughput. A API utiliza ProcessPoolExecutor para paralelizar cálculos intensivos e oferece validação de dados com Pydantic.
- Como Funciona
- Instalação
- Como Subir a Aplicação
- Entendendo Uvicorn
- Configuração do ProcessPoolExecutor
- Lógica do Forecast API
- Como Utilizar
- Testes
- Estrutura de Diretórios
Você tem um backlog de trabalho e quer saber: "Quantas semanas vou levar para concluir?"
O desafio é que a produtividade (throughput) varia de semana para semana. Então, em vez de fazer uma única previsão, usamos Monte Carlo para rodar múltiplas simulações com variações aleatórias.
-
Para cada simulação:
- Sorteamos um backlog aleatório entre o mínimo e máximo informado
- Semana após semana, sorteamos um throughput da sua lista histórica
- Acumulamos o throughput até cobrir o backlog
- Contamos quantas semanas levou
-
Depois de N simulações:
- Calculamos percentis (50%, 75%, 85%, 95%)
- Você sabe: "em 50% dos casos levo 5 semanas, em 95% levo 10 semanas"
Com 1.000 ou 10.000 simulações, o cálculo fica pesado. O ProcessPoolExecutor divide o trabalho entre os núcleos do processador, rodando tudo em paralelo.
- Python 3.13+ (recomendado)
- pip
- Docker (opcional)
-
Clone ou copie o projeto
cd seu-projeto -
Instale as dependências
pip install --upgrade pip pip install -r requirements.txt
Não precisa de nada além do Docker:
docker-compose up --builduvicorn main:app --reload --host 0.0.0.0 --port 8000A API estará disponível em: http://localhost:8000
Explicação dos parâmetros:
main:app→ importaappdo arquivomain.py--reload→ reinicia ao detectar mudanças nos arquivos (desenvolvimento)--host 0.0.0.0→ aceita requisições de qualquer IP--port 8000→ porta 8000
docker-compose up --buildO que acontece:
- Docker constrói a imagem usando o
Dockerfile - Instala as dependências do
requirements.txt - Executa o uvicorn dentro do container
- Expõe a porta 8000
Verifique os logs:
docker-compose logs -f dev-backendPara parar:
docker-compose downUvicorn é um servidor ASGI (Asynchronous Server Gateway Interface) que:
- Roda aplicações FastAPI
- Gerencia requisições HTTP
- Executa código assíncrono (async/await)
- Implementa WebSockets
Cliente HTTP
↓
Uvicorn (servidor)
↓
FastAPI (aplicação)
↓
Rotas e handlers
Quando você inicia o uvicorn, ele:
- Startup: Executa
lifespan(contexto manager emconfig.py) - Running: Aguarda requisições
- Shutdown: Limpa recursos
# Em config.py, o lifespan gerencia isso:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup - executa uma vez ao iniciar
with ProcessPoolExecutor() as pool:
app.state.pool_executor = pool
print("✓ ProcessPoolExecutor inicializado")
yield # ← Servidor fica rodando aqui
# Shutdown - executa ao parar a aplicação
print("✓ ProcessPoolExecutor finalizado")Um ProcessPoolExecutor cria um pool de processos paralelos que dividem tarefas CPU-intensivas.
- Simulações de Monte Carlo são CPU-bound (usam muito processador)
- FastAPI roda em uma thread, mas
ProcessPoolExecutorusa múltiplos processos - Cada processo usa um núcleo diferente do CPU → verdadeiro paralelismo
Arquivo: config.py
@asynccontextmanager
async def lifespan(app: FastAPI):
# Cria o pool uma única vez
with ProcessPoolExecutor() as pool:
app.state.pool_executor = pool # ← Armazena no estado da app
print("✓ ProcessPoolExecutor inicializado")
yield
print("✓ ProcessPoolExecutor finalizado")Vantagens:
- ✅ Compartilhado entre todas as requisições
- ✅ Criado uma vez (eficiente)
- ✅ Destruído automaticamente ao parar
Em forecast_routes.py:
@forecast_router.post("/run-forecast")
async def create_simulation(request: Request, new_simulation: CreateSimulation) -> dict:
# ... criar objeto Forecast ...
# Obter o pool do estado da aplicação
loop = asyncio.get_running_loop()
pool_executor = request.app.state.pool_executor
# Executar a simulação em paralelo
result = await loop.run_in_executor(pool_executor, forecast.run_forecast)
return resultO que acontece:
asyncio.get_running_loop()→ obtém o loop de eventosloop.run_in_executor()→ executa função síncrona (run_forecast) em um worker do poolawait→ aguarda o resultado sem bloquear a requisição
1. Cliente envia:
{
"nr_simulations": 1000,
"backlog_min": 10,
"backlog_max": 20,
"throughput": [2, 3, 4, 5]
}
2. FastAPI valida com Pydantic (models.py)
3. Cria objeto Forecast em services/forecast.py
4. Envia para ProcessPoolExecutor:
→ ProcessPoolExecutor.run_forecast()
5. Simulações rodando em paralelo:
Simulação 1: Backlog=15 → 7 semanas
Simulação 2: Backlog=12 → 5 semanas
Simulação 3: Backlog=18 → 9 semanas
...
Simulação 1000: Backlog=14 → 6 semanas
6. Calcula percentis dos resultados:
- P50: 6 semanas (mediana)
- P75: 8 semanas
- P85: 9 semanas
- P95: 11 semanas
7. Retorna resposta formatada
def _run_simulations(self) -> List[int]:
forecast_weeks = []
for _ in range(self.nr_simulations): # 1000 vezes
backlog_done = 0
random_weeks = 0
# Sorteia backlog aleatório
backlog = np.random.randint(self.backlog_min, self.backlog_max + 1)
# Semana por semana, até cobrir o backlog
while backlog_done < backlog:
# Sorteia um throughput da lista histórica
random_throughput = np.random.choice(self.throughput)
backlog_done += random_throughput
random_weeks += 1
# Salva quantas semanas levou
forecast_weeks.append(random_weeks)
return forecast_weeksEntrada:
nr_simulations: 3backlog_min: 10, backlog_max: 10(sempre 10)throughput: [2, 3, 4, 5]
Simulação 1:
Backlog: 10
Semana 1: throughput=3 → backlog_done=3
Semana 2: throughput=4 → backlog_done=7
Semana 3: throughput=5 → backlog_done=12 (✓ cobriu)
→ Resultado: 3 semanas
Simulação 2:
Backlog: 10
Semana 1: throughput=2 → backlog_done=2
Semana 2: throughput=2 → backlog_done=4
Semana 3: throughput=5 → backlog_done=9
Semana 4: throughput=3 → backlog_done=12 (✓ cobriu)
→ Resultado: 4 semanas
Simulação 3:
Backlog: 10
Semana 1: throughput=5 → backlog_done=5
Semana 2: throughput=5 → backlog_done=10 (✓ cobriu)
→ Resultado: 2 semanas
Cálculo de Percentis:
Resultados: [3, 4, 2]
P50 (mediana): 3 semanas
POST /forecast/run-forecast
nr_simulations→ deve ser > 0backlog_min→ deve ser > 0backlog_max→ deve ser ≥backlog_minthroughput→ deve ter no mínimo 4 valores
curl -X POST "http://localhost:8000/forecast/run-forecast" \
-H "Content-Type: application/json" \
-d '{
"nr_simulations": 1000,
"backlog_min": 10,
"backlog_max": 20,
"throughput": [2, 3, 4, 5]
}'{
"Backlog-min": 10,
"Backlog-max": 20,
"Throughput": [2, 3, 4, 5],
"Simulations": 1000,
"Percentil-50": 5,
"Percentil-75": 7,
"Percentil-85": 8,
"Percentil-95": 10
}GET / → Mensagem de boas-vindas
{
"message": "API de Forecast - Use POST /forecast/run-forecast"
}GET /forecast/ → Health check
{
"status": "API de Forecast rodando"
}FastAPI gera documentação automática! Acesse:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Lá você pode testar os endpoints diretamente no navegador.
pytest services/unit_tests.py -vSaída esperada:
test_forecast_init PASSED
test_run_simulation_returns_list_of_ints PASSED
test_calculate_percentiles_correct_values PASSED
test_calculate_percentiles_empty_input_raises PASSED
test_format_forecast_response_structure PASSED
docker exec -it <container_id> pytest services/unit_tests.py -vOu use docker-compose:
docker-compose exec dev-backend pytest services/unit_tests.py -v- ✅ Inicialização correta da classe
- ✅ Simulações retornam lista de inteiros
- ✅ Cálculo de percentis está correto
- ✅ Validação de entrada (valores vazios)
- ✅ Formatação da resposta
api_forecast_v-poolExecutor/
│
├── main.py # Inicializa a aplicação FastAPI
├── config.py # Gerencia ProcessPoolExecutor (NEW)
├── forecast_routes.py # Define as rotas da API
│
├── models/
│ └── models.py # Validação com Pydantic
│
├── services/
│ ├── forecast.py # Lógica de Monte Carlo
│ └── unit_tests.py # Testes unitários
│
├── requirements.txt # Dependências Python
├── Dockerfile # Imagem Docker
├── docker-compose.yaml # Orquestração Docker
├── README.md # Este arquivo
| Arquivo | Responsabilidade |
|---|---|
main.py |
Criar app FastAPI e incluir rotas |
config.py |
Gerenciar ciclo de vida do ProcessPoolExecutor |
forecast_routes.py |
Definir endpoint /forecast/run-forecast |
models.py |
Validar dados de entrada com Pydantic |
forecast.py |
Implementar lógica de Monte Carlo |
unit_tests.py |
Testar cada função isoladamente |
Python 3.13+
fastapi==0.x.x
uvicorn==0.x.x
pydantic==2.x.x
numpy==1.x.x
pytest==7.x.x
Veja requirements.txt para versões exatas.
# Linux/Mac
lsof -i :8000
kill -9 <PID>
# Windows
netstat -ano | findstr :8000
taskkill /PID <PID> /FCertifique-se de instalar as dependências:
pip install -r requirements.txtVerifique se o lifespan em config.py está sendo usado em main.py:
app = FastAPI(lifespan=lifespan)- FastAPI Documentation
- Uvicorn Documentation
- ProcessPoolExecutor
- Monte Carlo Simulation
- Pydantic Validation