From f309f3065b457d186882bde54c930028db906c1e Mon Sep 17 00:00:00 2001 From: Staging-Devin AI <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:01:16 +0000 Subject: [PATCH] docs: improve documentation with docstrings, type hints, and README - Add comprehensive docstrings to all Python modules and functions - Add type hints for parameters and return values - Add inline comments explaining complex logic - Create detailed README.md with: - Project description and structure - Installation and configuration instructions - How to run the project (local and Docker) - API endpoints documentation with examples - Database migration instructions - Dependencies list - Environment variables reference Co-Authored-By: sam.fertig@cognition.ai --- README.md | 339 +++++++++++++++++++++++++++++++++++++++++++++ app/__init__.py | 24 +++- app/actions.py | 183 ++++++++++++++++++++++-- app/config.py | 77 +++++++++- app/db/__init__.py | 28 +++- app/db/base.py | 67 ++++++++- app/db/session.py | 41 +++++- app/main.py | 240 ++++++++++++++++++++++++++++++-- app/models.py | 52 ++++++- app/schemas.py | 129 ++++++++++++++++- migrations/env.py | 153 ++++++++++++++------ tests/test_app.py | 37 ++++- 12 files changed, 1284 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index e69de29..3204622 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,339 @@ +# FastAPI Simple App Example + +Uma aplicacao FastAPI de exemplo que demonstra uma estrutura basica de API REST com operacoes CRUD para gerenciamento de posts. + +## Descricao do Projeto + +Este projeto e uma aplicacao de exemplo que implementa uma API REST completa utilizando FastAPI, um framework moderno e de alta performance para construcao de APIs em Python. A aplicacao demonstra boas praticas de desenvolvimento, incluindo separacao de responsabilidades, uso de ORM, validacao de dados e documentacao automatica. + +### Tecnologias Utilizadas + +- **FastAPI**: Framework web moderno e de alta performance para construcao de APIs +- **SQLAlchemy**: ORM (Object-Relational Mapping) para interacao com o banco de dados +- **Pydantic**: Validacao de dados e serializacao usando type hints do Python +- **PostgreSQL**: Banco de dados relacional +- **Alembic**: Ferramenta de migracao de banco de dados +- **Docker**: Containerizacao da aplicacao +- **Poetry**: Gerenciamento de dependencias Python +- **Uvicorn**: Servidor ASGI de alta performance + +## Estrutura do Projeto + +``` +fastapi-simple-app-example/ +├── app/ # Pacote principal da aplicacao +│ ├── __init__.py # Inicializacao do pacote e versao +│ ├── config.py # Configuracoes da aplicacao (variaveis de ambiente) +│ ├── main.py # Aplicacao FastAPI e endpoints da API +│ ├── models.py # Modelos SQLAlchemy (tabelas do banco de dados) +│ ├── schemas.py # Schemas Pydantic (validacao de entrada/saida) +│ ├── actions.py # Operacoes CRUD (Create, Read, Update, Delete) +│ └── db/ # Configuracao do banco de dados +│ ├── __init__.py # Exporta Base e importa modelos +│ ├── base.py # Classe Base declarativa do SQLAlchemy +│ └── session.py # Engine e SessionLocal do SQLAlchemy +├── migrations/ # Migracoes do Alembic +│ ├── env.py # Configuracao do ambiente Alembic +│ └── versions/ # Scripts de migracao +│ └── ee48ba03fe9f_create_posts_table.py +├── tests/ # Testes da aplicacao +│ ├── __init__.py +│ └── test_app.py # Testes unitarios +├── .env.example # Exemplo de variaveis de ambiente +├── alembic.ini # Configuracao do Alembic +├── docker-compose.yml # Configuracao do Docker Compose +├── docker-compose.override.yml # Override para desenvolvimento +├── Dockerfile # Imagem Docker da aplicacao +├── pyproject.toml # Configuracao do Poetry e dependencias +├── poetry.lock # Lock file das dependencias +└── prestart.sh # Script executado antes de iniciar a aplicacao +``` + +## Requisitos + +- Python 3.8 ou superior +- PostgreSQL 12 ou superior +- Poetry (para gerenciamento de dependencias) +- Docker e Docker Compose (opcional, para execucao containerizada) + +## Instalacao e Configuracao + +### Opcao 1: Instalacao Local + +1. Clone o repositorio: +```bash +git clone https://github.com/samfert-codeium/fastapi-simple-app-example.git +cd fastapi-simple-app-example +``` + +2. Instale as dependencias usando Poetry: +```bash +poetry install +``` + +3. Configure as variaveis de ambiente. Copie o arquivo de exemplo e edite conforme necessario: +```bash +cp .env.example .env +``` + +4. Edite o arquivo `.env` com suas configuracoes: +```env +# PostgreSQL +POSTGRES_SERVER=localhost +POSTGRES_USER=postgres +POSTGRES_PASSWORD=sua_senha +POSTGRES_DB=app +``` + +5. Crie o banco de dados PostgreSQL: +```bash +createdb app +``` + +6. Execute as migracoes do banco de dados: +```bash +poetry run alembic upgrade head +``` + +### Opcao 2: Usando Docker + +1. Clone o repositorio: +```bash +git clone https://github.com/samfert-codeium/fastapi-simple-app-example.git +cd fastapi-simple-app-example +``` + +2. Configure as variaveis de ambiente: +```bash +cp .env.example .env +``` + +3. Inicie os containers: +```bash +docker-compose up -d +``` + +Isso ira iniciar: +- Servidor da aplicacao FastAPI na porta 8000 +- Banco de dados PostgreSQL +- PgAdmin na porta 8080 (para administracao do banco) + +## Como Executar o Projeto + +### Execucao Local + +Ative o ambiente virtual do Poetry e inicie o servidor: +```bash +poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +### Execucao com Docker + +```bash +docker-compose up +``` + +A aplicacao estara disponivel em: http://localhost:8000 + +### Documentacao Interativa + +Apos iniciar a aplicacao, acesse: +- **Swagger UI**: http://localhost:8000/docs +- **ReDoc**: http://localhost:8000/redoc + +## Endpoints da API + +A API fornece os seguintes endpoints para gerenciamento de posts: + +| Metodo | Endpoint | Descricao | +|--------|----------|-----------| +| GET | `/` | Health check / boas-vindas | +| GET | `/posts` | Lista todos os posts (com paginacao) | +| POST | `/posts` | Cria um novo post | +| GET | `/posts/{id}` | Recupera um post especifico | +| PUT | `/posts/{id}` | Atualiza um post existente | +| DELETE | `/posts/{id}` | Remove um post | + +## Exemplos de Uso da API + +### Health Check + +```bash +curl http://localhost:8000/ +``` + +Resposta: +```json +{"message": "Hello world!"} +``` + +### Criar um Post + +```bash +curl -X POST http://localhost:8000/posts \ + -H "Content-Type: application/json" \ + -d '{"title": "Meu Primeiro Post", "body": "Conteudo do post aqui..."}' +``` + +Resposta (201 Created): +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "title": "Meu Primeiro Post", + "body": "Conteudo do post aqui..." +} +``` + +### Listar Posts + +```bash +curl http://localhost:8000/posts +``` + +Com paginacao: +```bash +curl "http://localhost:8000/posts?skip=0&limit=10" +``` + +Resposta: +```json +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "title": "Meu Primeiro Post", + "body": "Conteudo do post aqui..." + } +] +``` + +### Obter um Post Especifico + +```bash +curl http://localhost:8000/posts/123e4567-e89b-12d3-a456-426614174000 +``` + +Resposta: +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "title": "Meu Primeiro Post", + "body": "Conteudo do post aqui..." +} +``` + +### Atualizar um Post + +```bash +curl -X PUT http://localhost:8000/posts/123e4567-e89b-12d3-a456-426614174000 \ + -H "Content-Type: application/json" \ + -d '{"title": "Titulo Atualizado"}' +``` + +Resposta: +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "title": "Titulo Atualizado", + "body": "Conteudo do post aqui..." +} +``` + +### Deletar um Post + +```bash +curl -X DELETE http://localhost:8000/posts/123e4567-e89b-12d3-a456-426614174000 +``` + +Resposta: +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "title": "Titulo Atualizado", + "body": "Conteudo do post aqui..." +} +``` + +## Migracoes de Banco de Dados + +O projeto utiliza Alembic para gerenciamento de migracoes. + +### Aplicar Migracoes + +```bash +poetry run alembic upgrade head +``` + +### Criar Nova Migracao + +```bash +poetry run alembic revision --autogenerate -m "descricao da migracao" +``` + +### Reverter Ultima Migracao + +```bash +poetry run alembic downgrade -1 +``` + +### Ver Historico de Migracoes + +```bash +poetry run alembic history +``` + +## Testes + +Execute os testes usando pytest: + +```bash +poetry run pytest tests/ +``` + +Com cobertura de codigo: + +```bash +poetry run pytest tests/ --cov=app +``` + +## Dependencias + +### Producao + +- fastapi ^0.54.1 - Framework web +- sqlalchemy ^1.3.16 - ORM +- uvicorn ^0.11.5 - Servidor ASGI +- email_validator ^1.1.0 - Validacao de email +- psycopg2-binary ^2.8.5 - Driver PostgreSQL +- python-dotenv ^0.13.0 - Carregamento de .env +- alembic ^1.4.2 - Migracoes de banco de dados +- inflect ^4.1.0 - Pluralizacao de palavras + +### Desenvolvimento + +- pytest ^5.2 - Framework de testes +- flake8 ^3.8.1 - Linter +- autopep8 ^1.5.2 - Formatador de codigo +- mypy ^0.770 - Verificacao de tipos +- isort ^4.3.21 - Ordenacao de imports +- black ^19.10b0 - Formatador de codigo +- sqlalchemy-stubs ^0.3 - Type stubs para SQLAlchemy + +## Variaveis de Ambiente + +| Variavel | Descricao | Exemplo | +|----------|-----------|---------| +| POSTGRES_SERVER | Endereco do servidor PostgreSQL | localhost | +| POSTGRES_USER | Usuario do PostgreSQL | postgres | +| POSTGRES_PASSWORD | Senha do PostgreSQL | password | +| POSTGRES_DB | Nome do banco de dados | app | +| PGADMIN_DEFAULT_EMAIL | Email do admin do PgAdmin | admin@local.host | +| PGADMIN_DEFAULT_PASSWORD | Senha do PgAdmin | password | +| PGADMIN_LISTEN_PORT | Porta do PgAdmin | 8080 | + +## Licenca + +Este projeto e disponibilizado como exemplo educacional. + +## Autor + +Alexander van Zyl diff --git a/app/__init__.py b/app/__init__.py index 3dc1f76..e21f5b4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1,23 @@ -__version__ = "0.1.0" +""" +FastAPI Simple App Example. + +Este pacote contém uma aplicação FastAPI de exemplo que demonstra +uma estrutura básica de API REST com operações CRUD para posts. + +A aplicação utiliza: + - FastAPI para o framework web + - SQLAlchemy para ORM e interação com banco de dados + - Pydantic para validação de dados + - PostgreSQL como banco de dados + - Alembic para migrações de banco de dados + +Modules: + config: Configurações da aplicação usando variáveis de ambiente + models: Modelos SQLAlchemy para o banco de dados + schemas: Schemas Pydantic para validação de entrada/saída + actions: Operações CRUD genéricas e específicas + main: Endpoints da API FastAPI + db: Configuração de conexão com o banco de dados +""" + +__version__: str = "0.1.0" diff --git a/app/actions.py b/app/actions.py index b25bf86..7b4a2f5 100644 --- a/app/actions.py +++ b/app/actions.py @@ -1,3 +1,22 @@ +""" +Módulo de ações CRUD (Create, Read, Update, Delete). + +Este módulo implementa o padrão Repository para operações de banco de dados, +fornecendo uma classe base genérica (BaseActions) que pode ser estendida +para diferentes modelos. + +O padrão utilizado permite: + - Reutilização de código CRUD entre diferentes modelos + - Type hints genéricos para melhor suporte de IDE + - Separação clara entre lógica de negócio e acesso a dados + +Example: + >>> from app.actions import post + >>> from app.schemas import PostCreate + >>> new_post = PostCreate(title="Título", body="Conteúdo") + >>> created = post.create(db=session, obj_in=new_post) +""" + from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union from fastapi.encoders import jsonable_encoder @@ -8,33 +27,119 @@ from .db import Base from .models import Post -# Define custom types for SQLAlchemy model, and Pydantic schemas +# Tipos genéricos para tipagem forte das classes de ações +# ModelType: Tipo do modelo SQLAlchemy (deve herdar de Base) ModelType = TypeVar("ModelType", bound=Base) +# CreateSchemaType: Tipo do schema Pydantic para criação CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +# UpdateSchemaType: Tipo do schema Pydantic para atualização UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) class BaseActions(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - def __init__(self, model: Type[ModelType]): - """Base class that can be extend by other action classes. - Provides basic CRUD and listing operations. + """ + Classe base genérica para operações CRUD. + + Esta classe fornece implementações padrão para operações comuns de + banco de dados (Create, Read, Update, Delete) que podem ser herdadas + e customizadas por classes específicas de cada modelo. + + A classe utiliza Generics do Python para fornecer tipagem forte, + permitindo que IDEs e ferramentas de análise estática entendam + os tipos corretos de entrada e saída. + + Type Parameters: + ModelType: O modelo SQLAlchemy associado a esta classe de ações + CreateSchemaType: O schema Pydantic usado para criação de objetos + UpdateSchemaType: O schema Pydantic usado para atualização de objetos - :param model: The SQLAlchemy model - :type model: Type[ModelType] + Attributes: + model: A classe do modelo SQLAlchemy para operações de banco de dados + + Example: + >>> class UserActions(BaseActions[User, UserCreate, UserUpdate]): + ... pass + >>> user_actions = UserActions(User) + """ + + def __init__(self, model: Type[ModelType]) -> None: + """ + Inicializa a classe de ações com o modelo SQLAlchemy. + + Args: + model: A classe do modelo SQLAlchemy que será utilizada + para todas as operações de banco de dados desta instância. + + Example: + >>> post_actions = BaseActions(Post) """ self.model = model def get_all( self, db: Session, *, skip: int = 0, limit: int = 100 ) -> List[ModelType]: + """ + Recupera uma lista de objetos do banco de dados com paginação. + + Args: + db: Sessão do SQLAlchemy para acesso ao banco de dados + skip: Número de registros a pular (offset para paginação). Default: 0 + limit: Número máximo de registros a retornar. Default: 100 + + Returns: + List[ModelType]: Lista de objetos do modelo encontrados + + Example: + >>> posts = post_actions.get_all(db, skip=0, limit=10) + >>> len(posts) + 10 + """ return db.query(self.model).offset(skip).limit(limit).all() def get(self, db: Session, id: UUID4) -> Optional[ModelType]: + """ + Recupera um único objeto pelo seu ID. + + Args: + db: Sessão do SQLAlchemy para acesso ao banco de dados + id: UUID do objeto a ser recuperado + + Returns: + Optional[ModelType]: O objeto encontrado ou None se não existir + + Example: + >>> post = post_actions.get(db, id=some_uuid) + >>> if post: + ... print(post.title) + """ return db.query(self.model).filter(self.model.id == id).first() def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: + """ + Cria um novo objeto no banco de dados. + + O objeto Pydantic de entrada é convertido para um dicionário JSON-compatível, + que é então usado para instanciar o modelo SQLAlchemy. + + Args: + db: Sessão do SQLAlchemy para acesso ao banco de dados + obj_in: Schema Pydantic com os dados para criação do objeto + + Returns: + ModelType: O objeto criado com ID gerado pelo banco de dados + + Example: + >>> new_post = PostCreate(title="Título", body="Conteúdo") + >>> created = post_actions.create(db, obj_in=new_post) + >>> print(created.id) # UUID gerado automaticamente + """ + # Converte o schema Pydantic para dicionário JSON-compatível obj_in_data = jsonable_encoder(obj_in) + + # Cria instância do modelo SQLAlchemy com os dados db_obj = self.model(**obj_in_data) # type: ignore + + # Adiciona à sessão, commita e atualiza o objeto com dados do banco db.add(db_obj) db.commit() db.refresh(db_obj) @@ -47,30 +152,92 @@ def update( db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]] ) -> ModelType: + """ + Atualiza um objeto existente no banco de dados. + + Suporta atualização parcial - apenas os campos fornecidos em obj_in + serão atualizados. Aceita tanto um schema Pydantic quanto um dicionário. + + Args: + db: Sessão do SQLAlchemy para acesso ao banco de dados + db_obj: O objeto existente do banco de dados a ser atualizado + obj_in: Schema Pydantic ou dicionário com os campos a atualizar + + Returns: + ModelType: O objeto atualizado + + Example: + >>> existing_post = post_actions.get(db, id=some_uuid) + >>> update_data = PostUpdate(title="Novo Título") + >>> updated = post_actions.update(db, db_obj=existing_post, obj_in=update_data) + """ + # Converte o objeto do banco para dicionário para iterar sobre campos obj_data = jsonable_encoder(db_obj) + + # Extrai os dados de atualização do schema ou usa o dicionário diretamente if isinstance(obj_in, dict): update_data = obj_in else: + # exclude_unset=True garante que apenas campos explicitamente definidos sejam incluídos update_data = obj_in.dict(exclude_unset=True) + + # Atualiza apenas os campos que existem tanto no objeto quanto nos dados de atualização for field in obj_data: if field in update_data: setattr(db_obj, field, update_data[field]) + + # Persiste as alterações no banco de dados db.add(db_obj) db.commit() db.refresh(db_obj) return db_obj def remove(self, db: Session, *, id: UUID4) -> ModelType: + """ + Remove um objeto do banco de dados pelo seu ID. + + Args: + db: Sessão do SQLAlchemy para acesso ao banco de dados + id: UUID do objeto a ser removido + + Returns: + ModelType: O objeto que foi removido (antes da deleção) + + Example: + >>> deleted_post = post_actions.remove(db, id=some_uuid) + >>> print(f"Post '{deleted_post.title}' foi removido") + + Note: + O objeto retornado não existe mais no banco de dados após esta operação. + """ + # Recupera o objeto pelo ID obj = db.query(self.model).get(id) + + # Remove o objeto e commita a transação db.delete(obj) db.commit() return obj class PostActions(BaseActions[Post, schemas.PostCreate, schemas.PostUpdate]): - """Post actions with basic CRUD operations""" + """ + Classe de ações específica para o modelo Post. + + Herda todas as operações CRUD de BaseActions, tipada especificamente + para o modelo Post e seus schemas correspondentes. + + Esta classe pode ser estendida com métodos específicos para posts, + como busca por título, filtragem por data, etc. + + Example: + >>> from app.actions import post + >>> all_posts = post.get_all(db) + >>> single_post = post.get(db, id=some_uuid) + """ pass -post = PostActions(Post) +# Instância global para uso nos endpoints da API +# Permite acesso direto às operações CRUD de posts +post: PostActions = PostActions(Post) diff --git a/app/config.py b/app/config.py index cdcbc2d..6b5b2ef 100644 --- a/app/config.py +++ b/app/config.py @@ -1,9 +1,50 @@ +""" +Módulo de configuração da aplicação. + +Este módulo define as configurações da aplicação usando Pydantic BaseSettings, +que permite carregar valores de variáveis de ambiente automaticamente. + +As configurações incluem credenciais do PostgreSQL e a URI de conexão +com o banco de dados, que é construída automaticamente a partir das +credenciais individuais. + +Example: + As variáveis de ambiente podem ser definidas no sistema ou em um arquivo .env: + + ``` + POSTGRES_SERVER=localhost + POSTGRES_USER=postgres + POSTGRES_PASSWORD=password + POSTGRES_DB=app + ``` +""" + from typing import Any, Dict, Optional from pydantic import BaseSettings, PostgresDsn, validator class Settings(BaseSettings): + """ + Classe de configurações da aplicação. + + Esta classe utiliza Pydantic BaseSettings para carregar configurações + de variáveis de ambiente. Todas as variáveis são case-sensitive. + + Attributes: + POSTGRES_SERVER: Endereço do servidor PostgreSQL (ex: 'localhost' ou 'db') + POSTGRES_USER: Nome de usuário para conexão com o PostgreSQL + POSTGRES_PASSWORD: Senha para conexão com o PostgreSQL + POSTGRES_DB: Nome do banco de dados PostgreSQL + SQLALCHEMY_DATABASE_URI: URI completa de conexão com o banco de dados. + Se não fornecida, é construída automaticamente a partir das outras variáveis. + + Example: + >>> from app.config import settings + >>> print(settings.POSTGRES_SERVER) + 'localhost' + """ + POSTGRES_SERVER: str POSTGRES_USER: str POSTGRES_PASSWORD: str @@ -13,8 +54,27 @@ class Settings(BaseSettings): @validator("SQLALCHEMY_DATABASE_URI", pre=True) def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: + """ + Valida e constrói a URI de conexão com o banco de dados. + + Este validador é executado antes da validação padrão do Pydantic. + Se uma URI já foi fornecida como string, ela é retornada diretamente. + Caso contrário, a URI é construída a partir das credenciais individuais. + + Args: + v: Valor atual do campo SQLALCHEMY_DATABASE_URI (pode ser None ou string) + values: Dicionário com os valores dos outros campos já validados + + Returns: + Any: A URI de conexão com o banco de dados no formato PostgresDsn + + Example: + URI gerada: postgresql://postgres:password@localhost/app + """ if isinstance(v, str): return v + + # Constrói a URI PostgreSQL a partir das credenciais individuais return PostgresDsn.build( scheme="postgresql", user=values.get("POSTGRES_USER"), @@ -24,13 +84,20 @@ def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any ) class Config: - case_sensitive = True + """ + Configurações internas do Pydantic BaseSettings. - # If you want to read environment variables from a .env - # file instead un-comment the below line and create the - # .env file at the root of the project. + Attributes: + case_sensitive: Define que os nomes das variáveis de ambiente + são case-sensitive (POSTGRES_SERVER != postgres_server) + """ + + case_sensitive = True + # Se você deseja ler variáveis de ambiente de um arquivo .env, + # descomente a linha abaixo e crie o arquivo .env na raiz do projeto. # env_file = ".env" -settings = Settings() +# Instância global das configurações, carregada automaticamente das variáveis de ambiente +settings: Settings = Settings() diff --git a/app/db/__init__.py b/app/db/__init__.py index ad4f193..8e0e8c2 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -1,5 +1,25 @@ -from .base import Base # noqa +""" +Pacote de configuração do banco de dados. -# Import all the models, so that Base has -# them before being imported by Alembic. -from .. import models # noqa +Este pacote contém os módulos necessários para configuração e conexão +com o banco de dados PostgreSQL usando SQLAlchemy. + +Modules: + base: Define a classe Base declarativa do SQLAlchemy + session: Configura o engine e SessionLocal para conexões + +A importação de Base e models neste __init__.py é necessária para que +o Alembic possa detectar todos os modelos ao gerar migrações automáticas. + +Example: + >>> from app.db import Base + >>> from app.db.session import SessionLocal +""" + +# Exporta Base para uso em outros módulos +from .base import Base # noqa: F401 + +# Importa todos os modelos para que Base tenha conhecimento deles +# antes de ser importado pelo Alembic para geração de migrações. +# Isso garante que todas as tabelas sejam detectadas automaticamente. +from .. import models # noqa: F401 diff --git a/app/db/base.py b/app/db/base.py index 4cf0180..ec69575 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -1,18 +1,79 @@ +""" +Módulo base para modelos SQLAlchemy. + +Este módulo define a classe Base declarativa que serve como classe pai +para todos os modelos SQLAlchemy da aplicação. A classe Base fornece +funcionalidades comuns como geração automática de nomes de tabelas. + +A geração automática de nomes de tabelas converte o nome da classe +para minúsculas e pluraliza usando a biblioteca inflect. + +Example: + >>> from app.db.base import Base + >>> class Post(Base): + ... # A tabela será automaticamente nomeada 'posts' + ... pass +""" + from typing import Any import inflect from sqlalchemy.ext.declarative import as_declarative, declared_attr -p = inflect.engine() +# Engine do inflect para pluralização de palavras em inglês +# Usado para gerar nomes de tabelas automaticamente no plural +p: inflect.engine = inflect.engine() @as_declarative() class Base: + """ + Classe base declarativa para todos os modelos SQLAlchemy. + + Esta classe utiliza o decorator @as_declarative() do SQLAlchemy + para criar uma classe base que pode ser herdada por todos os modelos. + Fornece geração automática de nomes de tabelas no plural. + + Attributes: + id: Campo de identificação (deve ser definido nas subclasses) + __name__: Nome da classe (usado para gerar o nome da tabela) + + Example: + >>> class User(Base): + ... id = Column(Integer, primary_key=True) + ... name = Column(String) + >>> # A tabela será automaticamente nomeada 'users' + + Note: + O nome da tabela é gerado automaticamente como o plural do + nome da classe em minúsculas. Por exemplo: + - Post -> posts + - User -> users + - Category -> categories + """ + + # Campo de identificação - deve ser sobrescrito nas subclasses id: Any + + # Nome da classe - usado internamente pelo SQLAlchemy __name__: str - # Generate __tablename__ automatically in plural form. - # i.e 'Post' model will generate table name 'posts' @declared_attr def __tablename__(cls) -> str: + """ + Gera automaticamente o nome da tabela no banco de dados. + + O nome é derivado do nome da classe, convertido para minúsculas + e pluralizado usando a biblioteca inflect. + + Returns: + str: Nome da tabela no plural (ex: 'Post' -> 'posts') + + Example: + >>> class Post(Base): + ... pass + >>> Post.__tablename__ + 'posts' + """ + # Converte o nome da classe para minúsculas e pluraliza return p.plural(cls.__name__.lower()) diff --git a/app/db/session.py b/app/db/session.py index d37db5d..70f0d8b 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,7 +1,42 @@ +""" +Módulo de configuração de sessão do banco de dados. + +Este módulo configura o engine SQLAlchemy e a factory de sessões +para conexão com o banco de dados PostgreSQL. + +O engine é configurado com pool_pre_ping=True para verificar a +conexão antes de usá-la, evitando erros de conexões obsoletas. + +Example: + >>> from app.db.session import SessionLocal + >>> db = SessionLocal() + >>> try: + ... # usar db para queries + ... pass + ... finally: + ... db.close() +""" + from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session, sessionmaker from ..config import settings -engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# Engine SQLAlchemy para conexão com o banco de dados PostgreSQL +# pool_pre_ping=True: Verifica se a conexão está ativa antes de usar, +# evitando erros com conexões que foram fechadas pelo servidor +engine: Engine = create_engine( + settings.SQLALCHEMY_DATABASE_URI, + pool_pre_ping=True +) + +# Factory de sessões do SQLAlchemy +# autocommit=False: Transações devem ser commitadas explicitamente +# autoflush=False: Mudanças não são enviadas ao banco automaticamente +# bind=engine: Associa as sessões ao engine configurado +SessionLocal: sessionmaker[Session] = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine +) diff --git a/app/main.py b/app/main.py index da4f145..8178e47 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,36 @@ -from typing import Any, Generator, List +""" +Módulo principal da aplicação FastAPI. + +Este módulo define a aplicação FastAPI e todos os endpoints da API REST. +A aplicação fornece uma API CRUD completa para gerenciamento de posts. + +Endpoints disponíveis: + - GET /: Endpoint de health check / boas-vindas + - GET /posts: Lista todos os posts com paginação + - POST /posts: Cria um novo post + - GET /posts/{id}: Recupera um post específico por ID + - PUT /posts/{id}: Atualiza um post existente + - DELETE /posts/{id}: Remove um post + +A aplicação utiliza: + - FastAPI para o framework web e documentação automática + - SQLAlchemy para persistência de dados + - Pydantic para validação de entrada/saída + - Dependency Injection para gerenciamento de sessões do banco + +Example: + Para executar a aplicação: + + ```bash + uvicorn app.main:app --reload + ``` + + A documentação interativa estará disponível em: + - Swagger UI: http://localhost:8000/docs + - ReDoc: http://localhost:8000/redoc +""" + +from typing import Dict, Generator, List from fastapi import Depends, FastAPI, HTTPException from pydantic import UUID4 @@ -8,37 +40,136 @@ from . import actions, schemas from .db.session import SessionLocal -# Create all tables in database. -# Comment this out if you using migrations. +# Nota: Para criar tabelas automaticamente sem usar migrações Alembic, +# descomente a linha abaixo. Recomendado apenas para desenvolvimento. +# from .db.session import engine +# from . import models # models.Base.metadata.create_all(bind=engine) -app = FastAPI() +# Instância principal da aplicação FastAPI +app: FastAPI = FastAPI( + title="FastAPI Simple App", + description="Uma API REST simples para gerenciamento de posts", + version="0.1.0", +) + + +def get_db() -> Generator[Session, None, None]: + """ + Dependency que fornece uma sessão do banco de dados. + + Esta função é um generator que cria uma nova sessão do SQLAlchemy + para cada requisição e garante que ela seja fechada após o uso, + mesmo em caso de exceções. + + Yields: + Session: Uma sessão do SQLAlchemy para interação com o banco de dados + Example: + >>> @app.get("/example") + >>> def example_endpoint(db: Session = Depends(get_db)): + ... # db está disponível aqui + ... pass -# Dependency to get DB session. -def get_db() -> Generator: + Note: + O bloco finally garante que a sessão seja sempre fechada, + liberando a conexão de volta para o pool. + """ try: + # Cria uma nova sessão do banco de dados db = SessionLocal() yield db finally: + # Garante que a sessão seja fechada após o uso db.close() @app.get("/") -def index() -> Any: +def index() -> Dict[str, str]: + """ + Endpoint raiz / health check. + + Retorna uma mensagem simples de boas-vindas. Útil para verificar + se a API está funcionando corretamente. + + Returns: + Dict[str, str]: Dicionário com mensagem de boas-vindas + + Example: + >>> response = client.get("/") + >>> response.json() + {'message': 'Hello world!'} + """ return {"message": "Hello world!"} @app.get("/posts", response_model=List[schemas.Post], tags=["posts"]) -def list_posts(db: Session = Depends(get_db), skip: int = 0, limit: int = 100) -> Any: +def list_posts( + db: Session = Depends(get_db), + skip: int = 0, + limit: int = 100 +) -> List[schemas.Post]: + """ + Lista todos os posts com suporte a paginação. + + Recupera uma lista de posts do banco de dados, com opções + de paginação através dos parâmetros skip e limit. + + Args: + db: Sessão do banco de dados (injetada automaticamente) + skip: Número de registros a pular (offset). Default: 0 + limit: Número máximo de registros a retornar. Default: 100 + + Returns: + List[schemas.Post]: Lista de posts encontrados + + Example: + GET /posts?skip=0&limit=10 + + Response: + [ + {"id": "uuid", "title": "Post 1", "body": "Conteúdo..."}, + {"id": "uuid", "title": "Post 2", "body": "Conteúdo..."} + ] + """ posts = actions.post.get_all(db=db, skip=skip, limit=limit) return posts @app.post( - "/posts", response_model=schemas.Post, status_code=HTTP_201_CREATED, tags=["posts"] + "/posts", + response_model=schemas.Post, + status_code=HTTP_201_CREATED, + tags=["posts"] ) -def create_post(*, db: Session = Depends(get_db), post_in: schemas.PostCreate) -> Any: +def create_post( + *, + db: Session = Depends(get_db), + post_in: schemas.PostCreate +) -> schemas.Post: + """ + Cria um novo post. + + Recebe os dados do post no corpo da requisição e cria um novo + registro no banco de dados. O ID é gerado automaticamente como UUID. + + Args: + db: Sessão do banco de dados (injetada automaticamente) + post_in: Dados do post a ser criado (título e corpo obrigatórios) + + Returns: + schemas.Post: O post criado com seu ID gerado + + Raises: + HTTPException 422: Se os dados de entrada forem inválidos + + Example: + POST /posts + Body: {"title": "Meu Post", "body": "Conteúdo do post"} + + Response (201 Created): + {"id": "uuid-gerado", "title": "Meu Post", "body": "Conteúdo do post"} + """ post = actions.post.create(db=db, obj_in=post_in) return post @@ -50,11 +181,42 @@ def create_post(*, db: Session = Depends(get_db), post_in: schemas.PostCreate) - tags=["posts"], ) def update_post( - *, db: Session = Depends(get_db), id: UUID4, post_in: schemas.PostUpdate, -) -> Any: + *, + db: Session = Depends(get_db), + id: UUID4, + post_in: schemas.PostUpdate, +) -> schemas.Post: + """ + Atualiza um post existente. + + Atualiza os campos de um post identificado pelo ID. Suporta + atualização parcial - apenas os campos fornecidos serão atualizados. + + Args: + db: Sessão do banco de dados (injetada automaticamente) + id: UUID do post a ser atualizado + post_in: Dados para atualização (campos opcionais) + + Returns: + schemas.Post: O post atualizado + + Raises: + HTTPException 404: Se o post não for encontrado + HTTPException 422: Se os dados de entrada forem inválidos + + Example: + PUT /posts/123e4567-e89b-12d3-a456-426614174000 + Body: {"title": "Título Atualizado"} + + Response (200 OK): + {"id": "123e4567-...", "title": "Título Atualizado", "body": "..."} + """ + # Verifica se o post existe antes de tentar atualizar post = actions.post.get(db=db, id=id) if not post: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Post not found") + + # Atualiza o post com os novos dados post = actions.post.update(db=db, db_obj=post, obj_in=post_in) return post @@ -65,7 +227,28 @@ def update_post( responses={HTTP_404_NOT_FOUND: {"model": schemas.HTTPError}}, tags=["posts"], ) -def get_post(*, db: Session = Depends(get_db), id: UUID4) -> Any: +def get_post(*, db: Session = Depends(get_db), id: UUID4) -> schemas.Post: + """ + Recupera um post específico pelo ID. + + Busca e retorna um único post identificado pelo seu UUID. + + Args: + db: Sessão do banco de dados (injetada automaticamente) + id: UUID do post a ser recuperado + + Returns: + schemas.Post: O post encontrado + + Raises: + HTTPException 404: Se o post não for encontrado + + Example: + GET /posts/123e4567-e89b-12d3-a456-426614174000 + + Response (200 OK): + {"id": "123e4567-...", "title": "Meu Post", "body": "Conteúdo..."} + """ post = actions.post.get(db=db, id=id) if not post: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Post not found") @@ -78,9 +261,38 @@ def get_post(*, db: Session = Depends(get_db), id: UUID4) -> Any: responses={HTTP_404_NOT_FOUND: {"model": schemas.HTTPError}}, tags=["posts"], ) -def delete_post(*, db: Session = Depends(get_db), id: UUID4) -> Any: +def delete_post(*, db: Session = Depends(get_db), id: UUID4) -> schemas.Post: + """ + Remove um post pelo ID. + + Deleta permanentemente um post do banco de dados. + Retorna os dados do post removido para confirmação. + + Args: + db: Sessão do banco de dados (injetada automaticamente) + id: UUID do post a ser removido + + Returns: + schemas.Post: O post que foi removido + + Raises: + HTTPException 404: Se o post não for encontrado + + Example: + DELETE /posts/123e4567-e89b-12d3-a456-426614174000 + + Response (200 OK): + {"id": "123e4567-...", "title": "Post Removido", "body": "..."} + + Warning: + Esta operação é irreversível. O post será permanentemente + removido do banco de dados. + """ + # Verifica se o post existe antes de tentar remover post = actions.post.get(db=db, id=id) if not post: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Post not found") + + # Remove o post do banco de dados post = actions.post.remove(db=db, id=id) return post diff --git a/app/models.py b/app/models.py index e074d71..9dd3b86 100644 --- a/app/models.py +++ b/app/models.py @@ -1,3 +1,18 @@ +""" +Módulo de modelos SQLAlchemy. + +Este módulo define os modelos de banco de dados utilizando SQLAlchemy ORM. +Cada classe representa uma tabela no banco de dados PostgreSQL. + +Os modelos herdam de Base, que fornece funcionalidades comuns como +geração automática de nomes de tabelas no plural. + +Example: + >>> from app.models import Post + >>> post = Post(title="Meu Post", body="Conteúdo do post") +""" + +from uuid import UUID as PyUUID from uuid import uuid4 from sqlalchemy import Column, String, Text @@ -7,6 +22,37 @@ class Post(Base): - id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid4) - title = Column(String) - body = Column(Text) + """ + Modelo SQLAlchemy para a tabela de posts. + + Esta classe representa um post no blog/aplicação. O nome da tabela + é gerado automaticamente como 'posts' (plural de 'post') pela classe Base. + + Attributes: + id (UUID): Identificador único do post, gerado automaticamente como UUID v4. + É a chave primária da tabela e possui índice para buscas rápidas. + title (str): Título do post. Campo de texto curto (VARCHAR). + body (str): Corpo/conteúdo do post. Campo de texto longo (TEXT). + + Example: + >>> from app.models import Post + >>> from uuid import uuid4 + >>> post = Post( + ... id=uuid4(), + ... title="Introdução ao FastAPI", + ... body="FastAPI é um framework moderno para construção de APIs..." + ... ) + + Note: + O campo 'id' é gerado automaticamente se não fornecido, + utilizando uuid4() como valor padrão. + """ + + # Identificador único do post (UUID v4), chave primária com índice + id: PyUUID = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid4) + + # Título do post - campo de texto curto (VARCHAR) + title: str = Column(String) + + # Corpo/conteúdo do post - campo de texto longo (TEXT) + body: str = Column(Text) diff --git a/app/schemas.py b/app/schemas.py index 6af22e5..0e765e8 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,41 +1,160 @@ +""" +Módulo de schemas Pydantic. + +Este módulo define os schemas Pydantic utilizados para validação de dados +de entrada e saída da API. Os schemas seguem o padrão de herança para +reutilização de campos comuns. + +A hierarquia de schemas para Post é: + - PostBase: Campos compartilhados (base) + - PostCreate: Campos para criação (herda de PostBase) + - PostUpdate: Campos para atualização (herda de PostBase) + - PostInDBBase: Campos base para dados do banco (herda de PostBase) + - Post: Schema de resposta da API (herda de PostInDBBase) + - PostInDB: Schema interno do banco de dados (herda de PostInDBBase) + +Example: + >>> from app.schemas import PostCreate, Post + >>> post_data = PostCreate(title="Meu Post", body="Conteúdo") + >>> post_data.dict() + {'title': 'Meu Post', 'body': 'Conteúdo'} +""" + from typing import Optional from pydantic import UUID4, BaseModel class HTTPError(BaseModel): + """ + Schema para respostas de erro HTTP. + + Utilizado para padronizar as respostas de erro da API, + seguindo o formato esperado pelo FastAPI para documentação OpenAPI. + + Attributes: + detail: Mensagem descritiva do erro ocorrido. + + Example: + >>> error = HTTPError(detail="Post not found") + >>> error.dict() + {'detail': 'Post not found'} + """ + detail: str -# Shared properties class PostBase(BaseModel): + """ + Schema base com propriedades compartilhadas de Post. + + Esta classe serve como base para outros schemas de Post, + definindo os campos comuns que podem ser opcionais em + diferentes contextos (criação, atualização, etc.). + + Attributes: + title: Título do post (opcional na base). + body: Corpo/conteúdo do post (opcional na base). + """ + title: Optional[str] = None body: Optional[str] = None -# Properties to receive via API on creation class PostCreate(PostBase): + """ + Schema para criação de um novo Post. + + Herda de PostBase mas torna os campos obrigatórios, + pois um post deve ter título e corpo ao ser criado. + + Attributes: + title: Título do post (obrigatório). + body: Corpo/conteúdo do post (obrigatório). + + Example: + >>> post = PostCreate(title="Novo Post", body="Conteúdo do post") + >>> post.title + 'Novo Post' + """ + title: str body: str -# Properties to receive via API on update class PostUpdate(PostBase): + """ + Schema para atualização de um Post existente. + + Herda de PostBase mantendo os campos opcionais, + permitindo atualização parcial (PATCH-like behavior). + Apenas os campos fornecidos serão atualizados. + + Example: + >>> update = PostUpdate(title="Título Atualizado") + >>> update.dict(exclude_unset=True) + {'title': 'Título Atualizado'} + """ + pass class PostInDBBase(PostBase): + """ + Schema base para Post armazenado no banco de dados. + + Adiciona o campo 'id' que é gerado pelo banco de dados. + Configura orm_mode para permitir conversão automática + de objetos SQLAlchemy para Pydantic. + + Attributes: + id: Identificador único UUID do post (gerado pelo banco). + title: Título do post (herdado de PostBase). + body: Corpo do post (herdado de PostBase). + """ + id: Optional[UUID4] = None class Config: + """ + Configuração do Pydantic para integração com ORM. + + Attributes: + orm_mode: Habilita leitura de dados de objetos ORM, + permitindo que Post.from_orm(db_post) funcione. + """ + orm_mode = True -# Additional properties to return via API class Post(PostInDBBase): + """ + Schema de resposta da API para Post. + + Utilizado como response_model nos endpoints da API. + Herda todos os campos de PostInDBBase sem modificações. + + Este schema representa os dados que serão retornados + ao cliente nas respostas da API. + + Example: + >>> from uuid import uuid4 + >>> post = Post(id=uuid4(), title="Post", body="Conteúdo") + """ + pass -# Additional properties stored in DB class PostInDB(PostInDBBase): + """ + Schema interno para Post no banco de dados. + + Utilizado internamente para representar dados completos + do banco de dados. Pode incluir campos adicionais que + não devem ser expostos na API (como senhas hash, etc.). + + Atualmente idêntico a Post, mas separado para permitir + extensões futuras sem afetar a API pública. + """ + pass diff --git a/migrations/env.py b/migrations/env.py index c92e865..e164659 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,55 +1,103 @@ +""" +Módulo de configuração do ambiente Alembic para migrações. + +Este módulo é executado pelo Alembic quando migrações são aplicadas. +Ele configura a conexão com o banco de dados e executa as migrações +tanto em modo offline quanto online. + +O modo offline gera scripts SQL sem conectar ao banco de dados, +enquanto o modo online aplica as migrações diretamente no banco. + +Usage: + Para gerar uma nova migração: + ```bash + alembic revision --autogenerate -m "descrição da migração" + ``` + + Para aplicar migrações: + ```bash + alembic upgrade head + ``` + + Para reverter migrações: + ```bash + alembic downgrade -1 + ``` +""" + from logging.config import fileConfig +from typing import Optional -from sqlalchemy import engine_from_config -from sqlalchemy import pool +from sqlalchemy import engine_from_config, pool +from sqlalchemy.engine import Engine from alembic import context +from alembic.config import Config -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config +# Objeto de configuração do Alembic que fornece acesso aos valores +# definidos no arquivo alembic.ini +config: Config = context.config -# Interpret the config file for Python logging. -# This line sets up loggers basically. +# Configura os loggers do Python usando o arquivo de configuração do Alembic +# Isso permite que mensagens de log sejam exibidas durante as migrações fileConfig(config.config_file_name) -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata +# Importa Base para obter os metadados de todos os modelos registrados +# Isso é necessário para o suporte a 'autogenerate' do Alembic +from app.db import Base # noqa: E402 -from app.db import Base # noqa +# Metadados dos modelos SQLAlchemy para geração automática de migrações +# O Alembic compara esses metadados com o estado atual do banco de dados target_metadata = Base.metadata -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. +def get_url() -> str: + """ + Constrói a URL de conexão com o banco de dados PostgreSQL. + + Obtém as credenciais das configurações da aplicação e monta + a string de conexão no formato esperado pelo SQLAlchemy. + + Returns: + str: URL de conexão no formato postgresql://user:password@server/db -def get_url(): + Example: + >>> url = get_url() + >>> print(url) + 'postgresql://postgres:password@localhost/app' + """ from app.config import settings - user = settings.POSTGRES_USER - password = settings.POSTGRES_PASSWORD - server = settings.POSTGRES_SERVER - db = settings.POSTGRES_DB + # Extrai as credenciais das configurações + user: str = settings.POSTGRES_USER + password: str = settings.POSTGRES_PASSWORD + server: str = settings.POSTGRES_SERVER + db: str = settings.POSTGRES_DB + + # Monta e retorna a URL de conexão return f"postgresql://{user}:{password}@{server}/{db}" -def run_migrations_offline(): - """Run migrations in 'offline' mode. +def run_migrations_offline() -> None: + """ + Executa migrações em modo 'offline'. - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. + No modo offline, o Alembic gera scripts SQL sem conectar ao banco + de dados. Isso é útil para: + - Gerar scripts SQL para revisão antes da aplicação + - Ambientes onde não há acesso direto ao banco de dados + - Geração de scripts para aplicação manual por DBAs - Calls to context.execute() here emit the given string to the - script output. + O contexto é configurado apenas com a URL, sem criar um Engine. + As chamadas a context.execute() emitem SQL para a saída do script. + Note: + literal_binds=True faz com que os valores sejam renderizados + diretamente no SQL ao invés de usar placeholders. """ - url = get_url() + url: str = get_url() + + # Configura o contexto do Alembic para modo offline context.configure( url=url, target_metadata=target_metadata, @@ -57,35 +105,62 @@ def run_migrations_offline(): dialect_opts={"paramstyle": "named"}, ) + # Executa as migrações dentro de uma transação with context.begin_transaction(): context.run_migrations() -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - +def run_migrations_online() -> None: + """ + Executa migrações em modo 'online'. + + No modo online, o Alembic cria uma conexão real com o banco de dados + e aplica as migrações diretamente. Este é o modo padrão e mais comum + de executar migrações. + + O processo: + 1. Obtém a configuração da seção do alembic.ini + 2. Substitui a URL do banco de dados pela URL dinâmica + 3. Cria um Engine com pool NullPool (sem pooling de conexões) + 4. Conecta ao banco e executa as migrações em uma transação + + Note: + NullPool é usado para garantir que a conexão seja fechada + imediatamente após o uso, evitando problemas com conexões + pendentes após a migração. """ - configuration = config.get_section(config.config_ini_section) + # Obtém a configuração do arquivo alembic.ini + configuration: Optional[dict] = config.get_section(config.config_ini_section) + + # Substitui a URL estática pela URL dinâmica das configurações configuration["sqlalchemy.url"] = get_url() - connectable = engine_from_config( + + # Cria o Engine a partir da configuração + # NullPool garante que conexões não fiquem abertas após a migração + connectable: Engine = engine_from_config( configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, ) + # Conecta ao banco e executa as migrações with connectable.connect() as connection: + # Configura o contexto com a conexão ativa context.configure( - connection=connection, target_metadata=target_metadata + connection=connection, + target_metadata=target_metadata ) + # Executa as migrações dentro de uma transação with context.begin_transaction(): context.run_migrations() +# Ponto de entrada: determina qual modo de migração usar +# O modo é determinado pela flag --sql na linha de comando do Alembic if context.is_offline_mode(): + # Modo offline: gera scripts SQL sem conectar ao banco run_migrations_offline() else: + # Modo online: aplica migrações diretamente no banco run_migrations_online() diff --git a/tests/test_app.py b/tests/test_app.py index eb03066..56ea555 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,40 @@ +""" +Módulo de testes da aplicação. + +Este módulo contém os testes unitários e de integração para a aplicação +FastAPI. Os testes são executados usando pytest. + +Para executar os testes: + ```bash + pytest tests/ + ``` + +Para executar com cobertura de código: + ```bash + pytest tests/ --cov=app + ``` + +Example: + >>> pytest tests/test_app.py -v + tests/test_app.py::test_version PASSED +""" + from app import __version__ -def test_version(): +def test_version() -> None: + """ + Testa se a versão da aplicação está correta. + + Verifica se a variável __version__ do pacote app corresponde + à versão esperada (0.1.0). Este teste garante que a versão + seja atualizada corretamente quando necessário. + + Raises: + AssertionError: Se a versão não corresponder a '0.1.0' + + Example: + >>> from app import __version__ + >>> assert __version__ == '0.1.0' + """ assert __version__ == '0.1.0'