2024-06-16 18:33:01 +00:00
|
|
|
import datetime
|
|
|
|
import imghdr
|
|
|
|
import json
|
|
|
|
import random
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
|
|
class HttpError(Exception):
|
|
|
|
def __init__(self, status_code: int, reason: str) -> None:
|
|
|
|
"""
|
|
|
|
Initialize the HttpError.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
status_code : int
|
|
|
|
The status code of the error.
|
|
|
|
reason : str
|
|
|
|
The reason of the error.
|
|
|
|
"""
|
|
|
|
self.status_code = status_code
|
|
|
|
self.reason = reason
|
|
|
|
super().__init__(f"HTTP Error {status_code}: {reason}")
|
|
|
|
|
|
|
|
|
|
|
|
class Comic:
|
|
|
|
"""
|
2024-06-20 17:48:49 +00:00
|
|
|
A class representing a xkcd comic.
|
2024-06-16 18:33:01 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
xkcd_dict: dict[str, Any],
|
|
|
|
raw_image: bytes | None = None,
|
|
|
|
comic_url: str | None = None,
|
|
|
|
explanation_url: str | None = None,
|
|
|
|
) -> None:
|
|
|
|
self.id: int | None = xkcd_dict.get("num")
|
|
|
|
self.date: datetime.date | None = self._determine_date(xkcd_dict)
|
|
|
|
self.title: str | None = xkcd_dict.get("safe_title")
|
|
|
|
self.description: str | None = xkcd_dict.get("alt")
|
|
|
|
self.transcript: str | None = xkcd_dict.get("transcript")
|
|
|
|
self.image: bytes | None = raw_image
|
|
|
|
self.image_extension: str | None = self._determine_image_extension()
|
|
|
|
self.image_url: str | None = xkcd_dict.get("img")
|
|
|
|
self.comic_url: str | None = comic_url
|
|
|
|
self.explanation_url: str | None = explanation_url
|
|
|
|
|
2024-06-20 17:48:49 +00:00
|
|
|
@staticmethod
|
|
|
|
def _determine_date(xkcd_dict: dict[str, Any]) -> datetime.date | None:
|
2024-06-16 18:33:01 +00:00
|
|
|
"""
|
|
|
|
Determine the date of the comic.
|
2024-06-20 17:48:49 +00:00
|
|
|
Args:
|
|
|
|
xkcd_dict:
|
2024-06-16 18:33:01 +00:00
|
|
|
|
2024-06-20 17:48:49 +00:00
|
|
|
Returns:
|
2024-06-16 18:33:01 +00:00
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return datetime.date(
|
|
|
|
int(xkcd_dict["year"]), int(xkcd_dict["month"]), int(xkcd_dict["day"])
|
|
|
|
)
|
|
|
|
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _determine_image_extension(self) -> str | None:
|
|
|
|
"""
|
|
|
|
Determine the image extension of the comic.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str | None
|
|
|
|
The extension of the image.
|
|
|
|
"""
|
|
|
|
return f".{imghdr.what(None, h=self.image)}" if self.image else None
|
|
|
|
|
|
|
|
def update_raw_image(self, raw_image: bytes) -> None:
|
|
|
|
"""
|
|
|
|
Update the raw image of the comic.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
raw_image : bytes
|
|
|
|
The raw image data.
|
|
|
|
"""
|
|
|
|
self.image = raw_image
|
|
|
|
self.image_extension = self._determine_image_extension()
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
"""
|
|
|
|
Return the representation of the comic.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str
|
|
|
|
The representation of the comic.
|
|
|
|
"""
|
|
|
|
return f"Comic({self.title})"
|
|
|
|
|
|
|
|
|
|
|
|
class Client:
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
api_url: str = "https://xkcd.com",
|
|
|
|
explanation_wiki_url: str = "https://www.explainxkcd.com/wiki/index.php/",
|
|
|
|
) -> None:
|
|
|
|
self._api_url = api_url
|
|
|
|
self._explanation_wiki_url = explanation_wiki_url
|
|
|
|
|
|
|
|
def latest_comic_url(self) -> str:
|
|
|
|
"""
|
|
|
|
Get the URL for the latest comic.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str
|
|
|
|
The URL for the latest comic.
|
|
|
|
"""
|
|
|
|
return f"{self._api_url}/info.0.json"
|
|
|
|
|
|
|
|
def comic_id_url(self, comic_id: int) -> str:
|
|
|
|
"""
|
|
|
|
Get the URL for a specific comic ID.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
comic_id : int
|
|
|
|
The ID of the comic.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str
|
|
|
|
The URL for the specific comic ID.
|
|
|
|
"""
|
|
|
|
return f"{self._api_url}/{comic_id}/info.0.json"
|
|
|
|
|
|
|
|
def _parse_response(self, response_text: str) -> Comic:
|
|
|
|
"""
|
|
|
|
Parse the response text into a Comic object.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
response_text : str
|
|
|
|
The response text to parse.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
Comic
|
|
|
|
The parsed comic object.
|
|
|
|
"""
|
|
|
|
response_dict: dict[str, Any] = json.loads(response_text)
|
|
|
|
comic_url: str = f"{self._api_url}/{response_dict['num']}/"
|
|
|
|
explanation_url: str = f"{self._explanation_wiki_url}{response_dict['num']}"
|
|
|
|
|
|
|
|
return Comic(response_dict, comic_url=comic_url, explanation_url=explanation_url)
|
|
|
|
|
|
|
|
def _fetch_comic(self, comic_id: int, raw_comic_image: bool) -> Comic:
|
|
|
|
"""
|
|
|
|
Fetch a comic from the xkcd API.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
comic_id : int
|
|
|
|
The ID of the comic to fetch.
|
|
|
|
raw_comic_image : bool
|
|
|
|
Whether to fetch the raw image data.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
Comic
|
|
|
|
The fetched comic.
|
|
|
|
"""
|
|
|
|
comic = self._parse_response(self._request_comic(comic_id))
|
|
|
|
|
|
|
|
if raw_comic_image:
|
|
|
|
raw_image = self._request_raw_image(comic.image_url)
|
|
|
|
comic.update_raw_image(raw_image)
|
|
|
|
|
|
|
|
return comic
|
|
|
|
|
|
|
|
def get_latest_comic(self, raw_comic_image: bool = False) -> Comic:
|
|
|
|
"""
|
|
|
|
Get the latest xkcd comic.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
raw_comic_image : bool, optional
|
|
|
|
Whether to fetch the raw image data, by default False
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
Comic
|
|
|
|
The latest xkcd comic.
|
|
|
|
"""
|
|
|
|
return self._fetch_comic(0, raw_comic_image)
|
|
|
|
|
|
|
|
def get_comic(self, comic_id: int, raw_comic_image: bool = False) -> Comic:
|
|
|
|
"""
|
|
|
|
Get a specific xkcd comic.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
comic_id : int
|
|
|
|
The ID of the comic to fetch.
|
|
|
|
raw_comic_image : bool, optional
|
|
|
|
Whether to fetch the raw image data, by default False
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
Comic
|
|
|
|
The fetched xkcd comic.
|
|
|
|
"""
|
|
|
|
return self._fetch_comic(comic_id, raw_comic_image)
|
|
|
|
|
|
|
|
def get_random_comic(self, raw_comic_image: bool = False) -> Comic:
|
|
|
|
"""
|
|
|
|
Get a random xkcd comic.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
raw_comic_image : bool, optional
|
|
|
|
Whether to fetch the raw image data, by default False
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
Comic
|
|
|
|
The random xkcd comic.
|
|
|
|
"""
|
|
|
|
latest_comic_id: int = self._parse_response(self._request_comic(0)).id or 0
|
|
|
|
random_id: int = random.randint(1, latest_comic_id)
|
|
|
|
|
|
|
|
return self._fetch_comic(random_id, raw_comic_image)
|
|
|
|
|
|
|
|
def _request_comic(self, comic_id: int) -> str:
|
|
|
|
"""
|
|
|
|
Request the comic data from the xkcd API.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
comic_id : int
|
|
|
|
The ID of the comic to fetch.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str
|
|
|
|
The response text.
|
|
|
|
|
|
|
|
Raises
|
|
|
|
------
|
|
|
|
HttpError
|
|
|
|
If the request fails.
|
|
|
|
"""
|
|
|
|
comic_url = self.latest_comic_url() if comic_id <= 0 else self.comic_id_url(comic_id)
|
|
|
|
|
|
|
|
try:
|
|
|
|
response = httpx.get(comic_url)
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
except httpx.HTTPStatusError as exc:
|
|
|
|
raise HttpError(exc.response.status_code, exc.response.reason_phrase) from exc
|
|
|
|
|
|
|
|
return response.text
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _request_raw_image(raw_image_url: str | None) -> bytes:
|
|
|
|
"""
|
|
|
|
Request the raw image data from the xkcd API.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
raw_image_url : str | None
|
|
|
|
The URL of the raw image data.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
bytes
|
|
|
|
The raw image data.
|
|
|
|
|
|
|
|
Raises
|
|
|
|
------
|
|
|
|
HttpError
|
|
|
|
If the request fails.
|
|
|
|
"""
|
|
|
|
if not raw_image_url:
|
|
|
|
raise HttpError(404, "Image URL not found")
|
|
|
|
|
|
|
|
try:
|
|
|
|
response = httpx.get(raw_image_url)
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
except httpx.HTTPStatusError as exc:
|
|
|
|
raise HttpError(exc.response.status_code, exc.response.reason_phrase) from exc
|
|
|
|
|
|
|
|
return response.content
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
"""
|
|
|
|
Return the representation of the client.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
str
|
|
|
|
The representation of the client.
|
|
|
|
"""
|
|
|
|
return "Client()"
|