feat(api): Add type validation for API Responses

This commit is contained in:
TheOnlyWayUp
2024-11-30 21:35:31 +00:00
parent a31c26f8c5
commit f91a01e574
4 changed files with 57 additions and 12 deletions
+1
View File
@@ -13,6 +13,7 @@ dependencies = [
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
"pydantic-settings>=2.6.1", "pydantic-settings>=2.6.1",
"eliot>=1.16.0", "eliot>=1.16.0",
"type-extensions>=0.1.2",
] ]
[tool.ruff.lint] [tool.ruff.lint]
+45 -9
View File
@@ -1,4 +1,5 @@
from typing import Optional, Tuple from typing import List, Optional, Tuple
from typing_extensions import TypedDict
import re import re
import unicodedata import unicodedata
import logging import logging
@@ -11,7 +12,7 @@ from dotenv import load_dotenv
from ebooklib import epub from ebooklib import epub
from ebooklib.epub import EpubBook from ebooklib.epub import EpubBook
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from pydantic import model_validator, field_validator from pydantic import TypeAdapter, model_validator, field_validator
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from aiohttp_client_cache.session import CachedSession from aiohttp_client_cache.session import CachedSession
@@ -157,13 +158,48 @@ async def wp_get_cookies(username: str, password: str) -> dict:
return cookies return cookies
# --- Models --- #
class Language(TypedDict):
name: str
class User(TypedDict):
username: str
class Part(TypedDict):
id: int
title: str
class Story(TypedDict):
id: str
title: str
createDate: str
modifyDate: str
language: Language
user: User
description: str
cover: str
completed: bool
tags: List[str]
mature: bool
url: str
parts: List[Part]
isPaywalled: bool
story_ta = TypeAdapter(Story)
# --- API Calls --- # # --- API Calls --- #
@backoff.on_exception(backoff.expo, ClientResponseError, max_time=15) @backoff.on_exception(backoff.expo, ClientResponseError, max_time=15)
async def fetch_story_from_partId( async def fetch_story_from_partId(
part_id: int, cookies: Optional[dict] = None part_id: int, cookies: Optional[dict] = None
) -> Tuple[int, dict]: ) -> Tuple[str, Story]:
"""Return a Story ID from a Part ID.""" """Return a Story ID from a Part ID."""
with start_action(action_type="api_fetch_storyFromPartId"): with start_action(action_type="api_fetch_storyFromPartId"):
async with CachedSession( async with CachedSession(
@@ -176,11 +212,11 @@ async def fetch_story_from_partId(
body = await response.json() body = await response.json()
return body["groupId"], body["group"] return str(body["groupId"]), story_ta.validate_python(body["group"])
@backoff.on_exception(backoff.expo, ClientResponseError, max_time=15) @backoff.on_exception(backoff.expo, ClientResponseError, max_time=15)
async def retrieve_story(story_id: int, cookies: Optional[dict] = None) -> dict: async def retrieve_story(story_id: int, cookies: Optional[dict] = None) -> Story:
"""Taking a story_id, return its information from the Wattpad API.""" """Taking a story_id, return its information from the Wattpad API."""
with start_action(action_type="api_fetch_story", story_id=story_id): with start_action(action_type="api_fetch_story", story_id=story_id):
async with CachedSession( async with CachedSession(
@@ -193,7 +229,7 @@ async def retrieve_story(story_id: int, cookies: Optional[dict] = None) -> dict:
body = await response.json() body = await response.json()
return body return story_ta.validate_python(body)
@backoff.on_exception(backoff.expo, ClientResponseError, max_time=15) @backoff.on_exception(backoff.expo, ClientResponseError, max_time=15)
@@ -231,7 +267,7 @@ async def fetch_cover(url: str) -> bytes:
# --- EPUB Generation --- # # --- EPUB Generation --- #
def set_metadata(book: EpubBook, data: dict) -> None: def set_metadata(book: EpubBook, data: Story) -> None:
"""Set book metadata.""" """Set book metadata."""
book.add_author(data["user"]["username"]) book.add_author(data["user"]["username"])
@@ -252,7 +288,7 @@ def set_metadata(book: EpubBook, data: dict) -> None:
) )
async def set_cover(book: EpubBook, data: dict) -> None: async def set_cover(book: EpubBook, data: Story) -> None:
"""Set book cover.""" """Set book cover."""
book.set_cover("cover.jpg", await fetch_cover(data["cover"])) book.set_cover("cover.jpg", await fetch_cover(data["cover"]))
chapter = epub.EpubHtml( chapter = epub.EpubHtml(
@@ -263,7 +299,7 @@ async def set_cover(book: EpubBook, data: dict) -> None:
async def add_chapters( async def add_chapters(
book: EpubBook, book: EpubBook,
data: dict, data: Story,
download_images: bool = False, download_images: bool = False,
cookies: Optional[dict] = None, cookies: Optional[dict] = None,
): ):
-3
View File
@@ -72,7 +72,6 @@ app.add_middleware(RequestCancelledMiddleware)
class DownloadMode(Enum): class DownloadMode(Enum):
story = "story" story = "story"
part = "part" part = "part"
collection = "collection"
@app.get("/") @app.get("/")
@@ -148,9 +147,7 @@ async def handle_download(
logger.error(f"Retrieved story id ({story_id=})") logger.error(f"Retrieved story id ({story_id=})")
book = epub.EpubBook() book = epub.EpubBook()
set_metadata(book, metadata) set_metadata(book, metadata)
await set_cover(book, metadata) await set_cover(book, metadata)
async for title in add_chapters( async for title in add_chapters(
+11
View File
@@ -195,6 +195,7 @@ dependencies = [
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "rich" }, { name = "rich" },
{ name = "type-extensions" },
] ]
[package.metadata] [package.metadata]
@@ -207,6 +208,7 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pydantic-settings", specifier = ">=2.6.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "rich", specifier = ">=13.9.4" }, { name = "rich", specifier = ">=13.9.4" },
{ name = "type-extensions", specifier = ">=0.1.2" },
] ]
[[package]] [[package]]
@@ -894,6 +896,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
] ]
[[package]]
name = "type-extensions"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/d0/18bf3a92af3f6a0fc38f874e858d707242ac5c14148ff1c4ee1615397e73/type_extensions-0.1.2.tar.gz", hash = "sha256:7c7b54eba6d7401ad5e69ecec6b7c767d7d7aae9b2b7e56249bc7bcaf833161f", size = 16102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/dc/707d386bbd830d72fdd58c06f1d8a935d2553498f6868cd7de141addbe06/type_extensions-0.1.2-py2.py3-none-any.whl", hash = "sha256:fc9c8506f87b6227eeb43795061d61571b66a07df6fbcacf31a17d2cb2050153", size = 5566 },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"