Skip to content

Commit e0d6a39

Browse files
committed
Merge branch 'main' into implements_scanapi
2 parents 307ea67 + a2be9d1 commit e0d6a39

File tree

14 files changed

+742
-31
lines changed

14 files changed

+742
-31
lines changed

.env.example

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
PYTHONPATH=/server
2-
BASE_URL=http://localhost:8000
3-
USERNAME_TEST=
4-
PASSWORD_TEST=
2+
3+
# SQLite Database Configuration
4+
SQLITE_PATH=/app/data/pynewsdb.db
5+
SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db
6+
7+
# Authentication Configuration
8+
SECRET_KEY=1...
9+
ENCRYPTION_KEY=r0...
10+
ALGORITHM=HS256
11+
ACCESS_TOKEN_EXPIRE_MINUTES=20
12+
ADMIN_USER=admin
13+
ADMIN_PASSWORD=admin
14+
ADMIN_EMAIL=[email protected]

.gitignore

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

210210
# SQLiteDB
211211
pynewsdb.db
212+
data/

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ 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"]
6465

Makefile

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

72+
db-backup: ## Cria backup do banco SQLite
73+
@echo "$(YELLOW)Criando backup do banco...$(NC)"
74+
@if [ -f "./data/pynewsdb.db" ]; then \
75+
cp ./data/pynewsdb.db ./data/pynewsdb.db.backup-$(shell date +%Y%m%d_%H%M%S); \
76+
echo "$(GREEN)Backup criado com sucesso!$(NC)"; \
77+
else \
78+
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
79+
fi
80+
81+
db-restore: ## Restaura backup do banco SQLite (usar: make db-restore BACKUP=filename)
82+
@echo "$(YELLOW)Restaurando backup do banco...$(NC)"
83+
@if [ -z "$(BACKUP)" ]; then \
84+
echo "Use: make db-restore BACKUP=filename"; \
85+
exit 1; \
86+
fi
87+
@if [ -f "./data/$(BACKUP)" ]; then \
88+
cp ./data/$(BACKUP) ./data/pynewsdb.db; \
89+
echo "$(GREEN)Backup restaurado com sucesso!$(NC)"; \
90+
else \
91+
echo "Arquivo de backup não encontrado: ./data/$(BACKUP)"; \
92+
fi
93+
94+
db-reset: ## Remove o banco SQLite (será recriado na próxima execução)
95+
@echo "$(YELLOW)Removendo banco de dados...$(NC)"
96+
@if [ -f "./data/pynewsdb.db" ]; then \
97+
rm ./data/pynewsdb.db; \
98+
echo "$(GREEN)Banco removido. Será recriado na próxima execução.$(NC)"; \
99+
else \
100+
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
101+
fi
102+
103+
db-shell: ## Abre shell SQLite para interagir com o banco
104+
@echo "$(YELLOW)Abrindo shell SQLite...$(NC)"
105+
@if [ -f "./data/pynewsdb.db" ]; then \
106+
sqlite3 ./data/pynewsdb.db; \
107+
else \
108+
echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \
109+
fi
110+
111+
install: ## Instala dependências com Poetry
112+
@echo "$(YELLOW)Instalando dependências...$(NC)"
113+
poetry install
114+
72115
shell: ## Entra no shell do container
73116
docker-compose exec pynews-api bash
74117

@@ -80,3 +123,25 @@ clean: ## Remove containers, volumes e imagens
80123
setup: install build up ## Setup completo do projeto
81124
@echo "$(GREEN)Setup completo realizado!$(NC)"
82125
@echo "$(GREEN)Acesse: http://localhost:8000/docs$(NC)"
126+
127+
128+
docker/test:
129+
docker exec -e PYTHONPATH=/app $(API_CONTAINER_NAME) pytest -s --cov-report=term-missing --cov-report html --cov-report=xml --cov=app tests/
130+
131+
create-community: ## Cria uma nova comunidade (usar: make create-community NAME=nome EMAIL=email PASSWORD=senha)
132+
@echo "$(YELLOW)Criando nova comunidade...$(NC)"
133+
@if [ -z "$(NAME)" ] || [ -z "$(EMAIL)" ] || [ -z "$(PASSWORD)" ]; then \
134+
echo "Use: make create-community NAME=nome EMAIL=email PASSWORD=senha"; \
135+
exit 1; \
136+
fi
137+
docker exec $(API_CONTAINER_NAME) python scripts/create_community.py "$(NAME)" "$(EMAIL)" "$(PASSWORD)"
138+
@echo "$(GREEN)Comunidade criada com sucesso!$(NC)"
139+
140+
exec-script: ## Executa um script dentro do container (usar: make exec-script SCRIPT=caminho/script.py ARGS="arg1 arg2")
141+
@echo "$(YELLOW)Executando script no container...$(NC)"
142+
@if [ -z "$(SCRIPT)" ]; then \
143+
echo "Use: make exec-script SCRIPT=caminho/script.py ARGS=\"arg1 arg2\""; \
144+
exit 1; \
145+
fi
146+
docker exec $(API_CONTAINER_NAME) python $(SCRIPT) $(ARGS)
147+
@echo "$(GREEN)Script executado!$(NC)"

README.md

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

124+
### 🗄️ Banco de Dados SQLite
125+
126+
O projeto utiliza SQLite como banco de dados com as seguintes características:
127+
- **Persistência**: Dados armazenados em `./data/pynewsdb.db`
128+
- **Async Support**: Utiliza `aiosqlite` para operações assíncronas
129+
- **ORM**: SQLModel para mapeamento objeto-relacional
130+
- **Auto-inicialização**: Banco e tabelas criados automaticamente na primeira execução
131+
132+
Para mais detalhes sobre configuração do SQLite, consulte: [docs/sqlite-setup.md](docs/sqlite-setup.md)
133+
124134
## 🧩 Configuração Inicial
125135

126136
### ▶️ 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 & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,47 @@ services:
99
container_name: pynews-server
1010
ports:
1111
- "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-
21-
- ALGORITHM=HS256
22-
- ACCESS_TOKEN_EXPIRE_MINUTES=20
12+
env_file:
13+
- .env
2314
restart: unless-stopped
15+
volumes:
16+
- sqlite_data:/app/data
17+
environment:
18+
- SQLITE_PATH=/app/data/pynewsdb.db
19+
- SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db
20+
depends_on:
21+
- sqlite-init
2422
healthcheck:
2523
test: ["CMD", "curl", "-f", "http://localhost:8000/api/healthcheck"]
2624
interval: 30s
2725
timeout: 10s
2826
retries: 3
2927
start_period: 40s
3028

29+
sqlite-init:
30+
image: alpine:latest
31+
container_name: pynews-sqlite-init
32+
volumes:
33+
- sqlite_data:/data
34+
command: >
35+
sh -c "
36+
mkdir -p /data &&
37+
touch /data/pynewsdb.db &&
38+
chmod 777 /data &&
39+
chmod 666 /data/pynewsdb.db &&
40+
chown -R root:root /data &&
41+
echo 'SQLite database initialized'
42+
"
43+
restart: "no"
44+
45+
volumes:
46+
sqlite_data:
47+
driver: local
48+
driver_opts:
49+
type: none
50+
o: bind
51+
device: ./data
52+
3153
scanapi-tests:
3254
build:
3355
context: .

0 commit comments

Comments
 (0)