waiross 0.1.0__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.
Files changed (144) hide show
  1. waiross-0.1.0/.env.example +35 -0
  2. waiross-0.1.0/.github/workflows/release.yml +49 -0
  3. waiross-0.1.0/.gitignore +20 -0
  4. waiross-0.1.0/Dockerfile +17 -0
  5. waiross-0.1.0/LICENSE +21 -0
  6. waiross-0.1.0/Makefile +46 -0
  7. waiross-0.1.0/PKG-INFO +403 -0
  8. waiross-0.1.0/PUBLISHING.md +84 -0
  9. waiross-0.1.0/README.md +377 -0
  10. waiross-0.1.0/alembic.ini +44 -0
  11. waiross-0.1.0/app/__init__.py +0 -0
  12. waiross-0.1.0/app/application/__init__.py +0 -0
  13. waiross-0.1.0/app/application/dto/__init__.py +0 -0
  14. waiross-0.1.0/app/application/dto/user/__init__.py +0 -0
  15. waiross-0.1.0/app/application/dto/user/create_user_input.py +10 -0
  16. waiross-0.1.0/app/application/dto/user/delete_user_input.py +9 -0
  17. waiross-0.1.0/app/application/dto/user/get_user_input.py +9 -0
  18. waiross-0.1.0/app/application/dto/user/list_users_input.py +9 -0
  19. waiross-0.1.0/app/application/dto/user/login_input.py +9 -0
  20. waiross-0.1.0/app/application/dto/user/token_output.py +15 -0
  21. waiross-0.1.0/app/application/dto/user/update_user_input.py +11 -0
  22. waiross-0.1.0/app/application/dto/user/user_output.py +47 -0
  23. waiross-0.1.0/app/application/services/__init__.py +0 -0
  24. waiross-0.1.0/app/application/services/user/__init__.py +0 -0
  25. waiross-0.1.0/app/application/use_cases/__init__.py +0 -0
  26. waiross-0.1.0/app/application/use_cases/user/__init__.py +0 -0
  27. waiross-0.1.0/app/application/use_cases/user/create_user_use_case.py +41 -0
  28. waiross-0.1.0/app/application/use_cases/user/delete_user_use_case.py +19 -0
  29. waiross-0.1.0/app/application/use_cases/user/get_user_use_case.py +19 -0
  30. waiross-0.1.0/app/application/use_cases/user/list_users_use_case.py +16 -0
  31. waiross-0.1.0/app/application/use_cases/user/login_use_case.py +44 -0
  32. waiross-0.1.0/app/application/use_cases/user/update_user_use_case.py +29 -0
  33. waiross-0.1.0/app/config/__init__.py +0 -0
  34. waiross-0.1.0/app/config/logging.py +23 -0
  35. waiross-0.1.0/app/config/settings.py +70 -0
  36. waiross-0.1.0/app/di/__init__.py +0 -0
  37. waiross-0.1.0/app/di/autodiscovery.py +94 -0
  38. waiross-0.1.0/app/di/bootstrap.py +43 -0
  39. waiross-0.1.0/app/di/container.py +114 -0
  40. waiross-0.1.0/app/domain/__init__.py +0 -0
  41. waiross-0.1.0/app/domain/entities/__init__.py +0 -0
  42. waiross-0.1.0/app/domain/entities/user_entity.py +65 -0
  43. waiross-0.1.0/app/domain/enums/__init__.py +0 -0
  44. waiross-0.1.0/app/domain/enums/user_status.py +9 -0
  45. waiross-0.1.0/app/domain/events/__init__.py +0 -0
  46. waiross-0.1.0/app/domain/exceptions/__init__.py +35 -0
  47. waiross-0.1.0/app/domain/exceptions/already_exists_exception.py +5 -0
  48. waiross-0.1.0/app/domain/exceptions/app_exception.py +12 -0
  49. waiross-0.1.0/app/domain/exceptions/not_found_exception.py +5 -0
  50. waiross-0.1.0/app/domain/exceptions/unauthorized_exception.py +5 -0
  51. waiross-0.1.0/app/domain/exceptions/user/__init__.py +0 -0
  52. waiross-0.1.0/app/domain/exceptions/user/already_exists_error.py +5 -0
  53. waiross-0.1.0/app/domain/exceptions/user/inactive_user_error.py +5 -0
  54. waiross-0.1.0/app/domain/exceptions/user/invalid_credentials_error.py +5 -0
  55. waiross-0.1.0/app/domain/exceptions/user/invalid_email_error.py +5 -0
  56. waiross-0.1.0/app/domain/exceptions/user/not_found_error.py +5 -0
  57. waiross-0.1.0/app/domain/exceptions/user/weak_password_error.py +5 -0
  58. waiross-0.1.0/app/domain/exceptions/validation_exception.py +5 -0
  59. waiross-0.1.0/app/domain/repositories/__init__.py +0 -0
  60. waiross-0.1.0/app/domain/repositories/user_repository.py +41 -0
  61. waiross-0.1.0/app/domain/services/__init__.py +0 -0
  62. waiross-0.1.0/app/domain/value_objects/__init__.py +0 -0
  63. waiross-0.1.0/app/domain/value_objects/user_email.py +27 -0
  64. waiross-0.1.0/app/domain/value_objects/user_password.py +22 -0
  65. waiross-0.1.0/app/infrastructure/__init__.py +0 -0
  66. waiross-0.1.0/app/infrastructure/cache/__init__.py +0 -0
  67. waiross-0.1.0/app/infrastructure/cache/cache.py +24 -0
  68. waiross-0.1.0/app/infrastructure/cache/redis_cache.py +19 -0
  69. waiross-0.1.0/app/infrastructure/database/__init__.py +0 -0
  70. waiross-0.1.0/app/infrastructure/database/base.py +13 -0
  71. waiross-0.1.0/app/infrastructure/database/models/__init__.py +11 -0
  72. waiross-0.1.0/app/infrastructure/database/models/user_model.py +31 -0
  73. waiross-0.1.0/app/infrastructure/database/session.py +37 -0
  74. waiross-0.1.0/app/infrastructure/http/__init__.py +0 -0
  75. waiross-0.1.0/app/infrastructure/http/http_client.py +33 -0
  76. waiross-0.1.0/app/infrastructure/integrations/__init__.py +0 -0
  77. waiross-0.1.0/app/infrastructure/mail/__init__.py +0 -0
  78. waiross-0.1.0/app/infrastructure/mail/console_email_sender.py +18 -0
  79. waiross-0.1.0/app/infrastructure/mail/email_sender.py +11 -0
  80. waiross-0.1.0/app/infrastructure/queue/__init__.py +0 -0
  81. waiross-0.1.0/app/infrastructure/queue/celery_app.py +36 -0
  82. waiross-0.1.0/app/infrastructure/queue/tasks/__init__.py +0 -0
  83. waiross-0.1.0/app/infrastructure/queue/tasks/create_user_task.py +52 -0
  84. waiross-0.1.0/app/infrastructure/repositories/__init__.py +0 -0
  85. waiross-0.1.0/app/infrastructure/repositories/sqlalchemy_user_repository.py +90 -0
  86. waiross-0.1.0/app/main.py +76 -0
  87. waiross-0.1.0/app/presentation/__init__.py +0 -0
  88. waiross-0.1.0/app/presentation/cli/__init__.py +0 -0
  89. waiross-0.1.0/app/presentation/cli/main.py +117 -0
  90. waiross-0.1.0/app/presentation/grpc/__init__.py +0 -0
  91. waiross-0.1.0/app/presentation/grpc/server.py +40 -0
  92. waiross-0.1.0/app/presentation/grpc/users.proto +52 -0
  93. waiross-0.1.0/app/presentation/grpc/users_servicer.py +112 -0
  94. waiross-0.1.0/app/presentation/http/__init__.py +0 -0
  95. waiross-0.1.0/app/presentation/http/dependencies.py +49 -0
  96. waiross-0.1.0/app/presentation/http/routes/__init__.py +0 -0
  97. waiross-0.1.0/app/presentation/http/routes/http_router.py +22 -0
  98. waiross-0.1.0/app/presentation/http/routes/v1/__init__.py +0 -0
  99. waiross-0.1.0/app/presentation/http/routes/v1/auth_router.py +28 -0
  100. waiross-0.1.0/app/presentation/http/routes/v1/health_router.py +32 -0
  101. waiross-0.1.0/app/presentation/http/routes/v1/users_router.py +127 -0
  102. waiross-0.1.0/app/presentation/http/schemas/__init__.py +0 -0
  103. waiross-0.1.0/app/presentation/http/schemas/auth/__init__.py +0 -0
  104. waiross-0.1.0/app/presentation/http/schemas/auth/login_schema.py +8 -0
  105. waiross-0.1.0/app/presentation/http/schemas/auth/token_response_schema.py +19 -0
  106. waiross-0.1.0/app/presentation/http/schemas/health/__init__.py +0 -0
  107. waiross-0.1.0/app/presentation/http/schemas/health/health_response_schema.py +9 -0
  108. waiross-0.1.0/app/presentation/http/schemas/user/__init__.py +0 -0
  109. waiross-0.1.0/app/presentation/http/schemas/user/create_user_schema.py +9 -0
  110. waiross-0.1.0/app/presentation/http/schemas/user/update_user_schema.py +10 -0
  111. waiross-0.1.0/app/presentation/http/schemas/user/user_list_response_schema.py +14 -0
  112. waiross-0.1.0/app/presentation/http/schemas/user/user_response_schema.py +33 -0
  113. waiross-0.1.0/app/presentation/middleware/__init__.py +0 -0
  114. waiross-0.1.0/app/presentation/middleware/exception_handler.py +33 -0
  115. waiross-0.1.0/app/presentation/middleware/handlers/__init__.py +0 -0
  116. waiross-0.1.0/app/presentation/middleware/handlers/already_exists_exception_handler.py +14 -0
  117. waiross-0.1.0/app/presentation/middleware/handlers/app_exception_handler.py +21 -0
  118. waiross-0.1.0/app/presentation/middleware/handlers/not_found_exception_handler.py +14 -0
  119. waiross-0.1.0/app/presentation/middleware/handlers/registry.py +54 -0
  120. waiross-0.1.0/app/presentation/middleware/handlers/unauthorized_exception_handler.py +16 -0
  121. waiross-0.1.0/app/presentation/middleware/handlers/unhandled_exception_handler.py +25 -0
  122. waiross-0.1.0/app/presentation/middleware/handlers/validation_exception_handler.py +16 -0
  123. waiross-0.1.0/app/presentation/websocket/__init__.py +0 -0
  124. waiross-0.1.0/app/presentation/websocket/routes/__init__.py +0 -0
  125. waiross-0.1.0/app/presentation/websocket/routes/user_websocket.py +132 -0
  126. waiross-0.1.0/app/presentation/websocket/ws_router.py +14 -0
  127. waiross-0.1.0/app/security/__init__.py +0 -0
  128. waiross-0.1.0/app/security/bcrypt_password_hasher.py +33 -0
  129. waiross-0.1.0/app/security/jwt_provider.py +29 -0
  130. waiross-0.1.0/app/security/password_hasher.py +21 -0
  131. waiross-0.1.0/app/security/pyjwt_provider.py +54 -0
  132. waiross-0.1.0/app/security/token_payload.py +10 -0
  133. waiross-0.1.0/app/shared/__init__.py +0 -0
  134. waiross-0.1.0/app/shared/datetime_utils.py +24 -0
  135. waiross-0.1.0/docker-compose.yml +54 -0
  136. waiross-0.1.0/migrations/env.py +66 -0
  137. waiross-0.1.0/migrations/script.py.mako +26 -0
  138. waiross-0.1.0/migrations/versions/98b88b47ae11_create_users_table.py +38 -0
  139. waiross-0.1.0/pyproject.toml +68 -0
  140. waiross-0.1.0/tests/__init__.py +0 -0
  141. waiross-0.1.0/tests/conftest.py +42 -0
  142. waiross-0.1.0/tests/test_health.py +10 -0
  143. waiross-0.1.0/tests/test_users_http.py +65 -0
  144. waiross-0.1.0/uv.lock +2245 -0
@@ -0,0 +1,35 @@
1
+ # ------------------------------------------------------------------
2
+ # Configuração da aplicação. Copie este arquivo para ".env" e ajuste.
3
+ # Todas as variáveis são lidas por app/config/settings.py (pydantic-settings).
4
+ # ------------------------------------------------------------------
5
+
6
+ # Geral
7
+ APP_NAME="Waiross"
8
+ APP_ENV=development # development | staging | production
9
+ DEBUG=true
10
+ API_V1_PREFIX=/api/v1
11
+
12
+ # Banco de dados (PostgreSQL, driver assíncrono asyncpg)
13
+ DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/waiross
14
+
15
+ # Segurança / JWT
16
+ JWT_SECRET_KEY=change-me-to-a-long-random-string
17
+ JWT_ALGORITHM=HS256
18
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
19
+ REFRESH_TOKEN_EXPIRE_DAYS=7
20
+
21
+ # Redis (cache e broker do Celery)
22
+ REDIS_URL=redis://localhost:6379/0
23
+
24
+ # Celery (fila — quarto adaptador de protocolo, veja app/infrastructure/queue/)
25
+ CELERY_BROKER_URL=redis://localhost:6379/1
26
+ CELERY_RESULT_BACKEND=redis://localhost:6379/2
27
+
28
+ # Armazenamento local (app/infrastructure/storage)
29
+ LOCAL_STORAGE_PATH=./storage
30
+
31
+ # CORS
32
+ CORS_ALLOW_ORIGINS=http://localhost:3000
33
+
34
+ # gRPC (terceiro adaptador de protocolo — processo separado, `make grpc-server`)
35
+ GRPC_PORT=50051
@@ -0,0 +1,49 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build package
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v3
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Build
21
+ run: uv build
22
+
23
+ - name: Check package
24
+ run: uvx twine check dist/*
25
+
26
+ - name: Upload artifacts
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: distributions
30
+ path: dist/
31
+
32
+ publish:
33
+ name: Publish to PyPI
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/p/waiross
39
+ permissions:
40
+ id-token: write
41
+ steps:
42
+ - name: Download artifacts
43
+ uses: actions/download-artifact@v4
44
+ with:
45
+ name: distributions
46
+ path: dist/
47
+
48
+ - name: Publish to PyPI
49
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ .env
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ *.sqlite3
11
+ storage/
12
+ .DS_Store
13
+ dist/
14
+ build/
15
+
16
+ # Estrutura antiga (pré-arquitetura modular) — o sandbox que gerou este
17
+ # projeto não conseguiu apagar esses arquivos do disco por uma restrição
18
+ # do sistema de arquivos montado; nada no projeto os importa. Apague esta
19
+ # pasta manualmente quando puder.
20
+ _legacy_delete_me/
@@ -0,0 +1,17 @@
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /code
7
+
8
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
9
+ COPY pyproject.toml ./
10
+ RUN uv sync --no-dev --no-install-project
11
+
12
+ COPY . .
13
+ RUN uv sync --no-dev
14
+
15
+ EXPOSE 8000
16
+
17
+ CMD ["uv", "run", "fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"]
waiross-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tech-iross
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
waiross-0.1.0/Makefile ADDED
@@ -0,0 +1,46 @@
1
+ .PHONY: install run dev migrate migration downgrade test lint worker proto grpc-server cli
2
+
3
+ install:
4
+ uv sync
5
+
6
+ run:
7
+ uv run fastapi run app/main.py --host 0.0.0.0 --port 8000
8
+
9
+ dev:
10
+ uv run fastapi dev app/main.py --host 0.0.0.0 --port 8000
11
+
12
+ migration:
13
+ uv run alembic revision --autogenerate -m "$(m)"
14
+
15
+ migrate:
16
+ uv run alembic upgrade head
17
+
18
+ downgrade:
19
+ uv run alembic downgrade -1
20
+
21
+ test:
22
+ uv run pytest -v
23
+
24
+ lint:
25
+ uv run ruff check app tests
26
+
27
+ worker:
28
+ uv run celery -A app.infrastructure.queue.celery_app worker --loglevel=info
29
+
30
+ # CLI de administração (quinto adaptador de protocolo) — ex.: `make cli args="user:list"`.
31
+ cli:
32
+ uv run waiross $(args)
33
+
34
+ # Gera os stubs Python (users_pb2.py, users_pb2_grpc.py) a partir do
35
+ # users.proto. Requer `uv sync --extra grpc`.
36
+ proto:
37
+ uv run python -m grpc_tools.protoc \
38
+ -I app/presentation/grpc \
39
+ --python_out=app/presentation/grpc \
40
+ --grpc_python_out=app/presentation/grpc \
41
+ app/presentation/grpc/users.proto
42
+
43
+ # Sobe o servidor gRPC (processo separado do FastAPI).
44
+ # Rode `make proto` antes, na primeira vez.
45
+ grpc-server:
46
+ uv run python -m app.presentation.grpc.server
waiross-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,403 @@
1
+ Metadata-Version: 2.4
2
+ Name: waiross
3
+ Version: 0.1.0
4
+ Summary: Waiross — framework HTTP/WebSocket/gRPC/CLI construído sobre FastAPI
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: alembic>=1.13.2
9
+ Requires-Dist: asyncpg>=0.29.0
10
+ Requires-Dist: bcrypt>=4.1.0
11
+ Requires-Dist: celery>=5.4.0
12
+ Requires-Dist: email-validator>=2.2.0
13
+ Requires-Dist: fastapi[standard]>=0.115.0
14
+ Requires-Dist: pydantic-settings>=2.5.0
15
+ Requires-Dist: pydantic>=2.9.0
16
+ Requires-Dist: pyjwt>=2.9.0
17
+ Requires-Dist: python-multipart>=0.0.12
18
+ Requires-Dist: redis>=5.0.8
19
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.35
20
+ Requires-Dist: typer>=0.12.5
21
+ Requires-Dist: uvicorn[standard]>=0.32.0
22
+ Provides-Extra: grpc
23
+ Requires-Dist: grpcio-tools>=1.66.0; extra == 'grpc'
24
+ Requires-Dist: grpcio>=1.66.0; extra == 'grpc'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Waiross
28
+
29
+ Um framework construído sobre FastAPI, no espírito do Laravel: convenção
30
+ clara, injeção de dependências automática e independência de protocolo de
31
+ transporte. Nasceu como boilerplate de um projeto e foi desenhado para
32
+ crescer — a ideia é que qualquer projeto novo comece copiando isto.
33
+
34
+ ## Requisitos
35
+
36
+ - Python 3.10+
37
+ - [uv](https://docs.astral.sh/uv/) — gerenciador de pacotes (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
38
+ - Docker + Docker Compose (opcional, mas recomendado para subir Postgres/Redis localmente)
39
+
40
+ ## Instalação
41
+
42
+ ```bash
43
+ git clone <url-do-seu-fork-ou-template> meu-projeto
44
+ cd meu-projeto
45
+
46
+ cp .env.example .env # ajuste DATABASE_URL e JWT_SECRET_KEY antes de ir pra produção
47
+ make install # uv sync — cria .venv e instala as dependências
48
+
49
+ docker compose up -d db redis # sobe Postgres + Redis localmente
50
+ make migrate # aplica as migrations (alembic upgrade head)
51
+ make dev # sobe a aplicação em http://localhost:8000
52
+ ```
53
+
54
+ Confirme que subiu certo: `curl http://localhost:8000/api/v1/health` deve
55
+ responder `{"status":"ok","app_name":"Waiross",...}`. Swagger interativo em
56
+ `http://localhost:8000/docs`.
57
+
58
+ Ou tudo via Docker Compose (API + Postgres + Redis + worker Celery, sem
59
+ instalar nada localmente):
60
+
61
+ ```bash
62
+ docker compose up --build
63
+ ```
64
+
65
+ ## Filosofia
66
+
67
+ - **Organização por camada técnica, não por módulo de negócio.** Tudo que
68
+ é regra de negócio pura mora em `domain/`, toda orquestração em
69
+ `application/`, toda implementação concreta em `infrastructure/`, todo
70
+ adaptador de protocolo em `presentation/`. Para achar qualquer coisa,
71
+ você só precisa saber "que tipo de código é isto" — a árvore é sempre a
72
+ mesma, previsível, para qualquer tamanho de sistema.
73
+ - **Duas convenções de nomenclatura, conforme o volume esperado por
74
+ entidade.** Camadas onde uma entidade normalmente tem só 1-2 arquivos
75
+ (`domain/entities/`, `domain/value_objects/`, `domain/enums/`,
76
+ `domain/repositories/`, `infrastructure/repositories/`,
77
+ `infrastructure/database/models/`, `infrastructure/queue/tasks/`) ficam
78
+ flat, com o nome da entidade como prefixo do arquivo (ex.:
79
+ `domain/entities/user_entity.py`). Camadas onde uma entidade acumula
80
+ vários arquivos (`application/dto/`, `application/use_cases/`,
81
+ `application/services/`, `domain/exceptions/`,
82
+ `presentation/http/schemas/`) ganham uma subpasta por entidade (ex.:
83
+ `application/use_cases/user/create_user_use_case.py`), pra não virar uma
84
+ pasta única lotada de dezenas de arquivos.
85
+ - **HTTP é só mais um adaptador.** `domain/` e `application/` não sabem o
86
+ que é FastAPI, JSON, WebSocket ou gRPC. Um caso de uso é uma classe
87
+ Python comum com um método `execute()`. Isso é o que permite plugar
88
+ HTTP, WebSocket, gRPC, uma fila (Celery) ou uma CLI sem duplicar regra
89
+ de negócio — veja os cinco adaptadores de usuários mais abaixo, todos
90
+ chamando os mesmos casos de uso.
91
+ - **Convenção sobre configuração — inclusive na injeção de dependências.**
92
+ Um caso de uso novo é sempre um arquivo em
93
+ `application/use_cases/<entidade>/`. Um repositório novo (interface em
94
+ `domain/repositories/<entidade>/`, implementação em
95
+ `infrastructure/repositories/<entidade>/`) é descoberto e ligado
96
+ automaticamente no container — ninguém precisa abrir `di/bootstrap.py`
97
+ pra registrar isso na mão. Veja "Injeção de dependências" abaixo.
98
+ - **1 arquivo → 1 classe.** Sem exceções. Facilita navegação, `git blame`
99
+ e review — cada arquivo tem uma única razão para mudar.
100
+
101
+ ## Árvore de diretórios
102
+
103
+ ```
104
+ app/
105
+ ├── domain/ # regra de negócio pura — zero FastAPI/SQLAlchemy/HTTP
106
+ │ ├── entities/ user_entity.py (classe User)
107
+ │ ├── value_objects/ user_email.py, user_password.py (Email, PlainPassword)
108
+ │ ├── enums/ user_status.py (UserStatus)
109
+ │ ├── repositories/ user_repository.py (interface UserRepository)
110
+ │ ├── exceptions/ AppException + subclasses genéricas (raiz)
111
+ │ │ └── user/ not_found_error.py, already_exists_error.py, ... (específicas de User)
112
+ │ ├── services/ regras de negócio que cruzam mais de uma entidade
113
+ │ └── events/ eventos de domínio (futuro: event bus)
114
+
115
+ ├── application/ # casos de uso — orquestram domain + portas
116
+ │ ├── use_cases/user/ create_user_use_case.py, login_use_case.py, ... (1 classe cada)
117
+ │ ├── dto/user/ create_user_input.py, user_output.py, ...
118
+ │ └── services/user/ serviços de aplicação que orquestram mais de um caso de uso
119
+
120
+ ├── infrastructure/ # implementações concretas das portas do domain
121
+ │ ├── database/ engine/sessão SQLAlchemy, Base declarativa
122
+ │ │ └── models/ user_model.py (UserModel)
123
+ │ ├── repositories/ sqlalchemy_user_repository.py (implementa UserRepository)
124
+ │ ├── cache/ porta Cache + RedisCache
125
+ │ ├── queue/ Celery (config do broker)
126
+ │ │ └── tasks/ create_user_task.py
127
+ │ ├── storage/ porta FileStorage + LocalFileStorage
128
+ │ ├── mail/ porta EmailSender + ConsoleEmailSender
129
+ │ ├── http/ cliente HTTP de saída (outbound) para APIs externas
130
+ │ └── integrations/ SDKs de terceiros (pagamentos, e-mail transacional etc.)
131
+
132
+ ├── presentation/ # adaptadores de entrada — um por protocolo
133
+ │ ├── http/
134
+ │ │ ├── routes/
135
+ │ │ │ ├── http_router.py ← ponto de entrada, chamado em main.py
136
+ │ │ │ └── v1/ auth_router.py, health_router.py, users_router.py
137
+ │ │ └── schemas/ Pydantic (request/response), também por entidade
138
+ │ ├── websocket/
139
+ │ │ ├── ws_router.py ← ponto de entrada, chamado em main.py
140
+ │ │ └── routes/ user_websocket.py
141
+ │ ├── grpc/ users.proto + servicer (requer codegen)
142
+ │ ├── cli/ comandos `waiross ...` (Typer)
143
+ │ └── middleware/ exception handlers HTTP estilo Laravel
144
+
145
+ ├── security/ # PasswordHasher, JWTProvider (portas + implementações)
146
+ ├── config/ # Settings (pydantic-settings) + logging — única fonte de env vars
147
+ ├── di/ # injeção de dependências (o "motor")
148
+ │ ├── container.py Container genérico: bind()/resolve()
149
+ │ ├── autodiscovery.py descobre e liga interface -> implementação sozinho
150
+ │ └── bootstrap.py registra infraestrutura de base + chama o autodiscovery
151
+ ├── shared/ # utilidades técnicas sem estado (ex.: datetime_utils.py)
152
+ └── main.py # composition root: monta o container, registra
153
+ # exception handlers e inclui http_router + ws_router
154
+ ```
155
+
156
+ A regra de dependência é sempre "de fora para dentro":
157
+ `presentation → application → domain`, e `infrastructure` implementa
158
+ portas definidas em `domain`. Nada dentro de `domain/` importa de
159
+ `infrastructure/` ou `presentation/`.
160
+
161
+ | Camada | Sabe sobre | Não sabe sobre |
162
+ |---|---|---|
163
+ | `domain/` | Regras de negócio, entidades, value objects | FastAPI, SQLAlchemy, HTTP, JSON, protobuf |
164
+ | `application/` | `domain/`, portas (interfaces) | Qual protocolo chamou o caso de uso, qual banco é usado |
165
+ | `infrastructure/` | `domain/`, SQLAlchemy, bibliotecas externas | FastAPI, schemas HTTP |
166
+ | `presentation/` | `application/`, o protocolo específico (HTTP/WS/gRPC/fila/CLI) | Como o caso de uso é implementado por dentro |
167
+
168
+ ## Injeção de dependências (automática)
169
+
170
+ Em vez de escrever uma função `get_x_use_case(...)` para cada caso de uso,
171
+ o container resolve automaticamente qualquer classe cujo `__init__` peça
172
+ tipos que ele conhece:
173
+
174
+ ```python
175
+ # em qualquer rota:
176
+ from app.di.container import container
177
+ from app.application.use_cases.user.create_user_use_case import CreateUserUseCase
178
+
179
+ @router.post("/users")
180
+ async def create_user(
181
+ payload: CreateUserSchema,
182
+ use_case: Annotated[CreateUserUseCase, Depends(container.resolve(CreateUserUseCase))],
183
+ ):
184
+ ...
185
+ ```
186
+
187
+ `container.resolve(CreateUserUseCase)` lê os type hints do `__init__` de
188
+ `CreateUserUseCase`, vê que ele precisa de um `UserRepository` e de um
189
+ `PasswordHasher`, e resolve os dois recursivamente. Você nunca escreve
190
+ essa fiação manualmente.
191
+
192
+ **O binding interface -> implementação também é automático.** `di/bootstrap.py`
193
+ registra só duas coisas de infraestrutura genérica (`AsyncSession` via
194
+ `get_db_session`, `Settings` via `get_settings`) e chama
195
+ `di/autodiscovery.py`, que varre `domain/repositories/` + `security/`
196
+ (interfaces) e `infrastructure/repositories/` + `security/`
197
+ (implementações), casa cada uma pela herança e registra o binding
198
+ sozinho. Para adicionar uma entidade nova:
199
+
200
+ 1. Crie a interface em `domain/repositories/<entidade>_repository.py`
201
+ (uma `ABC` com `@abstractmethod`).
202
+ 2. Crie a implementação em
203
+ `infrastructure/repositories/sqlalchemy_<entidade>_repository.py`
204
+ (herdando da interface, recebendo `session: AsyncSession` no
205
+ `__init__`).
206
+ 3. Pronto — na próxima vez que a aplicação subir, o binding já existe.
207
+ **Nenhum arquivo de `di/` precisa ser tocado.**
208
+
209
+ Isso só não funciona se o construtor da implementação pedir algo que o
210
+ container não sabe resolver por tipo (nesse caso, registre-a manualmente
211
+ em `di/bootstrap.py` com `container.bind(...)` — é a saída de escape,
212
+ não o caminho padrão).
213
+
214
+ > **Ordem de import importa.** `container.resolve(...)` roda no momento em
215
+ > que o módulo de rota é *importado*, não a cada requisição. Por isso
216
+ > `app/main.py` chama `register_providers()` **antes** de importar
217
+ > `http_router`/`ws_router` — se você criar um novo arquivo de rota,
218
+ > garanta que ele só seja alcançado por um desses dois pontos de entrada
219
+ > depois do bootstrap (o padrão já existente em `main.py` cobre isso).
220
+
221
+ ## Tratamento de exceções (estilo Laravel)
222
+
223
+ Nenhuma exceção de negócio vira `HTTPException` dentro de `application/`
224
+ ou `domain/`. Em vez disso:
225
+
226
+ 1. A exceção herda de `AppException` (`domain/exceptions/app_exception.py`)
227
+ ou de uma subclasse genérica (`NotFoundException`, `ValidationException`,
228
+ `AlreadyExistsException`, `UnauthorizedException`). Exceções específicas
229
+ de uma entidade ficam em `domain/exceptions/<entidade>/`, um arquivo por
230
+ classe (veja `domain/exceptions/user/`).
231
+ 2. Uma classe herdando de `ExceptionHandler`
232
+ (`presentation/middleware/exception_handler.py`) traduz aquela exceção
233
+ para uma resposta HTTP — uma classe por arquivo, em
234
+ `presentation/middleware/handlers/*.py`.
235
+ 3. Ela é adicionada à lista `HANDLERS` em
236
+ `presentation/middleware/handlers/registry.py`.
237
+
238
+ O Starlette escolhe automaticamente o handler mais específico percorrendo
239
+ o MRO da exceção, então a ordem na lista não importa. Adaptadores
240
+ não-HTTP (WebSocket, gRPC, fila, CLI) capturam `AppException` diretamente
241
+ e traduzem para o formato deles.
242
+
243
+ ## Protocolos suportados
244
+
245
+ O domínio de usuários implementa cinco adaptadores de entrada sobre os
246
+ mesmos casos de uso, provando a independência de protocolo:
247
+
248
+ ### HTTP (REST)
249
+
250
+ ```
251
+ POST /api/v1/users cria um usuário
252
+ GET /api/v1/users lista usuários (paginado)
253
+ GET /api/v1/users/me usuário autenticado (Bearer token)
254
+ GET /api/v1/users/{id} busca por ID
255
+ PATCH /api/v1/users/{id} atualiza
256
+ DELETE /api/v1/users/{id} remove
257
+ POST /api/v1/auth/login login (retorna access + refresh token)
258
+ GET /api/v1/health health check
259
+ ```
260
+
261
+ Swagger: `http://localhost:8000/docs` · ReDoc: `http://localhost:8000/redoc`
262
+
263
+ Toda rota HTTP entra pelo mesmo ponto:
264
+ `presentation/http/routes/http_router.py` agrega as versões
265
+ (`routes/v1/`, e no futuro `routes/v2/`) — `main.py` só conhece esse
266
+ arquivo, nunca uma rota individual.
267
+
268
+ ### WebSocket
269
+
270
+ `ws://localhost:8000/ws/users` — mensagens JSON com um campo `action`
271
+ (`create`, `list`, `get`). Veja o docstring de
272
+ `app/presentation/websocket/routes/user_websocket.py` para o protocolo
273
+ completo e um exemplo de cliente Python. Assim como o HTTP,
274
+ `presentation/websocket/ws_router.py` é o único ponto de entrada que
275
+ `main.py` conhece.
276
+
277
+ ### gRPC
278
+
279
+ Contrato em `app/presentation/grpc/users.proto`. Roda como processo
280
+ separado (não é montado dentro do FastAPI):
281
+
282
+ ```bash
283
+ uv sync --extra grpc # instala grpcio + grpcio-tools (opcional)
284
+ make proto # gera users_pb2.py / users_pb2_grpc.py
285
+ make grpc-server # sobe o servidor na porta GRPC_PORT (padrão 50051)
286
+ ```
287
+
288
+ ### Filas (Celery)
289
+
290
+ `app/infrastructure/queue/tasks/create_user_task.py` expõe
291
+ `create_user_task`, reaproveitando `CreateUserUseCase` em background:
292
+
293
+ ```bash
294
+ make worker # sobe o worker Celery
295
+ ```
296
+
297
+ ```python
298
+ from app.infrastructure.queue.tasks.create_user_task import create_user_task
299
+ create_user_task.delay("jane@example.com", "Jane Doe", "senha-forte")
300
+ ```
301
+
302
+ ### CLI
303
+
304
+ ```bash
305
+ uv run waiross --help
306
+ uv run waiross user:create
307
+ uv run waiross user:list
308
+ uv run waiross user:show <id>
309
+ ```
310
+
311
+ Veja `app/presentation/cli/main.py`.
312
+
313
+ ## Como criar uma entidade nova (ex.: `product`)
314
+
315
+ 1. `domain/entities/product_entity.py` (classe `Product`),
316
+ `domain/value_objects/product_*.py` se precisar,
317
+ `domain/repositories/product_repository.py` (interface
318
+ `ProductRepository`), `domain/exceptions/product/` para as exceções
319
+ específicas.
320
+ 2. `application/use_cases/product/create_product_use_case.py` (e demais
321
+ casos de uso), `application/dto/product/create_product_input.py` (e
322
+ demais DTOs).
323
+ 3. `infrastructure/database/models/product_model.py` (adicione o import
324
+ em `infrastructure/database/models/__init__.py`),
325
+ `infrastructure/repositories/sqlalchemy_product_repository.py`
326
+ implementando `ProductRepository` — **não precisa registrar nada em
327
+ `di/`**, o autodiscovery liga sozinho na próxima subida.
328
+ 4. `presentation/http/schemas/product/` (Pydantic) e
329
+ `presentation/http/routes/v1/products_router.py`, usando
330
+ `Depends(container.resolve(SeuCasoDeUso))`. Inclua o router novo em
331
+ `presentation/http/routes/http_router.py`.
332
+ 5. Se alguma exceção nova precisar de um status HTTP específico, crie o
333
+ handler em `presentation/middleware/handlers/` e registre em
334
+ `registry.py` (senão, o fallback `AppExceptionHandler` já cobre com
335
+ HTTP 400).
336
+ 6. `make migration m="create products table"` seguido de `make migrate`.
337
+ 7. (Opcional) Repita o padrão de `presentation/websocket/routes/`,
338
+ `presentation/grpc/` e `infrastructure/queue/tasks/` se esta entidade
339
+ também precisar desses protocolos.
340
+
341
+ ## Stack
342
+
343
+ - FastAPI + Uvicorn (HTTP e WebSocket no mesmo processo)
344
+ - gRPC (`grpcio`, opcional — extra `grpc`) rodando como processo separado
345
+ - Typer (CLI — `waiross`)
346
+ - Celery + Redis (fila)
347
+ - SQLAlchemy 2.0 (async, driver `asyncpg`) + Alembic para migrations
348
+ - PostgreSQL
349
+ - JWT (access + refresh token) via PyJWT, senhas com `bcrypt` (lib pura, sem passlib)
350
+ - `uv` como gerenciador de pacotes
351
+
352
+ ## Testes
353
+
354
+ ```bash
355
+ make test
356
+ ```
357
+
358
+ Os testes de API (`tests/test_users_http.py`, `tests/test_health.py`)
359
+ rodam contra SQLite em memória (fixture em `tests/conftest.py`), sem
360
+ precisar de Postgres/Docker.
361
+
362
+ ## Migrations (Alembic)
363
+
364
+ Os modelos ORM vivem em `app/infrastructure/database/models/<entidade>_model.py`;
365
+ `migrations/env.py` importa `infrastructure/database/models/__init__.py`,
366
+ que por sua vez importa cada `XModel`, para o autogenerate enxergar todas
367
+ as tabelas. Ao criar uma entidade nova com tabela própria, adicione o
368
+ import do model novo em `infrastructure/database/models/__init__.py`.
369
+
370
+ ```bash
371
+ make migration m="descrição da mudança" # gera migration por autogenerate
372
+ make migrate # aplica
373
+ make downgrade # desfaz a última
374
+ ```
375
+
376
+ ## Comandos (Makefile)
377
+
378
+ | Comando | Descrição |
379
+ |---|---|
380
+ | `make install` | instala dependências (`uv sync`) |
381
+ | `make dev` | sobe HTTP + WebSocket com reload |
382
+ | `make run` | sobe a API em modo produção |
383
+ | `make migration m="..."` | gera nova migration |
384
+ | `make migrate` | aplica migrations pendentes |
385
+ | `make test` | roda a suíte de testes |
386
+ | `make lint` | roda o ruff |
387
+ | `make worker` | sobe um worker Celery |
388
+ | `make cli args="..."` | roda um comando da CLI (`waiross`) |
389
+ | `make proto` | gera os stubs gRPC a partir do `.proto` |
390
+ | `make grpc-server` | sobe o servidor gRPC (processo separado) |
391
+
392
+ ## Nota sobre este repositório
393
+
394
+ Este projeto passou por reestruturações "hard" sucessivas até chegar na
395
+ árvore atual (camadas técnicas no topo, entidade como subpasta dentro de
396
+ cada camada, DI auto-discovery). O sandbox que gerou esta versão não
397
+ conseguiu apagar os arquivos antigos do disco (restrição do sistema de
398
+ arquivos montado) — eles foram movidos para `_legacy_delete_me/` na raiz
399
+ do projeto (já no `.gitignore`) e não são importados por nada. Pode
400
+ apagar essa pasta manualmente quando quiser.
401
+
402
+ Publicação como template/framework e próximos passos (site de docs): veja
403
+ `PUBLISHING.md`.
@@ -0,0 +1,84 @@
1
+ # Publicando o Waiross
2
+
3
+ Hoje o Waiross é uma aplicação concreta (tem `users` implementado de ponta a
4
+ ponta) que também serve de template — não é uma biblioteca `pip install`
5
+ abstrata. Isso muda qual caminho de publicação faz sentido primeiro.
6
+
7
+ ## Caminho 1 — GitHub Template Repository (comece por aqui)
8
+
9
+ É o formato certo para o que existe hoje: alguém clica em "Use this
10
+ template", ganha uma cópia limpa do repo (sem histórico de commits) e
11
+ começa a apagar/adaptar o que não precisa (o módulo `users` de exemplo,
12
+ o adaptador gRPC, etc.).
13
+
14
+ Passos:
15
+
16
+ 1. Crie o repositório no GitHub (se ainda não existir): `github.com/new`.
17
+ 2. Suba o código: dentro da pasta do projeto,
18
+ `git init && git add . && git commit -m "Waiross v0.1.0"`, depois
19
+ `git remote add origin <url-do-repo> && git push -u origin main`.
20
+ (Confirme antes que `_legacy_delete_me/` está no `.gitignore` — já
21
+ está.)
22
+ 3. No GitHub: **Settings → General → Template repository**, marque a
23
+ caixa. A partir daí o botão "Use this template" aparece na página do
24
+ repo.
25
+ 4. Adicione uma licença (`LICENSE` — MIT é a escolha usual para
26
+ boilerplates) e tópicos no GitHub (`fastapi`, `python`, `boilerplate`,
27
+ `ddd`, `clean-architecture`) para aparecer em buscas.
28
+ 5. (Opcional) Crie uma release (`git tag v0.1.0 && git push --tags` +
29
+ "Draft a new release" no GitHub) para dar uma versão fixa a quem quiser
30
+ `pip install` uma versão específica do template mais adiante.
31
+
32
+ Onde acessar: tudo isso é feito na própria interface web do GitHub, na
33
+ página do repositório (`Settings`, aba lateral esquerda) — não precisa de
34
+ conta separada nem de aprovação de terceiros.
35
+
36
+ ## Caminho 2 — Pacote no PyPI (etapa futura, se fizer sentido)
37
+
38
+ Só vale a pena se o Waiross deixar de ser "aplicação de exemplo + template"
39
+ e virar de fato uma biblioteca instalável (ex.: um pacote `waiross-core` com
40
+ o `Container`, o `ExceptionHandler`, os decorators — sem o módulo `users`
41
+ de exemplo, que ficaria só no template do GitHub). Isso é um trabalho de
42
+ extração, não um passo de publicação.
43
+
44
+ Se/quando chegar lá:
45
+
46
+ 1. Crie uma conta em `pypi.org` (e em `test.pypi.org` para testar antes).
47
+ 2. Gere um API token em `pypi.org/manage/account/token/`.
48
+ 3. `uv build` gera o `.whl`/`.tar.gz` em `dist/`.
49
+ 4. `uv publish` (ou `twine upload dist/*`) envia para o PyPI, usando o
50
+ token como credencial.
51
+ 5. A partir daí, qualquer pessoa roda `pip install waiross-core` (o nome
52
+ `waiross` sozinho no PyPI pode já estar ocupado — vale checar em
53
+ `pypi.org/project/waiross/` antes e ter um nome alternativo em mente,
54
+ como `waiross-framework` ou `waiross-core`).
55
+
56
+ ## Próxima etapa: site do projeto
57
+
58
+ Um site de documentação (não uma landing page de marketing) é o que mais
59
+ ajuda adoção nesse estágio. Duas opções diretas, ambas grátis via GitHub
60
+ Pages:
61
+
62
+ - **MkDocs + Material for MkDocs** — Python, Markdown puro, configuração
63
+ em um único `mkdocs.yml`. Mais rápido de montar a partir do que já
64
+ existe (o `README.md` já está estruturado em seções que viram páginas).
65
+ `pip install mkdocs-material`, `mkdocs new docs`, `mkdocs gh-deploy`.
66
+ - **Docusaurus** — React/Node, mais recursos prontos (versionamento de
67
+ docs, busca integrada, blog). Vale se o projeto crescer e quiser algo
68
+ mais parecido com a documentação do Laravel/Next.js.
69
+
70
+ Passos práticos, na ordem:
71
+
72
+ 1. Quebrar o `README.md` atual em páginas: Introdução, Arquitetura
73
+ (a árvore de diretórios), DI, Exceções, Protocolos (HTTP/WS/gRPC/
74
+ Fila/CLI), Como criar uma capacidade nova, Deploy.
75
+ 2. Rodar localmente (`mkdocs serve`) até o conteúdo estar bom.
76
+ 3. `mkdocs gh-deploy` publica automaticamente em
77
+ `https://<seu-usuário>.github.io/<repo>/` — sem precisar de hospedagem
78
+ própria.
79
+ 4. (Opcional) Domínio próprio: registrar algo como `waiross.dev` ou
80
+ `wairossframework.dev` (verificar disponibilidade em qualquer registrar —
81
+ Namecheap, Cloudflare Registrar) e apontar via `CNAME` para o GitHub
82
+ Pages.
83
+ 5. Adicionar um badge/link do site no `README.md` do repo assim que estiver
84
+ no ar.