Skip to content
Open
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
69 changes: 68 additions & 1 deletion src/ssdlc_demo/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import FastAPI, Response, status
import requests
from fastapi import FastAPI, HTTPException, Response, status
from pydantic import BaseModel

from .logging_config import configure_logging
Expand All @@ -16,6 +17,28 @@ class EchoResponse(BaseModel):
app = FastAPI(title="SSDLCDemo", version="0.1.0")


class AnimeInfo(BaseModel):
id: int
name: str
altName: str | None = None


class CharacterInfo(BaseModel):
id: int
name: str


class AnimechanData(BaseModel):
content: str
anime: AnimeInfo
character: CharacterInfo


class AnimechanResponse(BaseModel):
status: str
data: AnimechanData


@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
Expand All @@ -25,3 +48,47 @@ def health() -> dict[str, str]:
def echo(payload: EchoRequest, response: Response) -> EchoResponse:
# Any validation is handled by Pydantic models
return EchoResponse(message=payload.message)


@app.get("/anime/quote", response_model=AnimechanResponse)
def get_random_anime_quote() -> AnimechanResponse:
"""Fetch a random anime quote from Animechan and return it.

Docs: https://animechan.io/
Endpoint used: https://api.animechan.io/v1/quotes/random
"""
try:
res = requests.get(
"https://api.animechan.io/v1/quotes/random",
timeout=5,
)
except requests.RequestException as exc: # network, DNS, timeout
raise HTTPException(
status_code=502,
detail="Upstream Animechan unavailable",
) from exc

if res.status_code != 200:
raise HTTPException(
status_code=502,
detail="Animechan returned non-200 status",
)

try:
payload = res.json()
except ValueError as exc:
raise HTTPException(
status_code=502,
detail="Invalid JSON from Animechan",
) from exc

# Validate and coerce to our response model
try:
validated = AnimechanResponse.model_validate(payload)
except Exception as exc:
raise HTTPException(
status_code=502,
detail="Unexpected Animechan schema",
) from exc

return validated
72 changes: 72 additions & 0 deletions tests/test_anime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from fastapi.testclient import TestClient

from ssdlc_demo.main import app


def test_anime_quote_success(monkeypatch) -> None:
class DummyResponse:
status_code = 200

def json(self):
return {
"status": "success",
"data": {
"content": "Test quote",
"anime": {"id": 1, "name": "Naruto", "altName": "NARUTO"},
"character": {"id": 2, "name": "Naruto Uzumaki"},
},
}

def fake_get(url, timeout=5): # type: ignore[no-untyped-def]
assert url == "https://api.animechan.io/v1/quotes/random"
assert timeout == 5
return DummyResponse()

import ssdlc_demo.main as main_mod

monkeypatch.setattr(main_mod.requests, "get", fake_get)

client = TestClient(app)
res = client.get("/anime/quote")
assert res.status_code == 200
body = res.json()
assert body["status"] == "success"
assert body["data"]["content"] == "Test quote"


def test_anime_quote_upstream_error(monkeypatch) -> None:
class DummyResponse:
status_code = 500

def json(self):
return {}

def fake_get(url, timeout=5): # type: ignore[no-untyped-def]
return DummyResponse()

import ssdlc_demo.main as main_mod

monkeypatch.setattr(main_mod.requests, "get", fake_get)

client = TestClient(app)
res = client.get("/anime/quote")
assert res.status_code == 502


def test_anime_quote_invalid_json(monkeypatch) -> None:
class DummyResponse:
status_code = 200

def json(self): # type: ignore[no-untyped-def]
raise ValueError("invalid json")

def fake_get(url, timeout=5): # type: ignore[no-untyped-def]
return DummyResponse()

import ssdlc_demo.main as main_mod

monkeypatch.setattr(main_mod.requests, "get", fake_get)

client = TestClient(app)
res = client.get("/anime/quote")
assert res.status_code == 502
Loading