Skip to content

Commit a2be9d1

Browse files
authored
Merge pull request #57 from PythonFloripa/feature/DataBasePersistAndUserCreation
Implement SQLite database support and community management scripts
2 parents 3caa3f7 + e6ce5e9 commit a2be9d1

File tree

14 files changed

+733
-34
lines changed

14 files changed

+733
-34
lines changed

.env.example

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
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+
ENCRYPTION_KEY=r0...
13+
ALGORITHM=HS256
14+
ACCESS_TOKEN_EXPIRE_MINUTES=20
15+
ADMIN_USER=admin
16+
ADMIN_PASSWORD=admin
17+
ADMIN_EMAIL=ADMIN_USER@mail.com

.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/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]:

app/services/database/models/communities.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
from typing import Optional
33

4-
from sqlalchemy import Text
4+
from sqlalchemy import Column, String
55
from sqlmodel import Field, SQLModel
66

77

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

1111
id: Optional[int] = Field(default=None, primary_key=True)
1212
username: str
13-
email: str = Field(sa_column=Text) # VARCHAR(255)
13+
email: str = Field(
14+
sa_column=Column("email", String, unique=True)
15+
) # VARCHAR(255), unique key
1416
password: str
1517
created_at: Optional[datetime] = Field(default_factory=datetime.now)
1618
updated_at: Optional[datetime] = Field(

docker-compose.yaml

Lines changed: 33 additions & 13 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,25 +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-
- ENCRYPTION_KEY=r0-QKv5qACJNFRqy2cNZCsfZ_zVvehlC-v8zDJb--EI=
18-
- ADMIN_USER=admin
19-
- ADMIN_PASSWORD=admin
20-
- ADMIN_EMAIL=ADMIN_USER@mail.com
21-
- ALGORITHM=HS256
22-
- ACCESS_TOKEN_EXPIRE_MINUTES=20
10+
env_file:
11+
- .env
2312
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
2420
healthcheck:
2521
test: ["CMD", "curl", "-f", "http://localhost:8000/api/healthcheck"]
2622
interval: 30s
2723
timeout: 10s
2824
retries: 3
2925
start_period: 40s
3026

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+
3151
networks:
3252
default:
3353
name: pynews-network

0 commit comments

Comments
 (0)