Metadata-Version: 2.4
Name: waiross
Version: 0.1.0
Summary: Waiross — framework HTTP/WebSocket/gRPC/CLI construído sobre FastAPI
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: alembic>=1.13.2
Requires-Dist: asyncpg>=0.29.0
Requires-Dist: bcrypt>=4.1.0
Requires-Dist: celery>=5.4.0
Requires-Dist: email-validator>=2.2.0
Requires-Dist: fastapi[standard]>=0.115.0
Requires-Dist: pydantic-settings>=2.5.0
Requires-Dist: pydantic>=2.9.0
Requires-Dist: pyjwt>=2.9.0
Requires-Dist: python-multipart>=0.0.12
Requires-Dist: redis>=5.0.8
Requires-Dist: sqlalchemy[asyncio]>=2.0.35
Requires-Dist: typer>=0.12.5
Requires-Dist: uvicorn[standard]>=0.32.0
Provides-Extra: grpc
Requires-Dist: grpcio-tools>=1.66.0; extra == 'grpc'
Requires-Dist: grpcio>=1.66.0; extra == 'grpc'
Description-Content-Type: text/markdown

# Waiross

Um framework construído sobre FastAPI, no espírito do Laravel: convenção
clara, injeção de dependências automática e independência de protocolo de
transporte. Nasceu como boilerplate de um projeto e foi desenhado para
crescer — a ideia é que qualquer projeto novo comece copiando isto.

## Requisitos

- Python 3.10+
- [uv](https://docs.astral.sh/uv/) — gerenciador de pacotes (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
- Docker + Docker Compose (opcional, mas recomendado para subir Postgres/Redis localmente)

## Instalação

```bash
git clone <url-do-seu-fork-ou-template> meu-projeto
cd meu-projeto

cp .env.example .env             # ajuste DATABASE_URL e JWT_SECRET_KEY antes de ir pra produção
make install                     # uv sync — cria .venv e instala as dependências

docker compose up -d db redis    # sobe Postgres + Redis localmente
make migrate                     # aplica as migrations (alembic upgrade head)
make dev                         # sobe a aplicação em http://localhost:8000
```

Confirme que subiu certo: `curl http://localhost:8000/api/v1/health` deve
responder `{"status":"ok","app_name":"Waiross",...}`. Swagger interativo em
`http://localhost:8000/docs`.

Ou tudo via Docker Compose (API + Postgres + Redis + worker Celery, sem
instalar nada localmente):

```bash
docker compose up --build
```

## Filosofia

- **Organização por camada técnica, não por módulo de negócio.** Tudo que
  é regra de negócio pura mora em `domain/`, toda orquestração em
  `application/`, toda implementação concreta em `infrastructure/`, todo
  adaptador de protocolo em `presentation/`. Para achar qualquer coisa,
  você só precisa saber "que tipo de código é isto" — a árvore é sempre a
  mesma, previsível, para qualquer tamanho de sistema.
- **Duas convenções de nomenclatura, conforme o volume esperado por
  entidade.** Camadas onde uma entidade normalmente tem só 1-2 arquivos
  (`domain/entities/`, `domain/value_objects/`, `domain/enums/`,
  `domain/repositories/`, `infrastructure/repositories/`,
  `infrastructure/database/models/`, `infrastructure/queue/tasks/`) ficam
  flat, com o nome da entidade como prefixo do arquivo (ex.:
  `domain/entities/user_entity.py`). Camadas onde uma entidade acumula
  vários arquivos (`application/dto/`, `application/use_cases/`,
  `application/services/`, `domain/exceptions/`,
  `presentation/http/schemas/`) ganham uma subpasta por entidade (ex.:
  `application/use_cases/user/create_user_use_case.py`), pra não virar uma
  pasta única lotada de dezenas de arquivos.
- **HTTP é só mais um adaptador.** `domain/` e `application/` não sabem o
  que é FastAPI, JSON, WebSocket ou gRPC. Um caso de uso é uma classe
  Python comum com um método `execute()`. Isso é o que permite plugar
  HTTP, WebSocket, gRPC, uma fila (Celery) ou uma CLI sem duplicar regra
  de negócio — veja os cinco adaptadores de usuários mais abaixo, todos
  chamando os mesmos casos de uso.
- **Convenção sobre configuração — inclusive na injeção de dependências.**
  Um caso de uso novo é sempre um arquivo em
  `application/use_cases/<entidade>/`. Um repositório novo (interface em
  `domain/repositories/<entidade>/`, implementação em
  `infrastructure/repositories/<entidade>/`) é descoberto e ligado
  automaticamente no container — ninguém precisa abrir `di/bootstrap.py`
  pra registrar isso na mão. Veja "Injeção de dependências" abaixo.
- **1 arquivo → 1 classe.** Sem exceções. Facilita navegação, `git blame`
  e review — cada arquivo tem uma única razão para mudar.

## Árvore de diretórios

```
app/
├── domain/                     # regra de negócio pura — zero FastAPI/SQLAlchemy/HTTP
│   ├── entities/                    user_entity.py             (classe User)
│   ├── value_objects/                user_email.py, user_password.py (Email, PlainPassword)
│   ├── enums/                        user_status.py             (UserStatus)
│   ├── repositories/                 user_repository.py         (interface UserRepository)
│   ├── exceptions/                   AppException + subclasses genéricas (raiz)
│   │   └── user/                     not_found_error.py, already_exists_error.py, ... (específicas de User)
│   ├── services/                     regras de negócio que cruzam mais de uma entidade
│   └── events/                       eventos de domínio (futuro: event bus)
│
├── application/                # casos de uso — orquestram domain + portas
│   ├── use_cases/user/              create_user_use_case.py, login_use_case.py, ... (1 classe cada)
│   ├── dto/user/                    create_user_input.py, user_output.py, ...
│   └── services/user/               serviços de aplicação que orquestram mais de um caso de uso
│
├── infrastructure/             # implementações concretas das portas do domain
│   ├── database/                    engine/sessão SQLAlchemy, Base declarativa
│   │   └── models/                  user_model.py              (UserModel)
│   ├── repositories/                sqlalchemy_user_repository.py (implementa UserRepository)
│   ├── cache/                       porta Cache + RedisCache
│   ├── queue/                       Celery (config do broker)
│   │   └── tasks/                   create_user_task.py
│   ├── storage/                     porta FileStorage + LocalFileStorage
│   ├── mail/                        porta EmailSender + ConsoleEmailSender
│   ├── http/                        cliente HTTP de saída (outbound) para APIs externas
│   └── integrations/                SDKs de terceiros (pagamentos, e-mail transacional etc.)
│
├── presentation/                # adaptadores de entrada — um por protocolo
│   ├── http/
│   │   ├── routes/
│   │   │   ├── http_router.py            ← ponto de entrada, chamado em main.py
│   │   │   └── v1/                       auth_router.py, health_router.py, users_router.py
│   │   └── schemas/                      Pydantic (request/response), também por entidade
│   ├── websocket/
│   │   ├── ws_router.py                  ← ponto de entrada, chamado em main.py
│   │   └── routes/                       user_websocket.py
│   ├── grpc/                             users.proto + servicer (requer codegen)
│   ├── cli/                              comandos `waiross ...` (Typer)
│   └── middleware/                       exception handlers HTTP estilo Laravel
│
├── security/                    # PasswordHasher, JWTProvider (portas + implementações)
├── config/                      # Settings (pydantic-settings) + logging — única fonte de env vars
├── di/                           # injeção de dependências (o "motor")
│   ├── container.py                  Container genérico: bind()/resolve()
│   ├── autodiscovery.py              descobre e liga interface -> implementação sozinho
│   └── bootstrap.py                  registra infraestrutura de base + chama o autodiscovery
├── shared/                       # utilidades técnicas sem estado (ex.: datetime_utils.py)
└── main.py                       # composition root: monta o container, registra
                                   # exception handlers e inclui http_router + ws_router
```

A regra de dependência é sempre "de fora para dentro":
`presentation → application → domain`, e `infrastructure` implementa
portas definidas em `domain`. Nada dentro de `domain/` importa de
`infrastructure/` ou `presentation/`.

| Camada | Sabe sobre | Não sabe sobre |
|---|---|---|
| `domain/` | Regras de negócio, entidades, value objects | FastAPI, SQLAlchemy, HTTP, JSON, protobuf |
| `application/` | `domain/`, portas (interfaces) | Qual protocolo chamou o caso de uso, qual banco é usado |
| `infrastructure/` | `domain/`, SQLAlchemy, bibliotecas externas | FastAPI, schemas HTTP |
| `presentation/` | `application/`, o protocolo específico (HTTP/WS/gRPC/fila/CLI) | Como o caso de uso é implementado por dentro |

## Injeção de dependências (automática)

Em vez de escrever uma função `get_x_use_case(...)` para cada caso de uso,
o container resolve automaticamente qualquer classe cujo `__init__` peça
tipos que ele conhece:

```python
# em qualquer rota:
from app.di.container import container
from app.application.use_cases.user.create_user_use_case import CreateUserUseCase

@router.post("/users")
async def create_user(
    payload: CreateUserSchema,
    use_case: Annotated[CreateUserUseCase, Depends(container.resolve(CreateUserUseCase))],
):
    ...
```

`container.resolve(CreateUserUseCase)` lê os type hints do `__init__` de
`CreateUserUseCase`, vê que ele precisa de um `UserRepository` e de um
`PasswordHasher`, e resolve os dois recursivamente. Você nunca escreve
essa fiação manualmente.

**O binding interface -> implementação também é automático.** `di/bootstrap.py`
registra só duas coisas de infraestrutura genérica (`AsyncSession` via
`get_db_session`, `Settings` via `get_settings`) e chama
`di/autodiscovery.py`, que varre `domain/repositories/` + `security/`
(interfaces) e `infrastructure/repositories/` + `security/`
(implementações), casa cada uma pela herança e registra o binding
sozinho. Para adicionar uma entidade nova:

1. Crie a interface em `domain/repositories/<entidade>_repository.py`
   (uma `ABC` com `@abstractmethod`).
2. Crie a implementação em
   `infrastructure/repositories/sqlalchemy_<entidade>_repository.py`
   (herdando da interface, recebendo `session: AsyncSession` no
   `__init__`).
3. Pronto — na próxima vez que a aplicação subir, o binding já existe.
   **Nenhum arquivo de `di/` precisa ser tocado.**

Isso só não funciona se o construtor da implementação pedir algo que o
container não sabe resolver por tipo (nesse caso, registre-a manualmente
em `di/bootstrap.py` com `container.bind(...)` — é a saída de escape,
não o caminho padrão).

> **Ordem de import importa.** `container.resolve(...)` roda no momento em
> que o módulo de rota é *importado*, não a cada requisição. Por isso
> `app/main.py` chama `register_providers()` **antes** de importar
> `http_router`/`ws_router` — se você criar um novo arquivo de rota,
> garanta que ele só seja alcançado por um desses dois pontos de entrada
> depois do bootstrap (o padrão já existente em `main.py` cobre isso).

## Tratamento de exceções (estilo Laravel)

Nenhuma exceção de negócio vira `HTTPException` dentro de `application/`
ou `domain/`. Em vez disso:

1. A exceção herda de `AppException` (`domain/exceptions/app_exception.py`)
   ou de uma subclasse genérica (`NotFoundException`, `ValidationException`,
   `AlreadyExistsException`, `UnauthorizedException`). Exceções específicas
   de uma entidade ficam em `domain/exceptions/<entidade>/`, um arquivo por
   classe (veja `domain/exceptions/user/`).
2. Uma classe herdando de `ExceptionHandler`
   (`presentation/middleware/exception_handler.py`) traduz aquela exceção
   para uma resposta HTTP — uma classe por arquivo, em
   `presentation/middleware/handlers/*.py`.
3. Ela é adicionada à lista `HANDLERS` em
   `presentation/middleware/handlers/registry.py`.

O Starlette escolhe automaticamente o handler mais específico percorrendo
o MRO da exceção, então a ordem na lista não importa. Adaptadores
não-HTTP (WebSocket, gRPC, fila, CLI) capturam `AppException` diretamente
e traduzem para o formato deles.

## Protocolos suportados

O domínio de usuários implementa cinco adaptadores de entrada sobre os
mesmos casos de uso, provando a independência de protocolo:

### HTTP (REST)

```
POST   /api/v1/users              cria um usuário
GET    /api/v1/users               lista usuários (paginado)
GET    /api/v1/users/me            usuário autenticado (Bearer token)
GET    /api/v1/users/{id}          busca por ID
PATCH  /api/v1/users/{id}          atualiza
DELETE /api/v1/users/{id}          remove
POST   /api/v1/auth/login          login (retorna access + refresh token)
GET    /api/v1/health              health check
```

Swagger: `http://localhost:8000/docs` · ReDoc: `http://localhost:8000/redoc`

Toda rota HTTP entra pelo mesmo ponto:
`presentation/http/routes/http_router.py` agrega as versões
(`routes/v1/`, e no futuro `routes/v2/`) — `main.py` só conhece esse
arquivo, nunca uma rota individual.

### WebSocket

`ws://localhost:8000/ws/users` — mensagens JSON com um campo `action`
(`create`, `list`, `get`). Veja o docstring de
`app/presentation/websocket/routes/user_websocket.py` para o protocolo
completo e um exemplo de cliente Python. Assim como o HTTP,
`presentation/websocket/ws_router.py` é o único ponto de entrada que
`main.py` conhece.

### gRPC

Contrato em `app/presentation/grpc/users.proto`. Roda como processo
separado (não é montado dentro do FastAPI):

```bash
uv sync --extra grpc     # instala grpcio + grpcio-tools (opcional)
make proto                # gera users_pb2.py / users_pb2_grpc.py
make grpc-server           # sobe o servidor na porta GRPC_PORT (padrão 50051)
```

### Filas (Celery)

`app/infrastructure/queue/tasks/create_user_task.py` expõe
`create_user_task`, reaproveitando `CreateUserUseCase` em background:

```bash
make worker   # sobe o worker Celery
```

```python
from app.infrastructure.queue.tasks.create_user_task import create_user_task
create_user_task.delay("jane@example.com", "Jane Doe", "senha-forte")
```

### CLI

```bash
uv run waiross --help
uv run waiross user:create
uv run waiross user:list
uv run waiross user:show <id>
```

Veja `app/presentation/cli/main.py`.

## Como criar uma entidade nova (ex.: `product`)

1. `domain/entities/product_entity.py` (classe `Product`),
   `domain/value_objects/product_*.py` se precisar,
   `domain/repositories/product_repository.py` (interface
   `ProductRepository`), `domain/exceptions/product/` para as exceções
   específicas.
2. `application/use_cases/product/create_product_use_case.py` (e demais
   casos de uso), `application/dto/product/create_product_input.py` (e
   demais DTOs).
3. `infrastructure/database/models/product_model.py` (adicione o import
   em `infrastructure/database/models/__init__.py`),
   `infrastructure/repositories/sqlalchemy_product_repository.py`
   implementando `ProductRepository` — **não precisa registrar nada em
   `di/`**, o autodiscovery liga sozinho na próxima subida.
4. `presentation/http/schemas/product/` (Pydantic) e
   `presentation/http/routes/v1/products_router.py`, usando
   `Depends(container.resolve(SeuCasoDeUso))`. Inclua o router novo em
   `presentation/http/routes/http_router.py`.
5. Se alguma exceção nova precisar de um status HTTP específico, crie o
   handler em `presentation/middleware/handlers/` e registre em
   `registry.py` (senão, o fallback `AppExceptionHandler` já cobre com
   HTTP 400).
6. `make migration m="create products table"` seguido de `make migrate`.
7. (Opcional) Repita o padrão de `presentation/websocket/routes/`,
   `presentation/grpc/` e `infrastructure/queue/tasks/` se esta entidade
   também precisar desses protocolos.

## Stack

- FastAPI + Uvicorn (HTTP e WebSocket no mesmo processo)
- gRPC (`grpcio`, opcional — extra `grpc`) rodando como processo separado
- Typer (CLI — `waiross`)
- Celery + Redis (fila)
- SQLAlchemy 2.0 (async, driver `asyncpg`) + Alembic para migrations
- PostgreSQL
- JWT (access + refresh token) via PyJWT, senhas com `bcrypt` (lib pura, sem passlib)
- `uv` como gerenciador de pacotes

## Testes

```bash
make test
```

Os testes de API (`tests/test_users_http.py`, `tests/test_health.py`)
rodam contra SQLite em memória (fixture em `tests/conftest.py`), sem
precisar de Postgres/Docker.

## Migrations (Alembic)

Os modelos ORM vivem em `app/infrastructure/database/models/<entidade>_model.py`;
`migrations/env.py` importa `infrastructure/database/models/__init__.py`,
que por sua vez importa cada `XModel`, para o autogenerate enxergar todas
as tabelas. Ao criar uma entidade nova com tabela própria, adicione o
import do model novo em `infrastructure/database/models/__init__.py`.

```bash
make migration m="descrição da mudança"   # gera migration por autogenerate
make migrate                              # aplica
make downgrade                            # desfaz a última
```

## Comandos (Makefile)

| Comando | Descrição |
|---|---|
| `make install` | instala dependências (`uv sync`) |
| `make dev` | sobe HTTP + WebSocket com reload |
| `make run` | sobe a API em modo produção |
| `make migration m="..."` | gera nova migration |
| `make migrate` | aplica migrations pendentes |
| `make test` | roda a suíte de testes |
| `make lint` | roda o ruff |
| `make worker` | sobe um worker Celery |
| `make cli args="..."` | roda um comando da CLI (`waiross`) |
| `make proto` | gera os stubs gRPC a partir do `.proto` |
| `make grpc-server` | sobe o servidor gRPC (processo separado) |

## Nota sobre este repositório

Este projeto passou por reestruturações "hard" sucessivas até chegar na
árvore atual (camadas técnicas no topo, entidade como subpasta dentro de
cada camada, DI auto-discovery). O sandbox que gerou esta versão não
conseguiu apagar os arquivos antigos do disco (restrição do sistema de
arquivos montado) — eles foram movidos para `_legacy_delete_me/` na raiz
do projeto (já no `.gitignore`) e não são importados por nada. Pode
apagar essa pasta manualmente quando quiser.

Publicação como template/framework e próximos passos (site de docs): veja
`PUBLISHING.md`.
