Skip to content

Commit 9ddec7d

Browse files
committed
Implement SQLite database support and community management scripts
- Added SQLite configuration to .env.example and Docker setup - Introduced db-backup, db-restore, db-reset, and db-shell commands in Makefile - Enhanced healthcheck endpoint to include database connectivity status - Created scripts for community management: create_community.py, add_community.py, and example_create_community.py - Documented SQLite setup and community scripts in README and new sqlite-setup.md
1 parent e295c0e commit 9ddec7d

File tree

14 files changed

+739
-42
lines changed

14 files changed

+739
-42
lines changed

.env.example

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
# API Configuration
44
PYTHONPATH=/server
55

6-
# Add any additional environment variables your application might need
7-
# DATABASE_URL=postgresql://user:password@localhost:5432/pynews
8-
# DEBUG=false
9-
# LOG_LEVEL=info
6+
# SQLite Database Configuration
7+
SQLITE_PATH=/app/data/pynewsdb.db
8+
SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db
9+
10+
# Authentication Configuration
11+
SECRET_KEY=1...
12+
ALGORITHM=HS256
13+
ACCESS_TOKEN_EXPIRE_MINUTES=20

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,4 @@ __marimo__/
208208

209209
# SQLiteDB
210210
pynewsdb.db
211+
data/

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ RUN poetry install --no-root --no-interaction
5959
WORKDIR $PROJECT_PATH
6060
COPY app app
6161
COPY tests tests
62+
COPY scripts scripts
6263

6364
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--lifespan", "on"]

Makefile

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,45 @@ health: ## Verifica o health check da API
6464
@echo "$(YELLOW)Verificando saúde da API...$(NC)"
6565
curl -f http://localhost:8000/api/healthcheck || echo "API não está respondendo"
6666

67+
db-backup: ## Cria backup do banco SQLite
68+
@echo "$(YELLOW)Criando backup do banco...$(NC)"
69+
@if [ -f "./data/pynewsdb.db" ]; then \
70+
cp ./data/pynewsdb.db ./data/pynewsdb.db.backup-$(shell date +%Y%m%d_%H%M%S); \
71+
echo "$(GREEN)Backup criado com sucesso!$(NC)"; \
72+
else \
73+
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
74+
fi
75+
76+
db-restore: ## Restaura backup do banco SQLite (usar: make db-restore BACKUP=filename)
77+
@echo "$(YELLOW)Restaurando backup do banco...$(NC)"
78+
@if [ -z "$(BACKUP)" ]; then \
79+
echo "Use: make db-restore BACKUP=filename"; \
80+
exit 1; \
81+
fi
82+
@if [ -f "./data/$(BACKUP)" ]; then \
83+
cp ./data/$(BACKUP) ./data/pynewsdb.db; \
84+
echo "$(GREEN)Backup restaurado com sucesso!$(NC)"; \
85+
else \
86+
echo "Arquivo de backup não encontrado: ./data/$(BACKUP)"; \
87+
fi
88+
89+
db-reset: ## Remove o banco SQLite (será recriado na próxima execução)
90+
@echo "$(YELLOW)Removendo banco de dados...$(NC)"
91+
@if [ -f "./data/pynewsdb.db" ]; then \
92+
rm ./data/pynewsdb.db; \
93+
echo "$(GREEN)Banco removido. Será recriado na próxima execução.$(NC)"; \
94+
else \
95+
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
96+
fi
97+
98+
db-shell: ## Abre shell SQLite para interagir com o banco
99+
@echo "$(YELLOW)Abrindo shell SQLite...$(NC)"
100+
@if [ -f "./data/pynewsdb.db" ]; then \
101+
sqlite3 ./data/pynewsdb.db; \
102+
else \
103+
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
104+
fi
105+
67106
install: ## Instala dependências com Poetry
68107
@echo "$(YELLOW)Instalando dependências...$(NC)"
69108
poetry install
@@ -78,3 +117,21 @@ setup: install build up ## Setup completo do projeto
78117

79118
docker/test:
80119
docker exec -e PYTHONPATH=/app $(API_CONTAINER_NAME) pytest -s --cov-report=term-missing --cov-report html --cov-report=xml --cov=app tests/
120+
121+
create-community: ## Cria uma nova comunidade (usar: make create-community NAME=nome EMAIL=email PASSWORD=senha)
122+
@echo "$(YELLOW)Criando nova comunidade...$(NC)"
123+
@if [ -z "$(NAME)" ] || [ -z "$(EMAIL)" ] || [ -z "$(PASSWORD)" ]; then \
124+
echo "Use: make create-community NAME=nome EMAIL=email PASSWORD=senha"; \
125+
exit 1; \
126+
fi
127+
docker exec $(API_CONTAINER_NAME) python scripts/create_community.py "$(NAME)" "$(EMAIL)" "$(PASSWORD)"
128+
@echo "$(GREEN)Comunidade criada com sucesso!$(NC)"
129+
130+
exec-script: ## Executa um script dentro do container (usar: make exec-script SCRIPT=caminho/script.py ARGS="arg1 arg2")
131+
@echo "$(YELLOW)Executando script no container...$(NC)"
132+
@if [ -z "$(SCRIPT)" ]; then \
133+
echo "Use: make exec-script SCRIPT=caminho/script.py ARGS=\"arg1 arg2\""; \
134+
exit 1; \
135+
fi
136+
docker exec $(API_CONTAINER_NAME) python $(SCRIPT) $(ARGS)
137+
@echo "$(GREEN)Script executado!$(NC)"

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ sequenceDiagram
111111
- Documentação Swagger: http://localhost:8000/docs
112112
- Health Check: http://localhost:8000/api/healthcheck
113113

114+
### 🗄️ Banco de Dados SQLite
115+
116+
O projeto utiliza SQLite como banco de dados com as seguintes características:
117+
- **Persistência**: Dados armazenados em `./data/pynewsdb.db`
118+
- **Async Support**: Utiliza `aiosqlite` para operações assíncronas
119+
- **ORM**: SQLModel para mapeamento objeto-relacional
120+
- **Auto-inicialização**: Banco e tabelas criados automaticamente na primeira execução
121+
122+
Para mais detalhes sobre configuração do SQLite, consulte: [docs/sqlite-setup.md](docs/sqlite-setup.md)
123+
114124
## 🧩 Configuração Inicial
115125

116126
### ▶️ Guia de Execução para Desenvolvimento

app/routers/authentication.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,20 @@ async def authenticate_community(
7272

7373
# Teste
7474

75-
@router.post("/create_commumity")
76-
async def create_community(request: Request):
77-
password = "123Asd!@#"
78-
hashed_password = auth.hash_password(password)
79-
community = DBCommunity(
80-
username="username",
81-
email="username@test.com",
82-
password=hashed_password,
83-
)
84-
session: AsyncSession = request.app.db_session_factory
85-
session.add(community)
86-
await session.commit()
87-
await session.refresh(community)
88-
return {"msg": "succes? "}
75+
# @router.post("/create_commumity")
76+
# async def create_community(request: Request):
77+
# password = "123Asd!@#"
78+
# hashed_password = auth.hash_password(password)
79+
# community = DBCommunity(
80+
# username="username",
81+
# email="username@test.com",
82+
# password=hashed_password,
83+
# )
84+
# session: AsyncSession = request.app.db_session_factory
85+
# session.add(community)
86+
# await session.commit()
87+
# await session.refresh(community)
88+
# return {"msg": "succes? "}
8989

9090
# Teste
9191

app/routers/healthcheck/routes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from fastapi import APIRouter, Request, status
22
from pydantic import BaseModel
33

4+
from app.services.database.database import get_session
45
from app.services.limiter import limiter
56

67

78
class HealthCheckResponse(BaseModel):
89
status: str = "healthy"
910
version: str = "2.0.0"
11+
database: str = "connected"
1012

1113

1214
def setup():
@@ -23,7 +25,18 @@ def setup():
2325
async def healthcheck(request: Request):
2426
"""
2527
Health check endpoint that returns the current status of the API.
28+
Includes database connectivity check.
2629
"""
27-
return HealthCheckResponse()
30+
database_status = "connected"
31+
32+
try:
33+
# Test database connection by getting a session
34+
async for _ in get_session():
35+
# If we can get a session, the database is connected
36+
break
37+
except Exception:
38+
database_status = "disconnected"
39+
40+
return HealthCheckResponse(database=database_status)
2841

2942
return router

app/services/database/database.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
import os
33
from typing import AsyncGenerator
44

5+
from sqlalchemy import text
56
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
67
from sqlmodel import Field, SQLModel
78
from sqlmodel.ext.asyncio.session import AsyncSession
89

910
from app.services.database import models # noqa F401
11+
from app.services.database.models import ( # noqa F401
12+
Community,
13+
Library,
14+
LibraryRequest,
15+
News,
16+
Subscription,
17+
)
1018

1119
logger = logging.getLogger(__name__)
1220

@@ -32,7 +40,7 @@
3240
engine,
3341
class_=AsyncSession,
3442
expire_on_commit=False,
35-
#echo=True, # expire_on_commit=False é importante!
43+
# echo=True, # expire_on_commit=False é importante!
3644
)
3745

3846

@@ -59,24 +67,57 @@ async def init_db():
5967
"""
6068
Inicializa o banco de dados:
6169
1. Verifica se o arquivo do banco de dados existe.
62-
2. Se não existir, cria o arquivo e todas as tabelas definidas
63-
nos modelos SQLModel nos imports e acima.
70+
2. Conecta ao banco e verifica se as tabelas existem.
71+
3. Cria tabelas faltantes se necessário.
6472
"""
65-
if not os.path.exists(DATABASE_FILE):
66-
logger.info(
67-
f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado."
68-
f"Criando novo banco de dados e tabelas."
69-
)
73+
try:
74+
# Cria o diretório do banco se não existir
75+
db_dir = os.path.dirname(DATABASE_FILE)
76+
if db_dir and not os.path.exists(db_dir):
77+
os.makedirs(db_dir, exist_ok=True)
78+
logger.info(f"Diretório criado: {db_dir}")
79+
80+
# Verifica se o arquivo existe
81+
db_exists = os.path.exists(DATABASE_FILE)
82+
83+
if not db_exists:
84+
logger.info(
85+
f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado. "
86+
f"Criando novo banco de dados."
87+
)
88+
else:
89+
logger.info(f"Conectando ao banco de dados '{DATABASE_FILE}'.")
90+
91+
# Sempre tenta criar as tabelas (create_all é idempotente)
92+
# Se as tabelas já existem, o SQLModel não fará nada
7093
async with engine.begin() as conn:
7194
# SQLModel.metadata.create_all é síncrono e precisa
7295
# ser executado via run_sync
7396
await conn.run_sync(SQLModel.metadata.create_all)
74-
logger.info("Tabelas criadas com sucesso.")
75-
else:
76-
logger.info(
77-
f"Arquivo de banco de dados '{DATABASE_FILE}'"
78-
f"já existe. Conectando."
79-
)
97+
98+
# Verifica quais tabelas foram criadas/existem
99+
async with AsyncSessionLocal() as session:
100+
result = await session.execute(
101+
text(
102+
"SELECT name FROM sqlite_master WHERE type='table' "
103+
"ORDER BY name"
104+
)
105+
)
106+
tables = [row[0] for row in result.fetchall()]
107+
108+
if not db_exists:
109+
message = "Banco de dados e tabelas criados com sucesso."
110+
logger.info(message)
111+
else:
112+
message = "Estrutura do banco de dados verificada."
113+
logger.info(message)
114+
115+
tables_message = f"Tabelas disponíveis: {', '.join(tables)}"
116+
logger.info(tables_message)
117+
118+
except Exception as e:
119+
logger.error(f"Erro ao inicializar banco de dados: {e}")
120+
raise
80121

81122

82123
async def get_session() -> AsyncGenerator[AsyncSession, None]:

docker-compose.yaml

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: '3.8'
2-
31
services:
42
pynews-api:
53
build:
@@ -9,21 +7,47 @@ services:
97
container_name: pynews-server
108
ports:
119
- "8000:8000"
12-
environment:
13-
- PYTHONPATH=/server
14-
- SQLITE_PATH=app/services/database/pynewsdb.db
15-
- SQLITE_URL=sqlite+aiosqlite://
16-
- SECRET_KEY=1a6c5f3b7d2e4a7fb68d0casd3f9a7b2d8c4e5f6a3b0d4e9c7a8f1b6d3c0a7f5e
17-
- ALGORITHM=HS256
18-
- ACCESS_TOKEN_EXPIRE_MINUTES=20
10+
env_file:
11+
- .env
1912
restart: unless-stopped
13+
volumes:
14+
- sqlite_data:/app/data
15+
environment:
16+
- SQLITE_PATH=/app/data/pynewsdb.db
17+
- SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db
18+
depends_on:
19+
- sqlite-init
2020
healthcheck:
2121
test: ["CMD", "curl", "-f", "http://localhost:8000/api/healthcheck"]
2222
interval: 30s
2323
timeout: 10s
2424
retries: 3
2525
start_period: 40s
2626

27+
sqlite-init:
28+
image: alpine:latest
29+
container_name: pynews-sqlite-init
30+
volumes:
31+
- sqlite_data:/data
32+
command: >
33+
sh -c "
34+
mkdir -p /data &&
35+
touch /data/pynewsdb.db &&
36+
chmod 777 /data &&
37+
chmod 666 /data/pynewsdb.db &&
38+
chown -R root:root /data &&
39+
echo 'SQLite database initialized'
40+
"
41+
restart: "no"
42+
43+
volumes:
44+
sqlite_data:
45+
driver: local
46+
driver_opts:
47+
type: none
48+
o: bind
49+
device: ./data
50+
2751
networks:
2852
default:
2953
name: pynews-network

0 commit comments

Comments
 (0)