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
34 changes: 34 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Python Application Test

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

permissions:
contents: read

jobs:
test-lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Install dependencies
run: uv sync --frozen

- name: Lint
run: uv run ruff check . --output-format=github

- name: Test with pytest
run: uv run pytest
2 changes: 1 addition & 1 deletion api/.env-example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
MONGO_URI=mongodb://user:password123@localhost:27017/finfrases?authSource=admin

API_KEY=ampyLVT7rSby5SZtFUK7RLiwlsUt5igG3tB0cWhcIB8
API_KEY=CHANGE_ME
4 changes: 2 additions & 2 deletions api/FinFrases/Quotes/Create Quote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ http:
auth:
type: apikey
key: X-API-Key
value: ampyLVT7rSby5SZtFUK7RLiwlsUt5igG3tB0cWhcIB8
value: "{{API_KEY}}"
placement: header

runtime:
scripts:
- type: before-request
code: |-
let random = Math.floor(Math.random() * 100)
let random = Math.floor(Math.random() * 1000)
let categories = [
"GERAL", "INVESTIMENTOS", "POUPANCA", "PSICOLOGIA", "DIVIDENDOS", "EDUCACAO", "EMPREENDEDORISMO", "ACAO", "FIIS"
]
Expand Down
2 changes: 2 additions & 0 deletions api/FinFrases/environments/local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ variables:
value: v1
- name: baseUrl
value: http://localhost:8000/api
- secret: true
name: API_KEY
3 changes: 3 additions & 0 deletions api/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import cache, quote_service, quote_repository, security

__all__ = ["cache", "quote_service", "quote_repository", "security"]
4 changes: 2 additions & 2 deletions api/core/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _normalize_cache_value(self, value):

return str(value)

def cacheable(self, expire: Callable[[], int]):
def cacheable(self, expire: Callable[[], int] | int = 3600):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
Expand Down Expand Up @@ -82,7 +82,7 @@ async def wrapper(*args, **kwargs):
await self.set(
key=cache_key,
value=dumps(serializable_result),
expire=expire(),
expire=expire() if callable(expire) else expire,
)

return result
Expand Down
22 changes: 18 additions & 4 deletions api/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@
from .config import settings
from loguru import logger

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=True)
api_key_header = APIKeyHeader(
name="X-API-Key",
scheme_name="API-Key",
description="Chave de API para autenticação.",
auto_error=False,
)


def verify_api_key(api_key: str = Security(api_key_header)):
def verify_api_key(api_key: str = Security(api_key_header)) -> str:
if not api_key:
logger.warning("Chave de API ausente.")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais ausentes.",
headers={"WWW-Authenticate": "API-Key"},
)

if not secrets.compare_digest(api_key, settings.API_KEY):
logger.warning(f"Chave de API inválida ou ausente. Chave recebida: {api_key}")
logger.warning("Chave de API inválida.")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais inválidas ou ausentes.",
detail="Credenciais inválidas.",
headers={"WWW-Authenticate": "API-Key"},
)
return api_key
Empty file added api/exceptions/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions api/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import enums, quote

__all__ = ["enums", "quote"]
3 changes: 3 additions & 0 deletions api/repositories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import quote_repository

__all__ = ["quote_repository"]
3 changes: 3 additions & 0 deletions api/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import quotes

__all__ = ["quotes"]
12 changes: 10 additions & 2 deletions api/routers/quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ async def get_all_quotes(
),
limit: int = Query(
default=0,
ge=1,
le=100,
description="Número máximo de citações a serem retornadas.",
),
skip: int = Query(
default=0, description="Número de citações a serem ignoradas para paginação."
default=0,
ge=0,
le=1000,
description="Número de citações a serem ignoradas para paginação.",
),
service: QuoteService = Depends(),
repo: QuoteRepository = Depends(),
Expand Down Expand Up @@ -89,7 +94,10 @@ async def get_all_quotes(
)
async def get_random_quote(
size: int = Query(
default=1, description="Número de citações aleatórias a serem retornadas."
default=1,
ge=1,
le=100,
description="Número de citações aleatórias a serem retornadas.",
),
service: QuoteService = Depends(),
repo: QuoteRepository = Depends(),
Expand Down
3 changes: 3 additions & 0 deletions api/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import pagination, quote_schema

__all__ = ["pagination", "quote_schema"]
3 changes: 3 additions & 0 deletions api/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import quote_service

__all__ = ["quote_service"]
3 changes: 3 additions & 0 deletions api/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import test_quote_service

__all__ = ["test_quote_service"]
53 changes: 53 additions & 0 deletions api/tests/test_quote_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from unittest.mock import AsyncMock

import pytest
from models.enums import CategoryQuote
from models.quote import Quote
from repositories.quote_repository import QuoteRepository
from schemas.quote_schema import CreateQuoteRequest
from services.quote_service import QuoteService


@pytest.fixture
def service():
return QuoteService()


@pytest.fixture
def mock_repo():
return AsyncMock(spec=QuoteRepository)


@pytest.fixture
def valid_request_data():
return CreateQuoteRequest(
content="Investir é sobre ter paciência.",
author="Warren Buffett",
tags=[CategoryQuote.INVESTIMENTOS, CategoryQuote.EDUCACAO],
source="Book of Finances",
verified=True,
)


class TestCreateQuote:
@pytest.mark.asyncio
async def test_create_quote_sucess(self, service, mock_repo, valid_request_data):
quote = Quote.model_construct(
content=valid_request_data.content,
author=valid_request_data.author,
tags=valid_request_data.tags,
source=valid_request_data.source,
verified=valid_request_data.verified,
)

mock_repo.create.return_value = quote

result = await service.create_quote(quote=valid_request_data, repo=mock_repo)

assert result.content == valid_request_data.content
assert result.author == valid_request_data.author
assert result.tags == valid_request_data.tags
assert result.source == valid_request_data.source
assert result.verified == valid_request_data.verified

mock_repo.create.assert_called_once_with(valid_request_data)
3 changes: 3 additions & 0 deletions api/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import utils

__all__ = ["utils"]
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ dependencies = [
"loguru>=0.7.3",
"pymongo>=4.16.0",
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"python-dotenv>=1.2.2",
"redis[hiredis]>=7.4.0",
"requests>=2.33.0",
"ruff>=0.15.8",
"streamlit>=1.55.0",
"uvicorn>=0.42.0",
]
83 changes: 83 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
altair==6.0.0
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.13.0
attrs==26.1.0
beanie==2.0.1
blinker==1.9.0
cachetools==7.0.5
certifi==2026.2.25
charset-normalizer==3.4.6
click==8.3.1
dnspython==2.8.0
email-validator==2.3.0
fastapi==0.135.2
fastapi-cli==0.0.24
fastapi-cloud-cli==0.15.0
fastar==0.9.0
gitdb==4.0.12
gitpython==3.1.46
h11==0.16.0
hiredis==3.3.1
httpcore==1.0.9
httptools==0.7.1
httpx==0.28.1
idna==3.11
iniconfig==2.3.0
jinja2==3.1.6
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
lazy-model==0.4.0
loguru==0.7.3
markdown-it-py==4.0.0
markupsafe==3.0.3
mdurl==0.1.2
narwhals==2.18.1
numpy==2.4.3
packaging==26.0
pandas==2.3.3
pillow==12.1.1
pluggy==1.6.0
protobuf==6.33.6
pyarrow==23.0.1
pydantic==2.12.5
pydantic-core==2.41.5
pydantic-extra-types==2.11.1
pydantic-settings==2.13.1
pydeck==0.9.1
pygments==2.19.2
pymongo==4.16.0
pytest==9.0.2
pytest-asyncio==1.3.0
python-dateutil==2.9.0.post0
python-dotenv==1.2.2
python-multipart==0.0.22
pytz==2026.1.post1
pyyaml==6.0.3
redis==7.4.0
referencing==0.37.0
requests==2.33.0
rich==14.3.3
rich-toolkit==0.19.7
rignore==0.7.6
rpds-py==0.30.0
ruff==0.15.8
sentry-sdk==2.56.0
shellingham==1.5.4
six==1.17.0
smmap==5.0.3
starlette==1.0.0
streamlit==1.55.0
tenacity==9.1.4
toml==0.10.2
tornado==6.5.5
typer==0.24.1
typing-extensions==4.15.0
typing-inspection==0.4.2
tzdata==2025.3
urllib3==2.6.3
uvicorn==0.42.0
uvloop==0.22.1
watchdog==6.0.0
watchfiles==1.1.1
websockets==16.0
Loading
Loading