Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
# API Configuration
PYTHONPATH=/server

# Add any additional environment variables your application might need
# DATABASE_URL=postgresql://user:password@localhost:5432/pynews
# DEBUG=false
# LOG_LEVEL=info
# SQLite Database Configuration
SQLITE_PATH=/app/data/pynewsdb.db
SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db

# Authentication Configuration
SECRET_KEY=1...
ENCRYPTION_KEY=r0...
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=20
ADMIN_USER=admin
ADMIN_PASSWORD=admin
ADMIN_EMAIL=ADMIN_USER@mail.com
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,4 @@ __marimo__/

# SQLiteDB
pynewsdb.db
data/
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ RUN poetry install --no-root --no-interaction
WORKDIR $PROJECT_PATH
COPY app app
COPY tests tests
COPY scripts scripts

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--lifespan", "on"]
57 changes: 57 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,45 @@ health: ## Verifica o health check da API
@echo "$(YELLOW)Verificando saúde da API...$(NC)"
curl -f http://localhost:8000/api/healthcheck || echo "API não está respondendo"

db-backup: ## Cria backup do banco SQLite
@echo "$(YELLOW)Criando backup do banco...$(NC)"
@if [ -f "./data/pynewsdb.db" ]; then \
cp ./data/pynewsdb.db ./data/pynewsdb.db.backup-$(shell date +%Y%m%d_%H%M%S); \
echo "$(GREEN)Backup criado com sucesso!$(NC)"; \
else \
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
fi

db-restore: ## Restaura backup do banco SQLite (usar: make db-restore BACKUP=filename)
@echo "$(YELLOW)Restaurando backup do banco...$(NC)"
@if [ -z "$(BACKUP)" ]; then \
echo "Use: make db-restore BACKUP=filename"; \
exit 1; \
fi
@if [ -f "./data/$(BACKUP)" ]; then \
cp ./data/$(BACKUP) ./data/pynewsdb.db; \
echo "$(GREEN)Backup restaurado com sucesso!$(NC)"; \
else \
echo "Arquivo de backup não encontrado: ./data/$(BACKUP)"; \
fi

db-reset: ## Remove o banco SQLite (será recriado na próxima execução)
@echo "$(YELLOW)Removendo banco de dados...$(NC)"
@if [ -f "./data/pynewsdb.db" ]; then \
rm ./data/pynewsdb.db; \
echo "$(GREEN)Banco removido. Será recriado na próxima execução.$(NC)"; \
else \
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
fi

db-shell: ## Abre shell SQLite para interagir com o banco
@echo "$(YELLOW)Abrindo shell SQLite...$(NC)"
@if [ -f "./data/pynewsdb.db" ]; then \
sqlite3 ./data/pynewsdb.db; \
else \
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
fi

install: ## Instala dependências com Poetry
@echo "$(YELLOW)Instalando dependências...$(NC)"
poetry install
Expand All @@ -78,3 +117,21 @@ setup: install build up ## Setup completo do projeto

docker/test:
docker exec -e PYTHONPATH=/app $(API_CONTAINER_NAME) pytest -s --cov-report=term-missing --cov-report html --cov-report=xml --cov=app tests/

create-community: ## Cria uma nova comunidade (usar: make create-community NAME=nome EMAIL=email PASSWORD=senha)
@echo "$(YELLOW)Criando nova comunidade...$(NC)"
@if [ -z "$(NAME)" ] || [ -z "$(EMAIL)" ] || [ -z "$(PASSWORD)" ]; then \
echo "Use: make create-community NAME=nome EMAIL=email PASSWORD=senha"; \
exit 1; \
fi
docker exec $(API_CONTAINER_NAME) python scripts/create_community.py "$(NAME)" "$(EMAIL)" "$(PASSWORD)"
@echo "$(GREEN)Comunidade criada com sucesso!$(NC)"

exec-script: ## Executa um script dentro do container (usar: make exec-script SCRIPT=caminho/script.py ARGS="arg1 arg2")
@echo "$(YELLOW)Executando script no container...$(NC)"
@if [ -z "$(SCRIPT)" ]; then \
echo "Use: make exec-script SCRIPT=caminho/script.py ARGS=\"arg1 arg2\""; \
exit 1; \
fi
docker exec $(API_CONTAINER_NAME) python $(SCRIPT) $(ARGS)
@echo "$(GREEN)Script executado!$(NC)"
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ sequenceDiagram
- Documentação Swagger: http://localhost:8000/docs
- Health Check: http://localhost:8000/api/healthcheck

### 🗄️ Banco de Dados SQLite

O projeto utiliza SQLite como banco de dados com as seguintes características:
- **Persistência**: Dados armazenados em `./data/pynewsdb.db`
- **Async Support**: Utiliza `aiosqlite` para operações assíncronas
- **ORM**: SQLModel para mapeamento objeto-relacional
- **Auto-inicialização**: Banco e tabelas criados automaticamente na primeira execução

Para mais detalhes sobre configuração do SQLite, consulte: [docs/sqlite-setup.md](docs/sqlite-setup.md)

## 🧩 Configuração Inicial

### ▶️ Guia de Execução para Desenvolvimento
Expand Down
15 changes: 14 additions & 1 deletion app/routers/healthcheck/routes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from fastapi import APIRouter, Request, status
from pydantic import BaseModel

from app.services.database.database import get_session
from app.services.limiter import limiter


class HealthCheckResponse(BaseModel):
status: str = "healthy"
version: str = "2.0.0"
database: str = "connected"


def setup():
Expand All @@ -23,7 +25,18 @@ def setup():
async def healthcheck(request: Request):
"""
Health check endpoint that returns the current status of the API.
Includes database connectivity check.
"""
return HealthCheckResponse()
database_status = "connected"

try:
# Test database connection by getting a session
async for _ in get_session():
# If we can get a session, the database is connected
break
except Exception:
database_status = "disconnected"

return HealthCheckResponse(database=database_status)

return router
69 changes: 55 additions & 14 deletions app/services/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
import os
from typing import AsyncGenerator

from sqlalchemy import text
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel import Field, SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession

from app.services.database import models # noqa F401
from app.services.database.models import ( # noqa F401
Community,
Library,
LibraryRequest,
News,
Subscription,
)

logger = logging.getLogger(__name__)

Expand All @@ -32,7 +40,7 @@
engine,
class_=AsyncSession,
expire_on_commit=False,
#echo=True, # expire_on_commit=False é importante!
# echo=True, # expire_on_commit=False é importante!
)


Expand All @@ -59,24 +67,57 @@ async def init_db():
"""
Inicializa o banco de dados:
1. Verifica se o arquivo do banco de dados existe.
2. Se não existir, cria o arquivo e todas as tabelas definidas
nos modelos SQLModel nos imports e acima.
2. Conecta ao banco e verifica se as tabelas existem.
3. Cria tabelas faltantes se necessário.
"""
if not os.path.exists(DATABASE_FILE):
logger.info(
f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado."
f"Criando novo banco de dados e tabelas."
)
try:
# Cria o diretório do banco se não existir
db_dir = os.path.dirname(DATABASE_FILE)
if db_dir and not os.path.exists(db_dir):
os.makedirs(db_dir, exist_ok=True)
logger.info(f"Diretório criado: {db_dir}")

# Verifica se o arquivo existe
db_exists = os.path.exists(DATABASE_FILE)

if not db_exists:
logger.info(
f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado. "
f"Criando novo banco de dados."
)
else:
logger.info(f"Conectando ao banco de dados '{DATABASE_FILE}'.")

# Sempre tenta criar as tabelas (create_all é idempotente)
# Se as tabelas já existem, o SQLModel não fará nada
async with engine.begin() as conn:
# SQLModel.metadata.create_all é síncrono e precisa
# ser executado via run_sync
await conn.run_sync(SQLModel.metadata.create_all)
logger.info("Tabelas criadas com sucesso.")
else:
logger.info(
f"Arquivo de banco de dados '{DATABASE_FILE}'"
f"já existe. Conectando."
)

# Verifica quais tabelas foram criadas/existem
async with AsyncSessionLocal() as session:
result = await session.execute(
text(
"SELECT name FROM sqlite_master WHERE type='table' "
"ORDER BY name"
)
)
tables = [row[0] for row in result.fetchall()]

if not db_exists:
message = "Banco de dados e tabelas criados com sucesso."
logger.info(message)
else:
message = "Estrutura do banco de dados verificada."
logger.info(message)

tables_message = f"Tabelas disponíveis: {', '.join(tables)}"
logger.info(tables_message)

except Exception as e:
logger.error(f"Erro ao inicializar banco de dados: {e}")
raise


async def get_session() -> AsyncGenerator[AsyncSession, None]:
Expand Down
6 changes: 4 additions & 2 deletions app/services/database/models/communities.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import Optional

from sqlalchemy import Text
from sqlalchemy import Column, String
from sqlmodel import Field, SQLModel


Expand All @@ -10,7 +10,9 @@ class Community(SQLModel, table=True):

id: Optional[int] = Field(default=None, primary_key=True)
username: str
email: str = Field(sa_column=Text) # VARCHAR(255)
email: str = Field(
sa_column=Column("email", String, unique=True)
) # VARCHAR(255), unique key
password: str
created_at: Optional[datetime] = Field(default_factory=datetime.now)
updated_at: Optional[datetime] = Field(
Expand Down
46 changes: 33 additions & 13 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.8'

services:
pynews-api:
build:
Expand All @@ -9,25 +7,47 @@ services:
container_name: pynews-server
ports:
- "8000:8000"
environment:
- PYTHONPATH=/server
- SQLITE_PATH=app/services/database/pynewsdb.db
- SQLITE_URL=sqlite+aiosqlite://
- SECRET_KEY=1a6c5f3b7d2e4a7fb68d0casd3f9a7b2d8c4e5f6a3b0d4e9c7a8f1b6d3c0a7f5e
- ENCRYPTION_KEY=r0-QKv5qACJNFRqy2cNZCsfZ_zVvehlC-v8zDJb--EI=
- ADMIN_USER=admin
- ADMIN_PASSWORD=admin
- ADMIN_EMAIL=ADMIN_USER@mail.com
- ALGORITHM=HS256
- ACCESS_TOKEN_EXPIRE_MINUTES=20
env_file:
- .env
restart: unless-stopped
volumes:
- sqlite_data:/app/data
environment:
- SQLITE_PATH=/app/data/pynewsdb.db
- SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db
depends_on:
- sqlite-init
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/healthcheck"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

sqlite-init:
image: alpine:latest
container_name: pynews-sqlite-init
volumes:
- sqlite_data:/data
command: >
sh -c "
mkdir -p /data &&
touch /data/pynewsdb.db &&
chmod 777 /data &&
chmod 666 /data/pynewsdb.db &&
chown -R root:root /data &&
echo 'SQLite database initialized'
"
restart: "no"

volumes:
sqlite_data:
driver: local
driver_opts:
type: none
o: bind
device: ./data

networks:
default:
name: pynews-network
Loading