Skip to main content

A modern, async-first, type-safe Python web framework with Spec-Driven Development (SDD)

Project description

🌌 Aura Framework

O framework Python moderno que você sempre quis.
Async nativo · Type-safe · Spec-Driven · Módulos · Jobs integrados · HTML server-rendered


Aura nasceu da frustração real com Django, FastAPI e Flask.
Frameworks que ou te dão baterias antigas, ou te deixam comprar tudo separado.
Aura entrega o melhor dos dois mundos: opiniões certas nos lugares certos, liberdade onde importa.


✨ Por que Aura?

Problema real Como outros resolvem Como Aura resolve
settings.py com 500 linhas Django: um arquivo global aura.toml modular + config type-safe por seção
ORM síncrono em stack async Django: sync_to_async() em todo lugar SQLAlchemy 2.x async genuíno desde o core
Serializers fazem tudo (DRF) ViewSet + Serializer + Permissions misturados Schemas (DTOs) separados de Services e Controllers
Celery complexo, sem async Celery 5: ainda sem async def nativo @task(queue="emails") — async de verdade
DI só funciona no HTTP FastAPI: Depends() não roda em jobs/CLI DIContainer funciona em qualquer contexto
Typing quebra com mypy Django: metaclass magic quebra o type checker Pydantic v2 em todo o framework, mypy strict
Sem estrutura de projeto FastAPI: 82+ boilerplates diferentes @Module NestJS-inspired com DI encapsulado
N+1 queries em produção DRF: serializers aninhados sem select_related Repository[T] com métodos otimizados
Context dict não tipado Django templates: render(request, "tmpl.html", {"key": val}) TemplateContext (Pydantic) — validado antes de renderizar

🚀 Instalação

# Instalação básica (API REST)
pip install "aura-web[uvicorn]"

# Com suporte a templates HTML (Jinja2)
pip install "aura-web[uvicorn,templates]"

# Tudo incluído
pip install "aura-web[all]"

⚡ Início rápido

Projeto novo (CLI)

# Cria um projeto completo e funcional com um módulo Users pronto
aura new meu-projeto
cd meu_projeto
pip install -e ".[dev]"
aura run --reload

Isso gera a estrutura completa:

meu_projeto/
├── main.py                      # app = Aura(modules=[UsersModule])
├── aura.toml                    # configuração do servidor
├── pyproject.toml
├── .env.example
├── modules/
│   └── users/
│       ├── schemas.py           # CreateUserDTO, UpdateUserDTO, UserResponse
│       ├── service.py           # @injectable UserService (in-memory, pronto pra usar)
│       ├── controller.py        # CRUD completo: @get @post @put @delete
│       └── module.py            # @Module(providers, controllers, prefix)
└── tests/
    ├── conftest.py              # AsyncClient via ASGITransport
    └── test_users.py            # 7 testes de integração

API REST com ORM assíncrono

Django tem ORM mas é síncrono — precisa de sync_to_async() em todo lugar.
FastAPI não tem ORM — você conecta SQLAlchemy na mão com Depends(get_db).
Aura tem AuraModel + Repository[T] + db.session() — async nativo, sem gambiarras.

# modules/posts/models.py
from aura.orm import AuraModel
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Text, Boolean

class Post(AuraModel):
    __tablename__ = "posts"

    title:     Mapped[str]  = mapped_column(String(200))
    body:      Mapped[str]  = mapped_column(Text)
    published: Mapped[bool] = mapped_column(Boolean, default=False)
    # id, created_at, updated_at → herdados de AuraModel automaticamente
# modules/posts/schemas.py  ← a Spec (SDD): fonte da verdade para validação e OpenAPI
from aura import Schema

class CreatePostDTO(Schema):
    title: str
    body: str

class UpdatePostDTO(Schema):
    title: str | None = None
    body:  str | None = None

class PostResponse(Schema):
    model_config = {"from_attributes": True}  # aceita ORM objects diretamente

    id:        int
    title:     str
    body:      str
    published: bool
# modules/posts/repository.py
from aura.orm import Repository
from sqlalchemy import select
from .models import Post

class PostRepository(Repository[Post]):
    model = Post

    # Repository[T] já inclui: get, get_or_raise, list, create, update,
    # delete, exists, count, first, bulk_create — sem escrever SQL.
    # Adicione métodos customizados para consultas específicas:

    async def list_published(self, *, limit: int = 20) -> list[Post]:
        stmt = (
            select(Post)
            .where(Post.published == True)
            .order_by(Post.created_at.desc())
            .limit(limit)
        )
        result = await self.session.execute(stmt)
        return list(result.scalars().all())
# modules/posts/service.py
from aura import injectable, NotFoundException
from aura.orm import db          # singleton DatabaseManager — inicializado no main.py
from .models import Post
from .repository import PostRepository
from .schemas import CreatePostDTO, UpdatePostDTO, PostResponse

@injectable
class PostService:
    """Business logic — não sabe nada de HTTP, só de Posts."""

    async def list_posts(self) -> list[PostResponse]:
        async with db.session() as session:           # ← abre transação async
            posts = await PostRepository(session).list()
            return [PostResponse.model_validate(p) for p in posts]

    async def get_post(self, post_id: int) -> PostResponse:
        async with db.session() as session:
            post = await PostRepository(session).get_or_raise(post_id)
            return PostResponse.model_validate(post)  # 404 automático se não existir

    async def create_post(self, data: CreatePostDTO) -> PostResponse:
        async with db.session() as session:           # ← commit automático ao sair
            post = await PostRepository(session).create(**data.model_dump())
            return PostResponse.model_validate(post)

    async def update_post(self, post_id: int, data: UpdatePostDTO) -> PostResponse:
        async with db.session() as session:
            updates = {k: v for k, v in data.model_dump().items() if v is not None}
            post = await PostRepository(session).update(post_id, **updates)
            return PostResponse.model_validate(post)  # rollback automático em exceção

    async def delete_post(self, post_id: int) -> None:
        async with db.session() as session:
            deleted = await PostRepository(session).delete(post_id)
            if not deleted:
                raise NotFoundException(f"Post {post_id} not found")
# modules/posts/controller.py  ← handlers finos: recebem input, chamam service, retornam
from typing import Annotated
from aura import get, post, put, delete, Body, Param
from .schemas import CreatePostDTO, UpdatePostDTO, PostResponse
from .service import PostService

class PostsController:
    def __init__(self, service: PostService) -> None:
        self.service = service          # injetado pelo DI container

    @get("/")
    async def list_posts(self) -> list[PostResponse]:
        return await self.service.list_posts()

    @get("/{post_id}")
    async def get_post(self, post_id: Annotated[int, Param()]) -> PostResponse:
        return await self.service.get_post(post_id)

    @post("/", status=201)
    async def create_post(self, body: Annotated[CreatePostDTO, Body()]) -> PostResponse:
        return await self.service.create_post(body)

    @put("/{post_id}")
    async def update_post(
        self,
        post_id: Annotated[int, Param()],
        body:    Annotated[UpdatePostDTO, Body()],
    ) -> PostResponse:
        return await self.service.update_post(post_id, body)

    @delete("/{post_id}", status=204)
    async def delete_post(self, post_id: Annotated[int, Param()]) -> None:
        await self.service.delete_post(post_id)
# modules/posts/module.py
from aura import Module
from .controller import PostsController
from .service import PostService

@Module(providers=[PostService], controllers=[PostsController], prefix="/posts", tags=["Posts"])
class PostsModule:
    pass
# main.py
from aura import Aura
from aura.orm import db
from modules.posts.models import Post
from modules.posts.module import PostsModule

# Inicializa o pool de conexões async (SQLite em dev, PostgreSQL em produção)
db.init("sqlite+aiosqlite:///./app.db")

app = Aura(modules=[PostsModule], title="Blog API", version="1.0.0")

# Para criar as tabelas em dev (use `aura migrate up` em produção):
# import asyncio; asyncio.run(db.create_all(Post))
aura run --reload
# POST /posts/    → cria post (persiste no banco)
# GET  /posts/    → lista todos
# GET  /posts/1   → busca por ID (404 automático se não existir)
# PUT  /posts/1   → atualiza
# DELETE /posts/1 → remove
# GET  /docs      → Swagger UI auto-gerado

Full-stack com HTML (Jinja2 + htmx)

@html e @sse são separados de @get/@post — eles coexistem no mesmo controller. Use @get/@post/etc. para JSON API. Use @html para páginas server-rendered.

from aura import Module
from aura.templates import html, sse, render, TemplateContext, AuraTemplateModule

class PostListContext(TemplateContext):
    """Spec do que o template posts/list.html precisa — validado pelo Pydantic."""
    posts: list[PostResponse]
    total: int
    page: int = 1

class PostsWebController:
    def __init__(self, service: PostService) -> None:
        self.service = service

    @html("/", template="posts/list.html")
    async def list_page(self) -> PostListContext:
        """Retorna TemplateContext → router renderiza automaticamente."""
        posts = await self.service.list_posts()
        return PostListContext(posts=posts, total=len(posts))

    @html("/{post_id}")
    async def detail_page(
        self,
        post_id: Annotated[int, Param()],
        request: AuraRequest,
    ) -> HtmlResponse:
        """Controle total: retorna fragmento htmx ou página completa."""
        post = await self.service.get_post(post_id)
        ctx = PostDetailContext(post=post)
        if request.htmx.is_htmx:
            return await render("posts/partials/detail.html", ctx)
        return await render("posts/detail.html", ctx)

    @sse("/live")
    async def live_updates(self):
        """Server-Sent Events — yield dicts ou strings."""
        async for event in event_bus.subscribe("posts"):
            yield {"type": "new_post", "id": event.post_id}

@Module(
    providers=[PostService],
    controllers=[PostsController, PostsWebController],
    prefix="/posts",
)
class PostsModule:
    pass

# main.py — adiciona AuraTemplateModule
app = Aura(
    modules=[
        AuraTemplateModule.for_root("templates"),  # configura o engine Jinja2
        PostsModule,
    ]
)

Diferença entre @html e @get

class UserController:
    # ✅ JSON API — use @get, @post, @put, @delete
    @get("/")
    async def list_users_api(self) -> list[UserResponse]:
        return await self.service.list_users()          # → {"id": 1, "name": ...}

    # ✅ Página HTML — use @html
    @html("/", template="users/list.html")
    async def list_users_page(self) -> UserListContext:
        users = await self.service.list_users()
        return UserListContext(users=users)              # → renderiza users/list.html

    # ✅ Live updates — use @sse
    @sse("/events")
    async def user_events(self):
        async for event in bus.subscribe("users"):
            yield {"action": event.type, "id": event.id}  # → text/event-stream

📦 CLI completo

# Criar projeto
aura new meu-projeto                     # scaffolding completo e funcional
aura new meu-projeto --dir ~/projetos    # em outro diretório

# Generators (todos geram código funcional, sem comentários)
aura generate module posts               # schemas + service + controller + module + tests
aura generate resource product           # alias para module
aura generate controller auth            # só o controller
aura generate service email              # @injectable service
aura generate schema invoice             # Create/Update/Response DTOs
aura generate guard jwt                  # Guard stub

# Flags
aura generate module posts --no-tests    # sem arquivo de testes
aura generate module posts --force       # sobrescrever existentes

# Servidor
aura run                                 # uvicorn (auto-detecta main:app)
aura run --reload                        # hot-reload em dev
aura run --host 0.0.0.0 --port 8080      # customizar host/porta
aura run --workers 4                     # múltiplos workers (produção)

# Banco de dados
aura migrate make "add posts table"      # criar migration (Alembic)
aura migrate up                          # aplicar migrations
aura migrate down                        # reverter última
aura migrate status                      # listar status

# Workers (jobs assíncronos)
aura worker --queue emails               # processar fila específica

🏗️ Estrutura de um módulo

modules/
└── posts/
    ├── __init__.py
    ├── schemas.py       ← DTOs: CreatePostDTO, UpdatePostDTO, PostResponse
    ├── service.py       ← @injectable PostService (business logic)
    ├── controller.py    ← HTTP handlers (@get, @post, @html, @sse)
    ├── module.py        ← @Module(providers, controllers, prefix, tags)
    └── repository.py    ← Repository[Post] (quando usar SQLAlchemy)

📚 Documentação

Documento Descrição
Motivação e Comparativo Por que Aura existe, dores que resolve
Schemas e Validação DTOs, Pydantic v2, validação
ORM e Queries Repository pattern, CRUD, filtros
Jobs e Workers @task, queues, periodic jobs
Módulos e DI Sistema de módulos, injeção de dependência
Templates @html, @sse, TemplateContext, Components, htmx
SDD Spec-Driven Development — a filosofia do Aura
Roadmap O que está sendo construído

✅ O que já está pronto

Core

  • App ASGI (Starlette core)
  • Routing: @get, @post, @put, @delete, @patch, @ws
  • Parameter binding: Body, Query, Param, Header, Cookie
  • Injeção automática de AuraRequest por type hint (sem marcador)
  • @Module com providers, controllers, imports, exports, prefix, tags, guards
  • DIContainer com lifetimes SINGLETON, SCOPED, TRANSIENT
  • @injectable (com e sem parênteses)
  • Schema e ResponseSchema (Pydantic v2)
  • Hierarquia completa de HTTPException (400–504)
  • Guard interface com can_activate / on_denied
  • AuraConfig com pydantic-settings e aura.toml
  • AuraRequest com .htmx, .user, .container

ORM

  • AuraModel com id, created_at, updated_at
  • Repository[T]: get, list, create, update, delete, exists, count, bulk_create
  • Suporte a SQLAlchemy 2.x async

Jobs

  • @task e @periodic decorators
  • MemoryBackend (dev/test)
  • SAQ backend stub (integração pendente)

Templates (HTML server-rendered)

  • TemplateContext — Pydantic model como spec do template
  • HtmlResponse com suporte a headers HX-*
  • AuraTemplateEngine — Jinja2 com async nativo
  • Component — classes Python com Props tipadas
  • HtmxInfo — detecta requests htmx via headers
  • HtmxResponseHeaders — builder fluente para HX-Trigger, HX-Redirect, etc.
  • render(), render_string(), render_to_string()
  • @html decorator — rotas que retornam HTML (integrado ao router)
  • @sse decorator — Server-Sent Events (streaming async)
  • Auto-conversão: TemplateContext → render, str → HtmlResponse, Response → passthrough
  • HTML error pages para exceções HTTP em rotas @html
  • AuraTemplateModule.for_root() — configura Jinja2 via módulo

OpenAPI / Docs

  • OpenAPI 3.1 auto-gerado
  • Swagger UI embutido em /docs
  • ReDoc embutido em /redoc
  • /health automático

CLI

  • aura version
  • aura new <name> — scaffolding completo com módulo Users funcional
  • aura generate module/resource/controller/service/schema/guard
  • aura run (uvicorn/granian)
  • aura migrate make/up/down/status (stubs)
  • aura worker (stub)

Qualidade

  • 156 testes passando
  • Publicado no PyPI como aura-web

🚧 O que está pendente

Alta prioridade (v0.2.0)

  • AuraTemplateModule no ciclo de vidaon_startup precisa ser chamado pelo ModuleRegistry para configurar o engine antes dos primeiros requests
  • url_for() em templates — resolver URLs por nome de rota dentro do Jinja2
  • static() tag real — integrar com StaticFiles do Starlette
  • DatabaseManager na startup — ler AURA__DATABASE__URL e iniciar pool automaticamente
  • aura migrate make/up/down — implementar os wrappers Alembic (stubs existem)
  • SAQ backend integradoAuraTask.dispatch() usando Redis + SAQ em produção
  • aura worker funcional — conectar ao SAQ e processar filas

Média prioridade (v0.3.0)

  • JWTGuard builtinpython-jose, extrai Bearer, popula request.state.user
  • RateLimitGuard — rate limiting por IP/usuário
  • Repository.paginate() — retorna Page[T] com total, has_next, etc.
  • async with db.transaction() — unit-of-work para múltiplos repos
  • InterceptorsLoggingInterceptor, TimingInterceptor, CacheInterceptor
  • @Gateway WebSocket — rooms, broadcast, on_connect/on_disconnect
  • Islands Architecture[data-island] + /js/islands.js para hidratação seletiva
  • CORSMiddleware simplificado — config via AuraConfig.cors

Roadmap futuro (v0.4.0+)

  • AdminModule — painel admin auto-gerado a partir dos models
  • GraphQL via Strawberry (GraphQLModule.for_root)
  • Multi-tenancy (row-level e schema-level)
  • Plugin system (RedisPlugin, S3Plugin, SentryPlugin)
  • Benchmarks vs FastAPI / Django / Litestar
  • Site de documentação (MkDocs + GitHub Pages)
  • Exemplos completos: examples/todo-app, examples/blog, examples/ecommerce

⚠️ O que precisa melhorar

Bugs conhecidos / limitações atuais

Item Status Impacto
AuraTemplateModule.on_startup não é chamado pelo registry Workaround: chamar set_engine() manualmente Alto — templates não funcionam sem setup manual
aura migrate make não implementado Stub retorna "não implementado" Alto — devs precisam usar Alembic diretamente
aura worker não processa filas Stub sem SAQ integrado Alto — jobs ficam no MemoryBackend
SAQ backend não conecta em produção saq_backend.py existe mas não está integrado Alto — jobs morrem ao reiniciar o servidor
Erro em route @html sem template= com TemplateContext Lança ValueError claro Baixo — erro descritivo
_resolve_params não resolve parâmetros de path sem Annotated[int, Param()] Precisa do marker explícito Médio — mais verboso que ideal

Melhorias de DX (Developer Experience)

  • @get sem Annotatedasync def get_user(self, user_id: int) deveria inferir Param() para path params
  • Mensagens de erro melhores — quando DI falha, o stack trace atual é confuso
  • Hot-reload de módulos — hoje aura run --reload reinicia o processo inteiro; modules poderiam ser recarregados individualmente
  • Autocompletar CLIaura generate <TAB> não funciona em todos os shells
  • aura generate component <name> — gera Component class + template HTML juntos

Performance

  • Serialização com msgspec (10x mais rápido que Pydantic para respostas simples)
  • Route caching — compilação de rotas acontece a cada startup
  • Template bytecode cache para produção

🗂️ Extras instaláveis

pip install "aura-web[uvicorn]"      # servidor ASGI (recomendado)
pip install "aura-web[granian]"      # servidor Rust (mais rápido)
pip install "aura-web[templates]"    # Jinja2 + HTML rendering
pip install "aura-web[sqlalchemy]"   # ORM async + migrations
pip install "aura-web[saq]"          # async job queue
pip install "aura-web[redis]"        # Redis client async
pip install "aura-web[jwt]"          # JWT auth (python-jose)
pip install "aura-web[all]"          # tudo acima

📄 Licença

MIT © Jonathas David


PyPI · Documentação · Issues

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aura_web-0.2.0.tar.gz (124.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aura_web-0.2.0-py3-none-any.whl (105.6 kB view details)

Uploaded Python 3

File details

Details for the file aura_web-0.2.0.tar.gz.

File metadata

  • Download URL: aura_web-0.2.0.tar.gz
  • Upload date:
  • Size: 124.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for aura_web-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ac29a1e148441def75d8a54079fe7750eb992f1018fc46611758dd7880a25f9a
MD5 f02a09e94c56b458083d101dea95c6b3
BLAKE2b-256 6577ca1a8886be895ddb36f71a001614822a55f21dbc4d5f975638b7f060575e

See more details on using hashes here.

File details

Details for the file aura_web-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: aura_web-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 105.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for aura_web-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a139c43dfb7a22063c9cba8b3f396ced60e1359b030803169f9b3996db69c649
MD5 d40fcccd90fb3c35ddadc87e2254862a
BLAKE2b-256 08a42988b27251beefebd7bbb44c6de49fc16ec0fa4cf9674060ec2cec3b6c43

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page