Estructura de directorios¶
app/
Core/ # EL FRAMEWORK (genérico, reutilizable)
Config/ # Settings (pydantic-settings, lee .env)
Console/ # kernel de consola (Typer) + comandos base
CeleryApp/ # app de Celery + dispatch (broker-agnóstico)
Cron/ # @cron_task + helpers de cadencia (scheduler estilo Laravel)
Database/ # Base, Session, Repository, @transactional, mixins
Http/ # create_app() FastAPI + middlewares + frontera de locale
Mail/ # Mailable + Mailer (smtp/log/null) + facade Mail + task Celery
Translate/ # i18n (i18nice, YAML)
View/ # motor de templates (Jinja2) + helper view()
Auth/ # validación de tokens OAuth2 de Laravel Passport
Registry/ # auto-discovery de routers, tasks, crons, estáticos, CLI
Models/ # modelos SQLAlchemy COMPARTIDOS (auto-discovery; vacío en el base)
Dictionaries/ # constantes de dominio (auto-discovery por submódulo)
Modules/
Example/ # módulo de ejemplo (plantilla de cómo construir uno)
Resources/ # vistas/lang/estáticos COMPARTIDOS del proyecto
surcos/ # (opt-in) microfrontends Vite, uno por vertical (workspace pnpm)
public/ # (opt-in) builds de los surcos (public/<app>); milpa lo sirve en /vite
Tests/ # tests unitarios (espeja app/ 1:1, sin BD)
documentation/ # esta documentación (pública)
secrets/ # llaves locales (contenido ignorado por git)
jornal # entrypoint de consola (el "artisan") en la raíz
start.sh / stop.sh # arranque/paro daemonizado de uvicorn (PID + logs)
docker-compose.yml # SOLO infra: redis + mailpit (+ rabbitmq opcional)
pyproject.toml # deps, extras de drivers, config de ruff/mypy/pytest/import-linter
Las tres capas¶
1. El kernel — milpa/Core¶
Es el framework. Genérico, sin sabor de dominio, reutilizable entre proyectos. Cada subcarpeta es un subsistema (espejo de los "componentes" de Laravel):
| Carpeta | Equivalente Laravel | Doc |
|---|---|---|
Config |
config() + .env |
Configuración |
Http |
Kernel HTTP + middleware | Ciclo de vida |
Console |
Kernel de consola / artisan | Consola |
Database |
Eloquent + migraciones | Base de datos |
Mail |
Mail + Mailables | Correo |
Cron |
Task Scheduling | Cron |
CeleryApp |
Queues | Colas y tareas |
Translate |
Localization | i18n |
View |
Blade / Views | Vistas |
Auth |
Auth / Passport | Autenticación |
Registry |
Service Provider auto-discovery | Monolito modular |
Regla: Core nunca importa de Modules. Lo fuerza import-linter.
2. El kernel compartido de dominio — app/Models, app/Dictionaries, app/Resources¶
Lo que comparten varios módulos:
app/Models/— modelos SQLAlchemy. Auto-descubiertos conpkgutil(SQLAlchemy necesita verlos todos para resolver relaciones por string). Un modelo por archivo.app/Dictionaries/— clases de constantes de dominio. Se importan por submódulo (from app.Dictionaries.MiDict import MiDict); no necesitan auto-discovery.app/Resources/— vistas (Views/), traducciones (Lang/), estáticos (Static/), imágenes y archivos COMPARTIDOS por todo el proyecto.
3. Los módulos — app/Modules/<Nombre>¶
Tus features. Cada módulo es autocontenido e independiente de los demás. El encarpetado
es libre: el discovery importa todo el árbol del módulo, así que sueltas la pieza
donde te quede y milpa la descubre. Ninguna carpeta es obligatoria. La convención que
producen los generadores make:* —una carpeta por concern— es solo una de las formas de
organizarse:
app/Modules/Example/
Http/ # @Controller (rutas; se auto-montan en create_app())
Jobs/ # @job (background on-demand)
Crons/ # @cron_task (agendados)
Observers/ # subclases de Observer (reaccionan a eventos)
Handlers/ # @handles(Cmd) del Mediator
Policies/ # políticas de autorización (RBAC/ABAC)
Mail/ # Mailables del módulo
Console/
Commands/ # @console_command (de su path se deduce el grupo CLI)
Resources/ # Lang/, Views/, Static/ propios (namespaced)
Este layout es una PROPUESTA, no una imposición
El demo (y lo que generan los make:*) sigue esta convención —carpeta por concern,
Jobs/ExportNotesJob.py, Mail/InvoiceMailable.py, …— para que se lea de un vistazo.
Pero el discovery no la exige: importa todo el árbol del módulo. Si prefieres
aplanar (Jobs.py) o agrupar de otra manera, funciona igual —la pieza se descubre
mientras lleve su decorador o herede de su base—. Para una prueba de concepto puedes
escribir todo de corrido en un solo archivo (job + cron + observer + handler +
command + policy juntos) y milpa registra todo. La única convención con peso es
Console/Commands/, de cuyo path se deduce el grupo CLI de cada @console_command. El
guardrail test_FreeLayoutDiscovery fija esta libertad.
El Http/ también se importa fuera de la web — pero ahí solo se registra
El barrido recursivo importa Http/ de cada módulo también en el CLI y el worker. Es
una decisión consciente: ahí los decoradores de ruta (@Controller, @Get, …) solo
se registran, no sirven nada — quien sirve es la capa web (create_app(), vía
iter_routers()). Importar un controller fuera del proceso web no levanta una ruta.
Ver Monolito modular para el detalle de cómo se descubre y monta cada cosa, y Rutas y controladores para crear el primero.