Write API wrapper class

- Query by IMDb IMDb
- Query by title

Remove the pytest-httpx plugin

Cleanup some of the omdb.result unit tests
This commit is contained in:
2024-05-06 12:42:20 -07:00
parent 15e468ea41
commit 300c7d7763
6 changed files with 376 additions and 92 deletions

115
omdb/api.py Normal file
View File

@@ -0,0 +1,115 @@
from __future__ import annotations
from typing import Any, Literal
from httpx import URL, Client, QueryParams
from .error import OmdbError
from .result import ExtendedResult, MovieExtendedResult, SeriesExtendedResult
PlotMode = Literal["short", "full"]
OMDB_SEARCH_API = URL("https://www.omdbapi.com/")
OMDB_POSTER_API = URL("https://img.omdbapi.com/")
class OmdbApi:
api_key: str
client: Client
def __init__(self, api_key: str, client: Client = Client()) -> None:
self.api_key = api_key
self.client = client
def _search(self, params: QueryParams) -> Any:
response = self.client.get(
OMDB_SEARCH_API,
params=params.set("apikey", self.api_key),
)
data = response.json()
if response.is_error:
raise OmdbError(response.status_code, data["Error"])
return data
def query_imdb_id(
self,
imdb_id: str,
*,
plot_mode: PlotMode | None = None,
) -> ExtendedResult:
params = QueryParams(i=imdb_id)
if plot_mode:
params = params.set("plot", plot_mode)
data = self._search(params)
return ExtendedResult.parse(data)
def query_title(
self,
title: str,
*,
year: int | None = None,
plot_mode: PlotMode | None = None,
) -> ExtendedResult:
params = QueryParams(t=title)
if year is not None:
params = params.set("y", year)
if plot_mode is not None:
params = params.set("plot", plot_mode)
data = self._search(params)
return ExtendedResult.parse(data)
def query_movie_title(
self,
title: str,
*,
year: int | None = None,
plot_mode: PlotMode | None = None,
) -> MovieExtendedResult:
params = QueryParams(t=title, type="movie")
if year is not None:
params = params.set("y", year)
if plot_mode is not None:
params = params.set("plot", plot_mode)
data = self._search(params)
return MovieExtendedResult.parse(data)
def query_series_title(
self,
title: str,
*,
year: int | None = None,
plot_mode: PlotMode | None = None,
) -> SeriesExtendedResult:
params = QueryParams(t=title, type="series")
if year is not None:
params = params.set("y", year)
if plot_mode is not None:
params = params.set("plot", plot_mode)
data = self._search(params)
return SeriesExtendedResult.parse(data)
# def query_movie_by_title(
# self,
# title: str,
# *,
# year: Optional[int] = None,
# plot: Optional[PlotMode] = None,
# ) -> MovieSearchResult:
# params = httpx.QueryParams(t=title, type="movie")
# if year:
# params = params.set("y", year)
# if plot:
# params = params.set("plot", plot)
# result = self._search(params)
# if not isinstance(result, MovieSearchResult):
# raise TypeError(result)
# return result

10
omdb/error.py Normal file
View File

@@ -0,0 +1,10 @@
from __future__ import annotations
class OmdbError(Exception):
status: int
def __init__(self, status: int, *args: object) -> None:
self.status = status
super().__init__(*args)

View File

@@ -10,7 +10,7 @@ requires-python = ">= 3.12"
dependencies = ["httpx"]
[project.optional-dependencies]
dev = ["pytest", "pytest-httpx", "pytest-cov"]
dev = ["pytest", "pytest-cov"]
[tool.pytest.ini_options]
addopts = "--cov=omdb --cov-report term-missing"

130
tests/api_test.py Normal file
View File

@@ -0,0 +1,130 @@
from unittest.mock import patch
import pytest
from httpx import Client, MockTransport, QueryParams, Request, Response
from omdb.api import OMDB_SEARCH_API, OmdbApi
from omdb.error import OmdbError
from omdb.result import SeriesExtendedResult
def test_omdb_api():
"""Tests the `OmdbApi` class initialization."""
client = Client()
omdb = OmdbApi("key", client)
assert omdb.api_key == "key"
assert omdb.client is client
def test_omdb_api_search_data():
"""Tests the `OmdbApi._search` method when data is returned."""
def handler(request: Request):
assert request.method == "GET"
assert request.url.copy_with(params=None) == OMDB_SEARCH_API
assert request.url.params == QueryParams(apikey="key")
return Response(200, json={"Response": "True"})
omdb = OmdbApi("key", Client(transport=MockTransport(handler)))
data = omdb._search(QueryParams())
assert data == {"Response": "True"}
def test_omdb_api_search_error():
"""Tests the `OmdbApi._search` method when an error is raised."""
def handler(request: Request):
assert request.method == "GET"
assert request.url.copy_with(params=None) == OMDB_SEARCH_API
assert request.url.params == QueryParams(apikey="key")
return Response(404, json={"Response": "False", "Error": "Not Found"})
omdb = OmdbApi("key", Client(transport=MockTransport(handler)))
with pytest.raises(OmdbError, match="Not Found"):
omdb._search(QueryParams())
def test_omdb_api_query_imdb_id():
"""Tests the `OmdbApi.query_imdb_id` method."""
def handler(request: Request):
assert request.method == "GET"
assert request.url.copy_with(params=None) == OMDB_SEARCH_API
assert request.url.params == QueryParams(
apikey="key",
i="tt0000000",
plot="short",
)
return Response(200, json={})
omdb = OmdbApi("key", Client(transport=MockTransport(handler)))
with patch("omdb.result.ExtendedResult.parse") as mock_parse:
omdb.query_imdb_id("tt0000000", plot_mode="short")
mock_parse.assert_called()
def test_omdb_api_query_title():
"""Tests the `OmdbApi.query_title` method."""
def handler(request: Request):
assert request.method == "GET"
assert request.url.copy_with(params=None) == OMDB_SEARCH_API
assert request.url.params == QueryParams(
apikey="key",
t="Title",
y="2000",
plot="short",
)
return Response(200, json={})
omdb = OmdbApi("key", Client(transport=MockTransport(handler)))
with patch("omdb.result.ExtendedResult.parse") as mock_parse:
omdb.query_title("Title", year=2000, plot_mode="short")
mock_parse.assert_called()
def test_omdb_api_query_movie_title():
"""Tests the `OmdbApi.query_movie_title` method."""
def handler(request: Request):
assert request.method == "GET"
assert request.url.copy_with(params=None) == OMDB_SEARCH_API
assert request.url.params == QueryParams(
apikey="key",
t="Title",
type="movie",
y="2000",
plot="short",
)
return Response(200, json={})
omdb = OmdbApi("key", Client(transport=MockTransport(handler)))
with patch("omdb.result.MovieExtendedResult.parse") as mock_parse:
omdb.query_movie_title("Title", year=2000, plot_mode="short")
mock_parse.assert_called()
def test_omdb_api_query_series_title():
"""Tests the `OmdbApi.query_series_title` method."""
def handler(request: Request):
assert request.method == "GET"
assert request.url.copy_with(params=None) == OMDB_SEARCH_API
assert request.url.params == QueryParams(
apikey="key",
t="Title",
type="series",
y="2000",
plot="short",
)
return Response(200, json={})
omdb = OmdbApi("key", Client(transport=MockTransport(handler)))
with patch("omdb.result.SeriesExtendedResult.parse") as mock_parse:
omdb.query_series_title("Title", year=2000, plot_mode="short")
mock_parse.assert_called()

9
tests/error_test.py Normal file
View File

@@ -0,0 +1,9 @@
from omdb.error import OmdbError
def test_omdb_error():
"""Tests the `OmdbError` class initialization."""
error = OmdbError(404, "Not Found")
assert error.status == 404
assert str(error) == "Not Found"

View File

@@ -1,4 +1,5 @@
from datetime import date
from unittest.mock import patch
import pytest
from httpx import URL
@@ -21,96 +22,56 @@ from omdb.result import (
_parse_string_set,
)
mock_movie_search_result = {
"Title": "Movie Title",
"Year": "2000",
"imdbID": "tt0000000",
"Type": "movie",
"Poster": "https://example.com/poster.jpg",
}
mock_series_search_result = {
"Title": "Series Title",
"Year": "20002010",
"imdbID": "tt0000000",
"Type": "series",
"Poster": "https://example.com/poster.jpg",
}
mock_movie_extended_result = {
"Type": "movie",
"Title": "Movie Title",
"Year": "2000",
"Rated": "R",
"Released": "01 Jan 2000",
"Genre": "Action, Adventure, Comedy",
"Plot": "Example movie",
"Language": "English, Esperanto",
"Country": "United States, Molossia",
"Poster": "https://example.com/poster.jpg",
"Ratings": [
{"Source": "Internet Movie Database", "Value": "10.0/10"},
],
"imdbID": "tt0000000",
"Runtime": "123 min",
"Director": "Alan Smithee",
"Writer": "Alan Smithee",
"Actors": "Jack Nicholson, Robert De Niro, Anthony Hopkins",
}
mock_series_extended_result = {
"Type": "series",
"Title": "Series Title",
"Year": "2000",
"Rated": "TV-MA",
"Released": "01 Jan 2000",
"Genre": "Action, Adventure, Comedy",
"Plot": "Example series",
"Language": "English, Esperanto",
"Country": "United States, Molossia",
"Poster": "https://example.com/poster.jpg",
"Ratings": [
{"Source": "Internet Movie Database", "Value": "10.0/10"},
],
"imdbID": "tt0000000",
"totalSeasons": 10,
}
def test_search_result_parse():
"""Tests the `SearchResult.parse` class method."""
movie_result = SearchResult.parse(mock_movie_search_result)
assert movie_result == MovieSearchResult.parse(mock_movie_search_result)
with patch("omdb.result.MovieSearchResult.parse") as mock_parse:
SearchResult.parse({"Type": "movie"})
mock_parse.assert_called()
series_result = SearchResult.parse(mock_series_search_result)
assert series_result == SeriesSearchResult.parse(mock_series_search_result)
with patch("omdb.result.SeriesSearchResult.parse") as mock_parse:
SearchResult.parse({"Type": "series"})
mock_parse.assert_called()
with pytest.raises(
NotImplementedError,
match='Result type "unknown" is not supported.',
):
with pytest.raises(NotImplementedError):
SearchResult.parse({"Type": "unknown"})
def test_movie_search_result_parse():
"""Tests the `MovieSearchResult.parse` class method."""
result = MovieSearchResult.parse(mock_movie_search_result)
result = MovieSearchResult.parse(
{
"Type": "movie",
"imdbID": "tt0000000",
"Title": "Title",
"Year": "2000",
"Poster": "https://example.com/poster.jpg",
}
)
assert result == MovieSearchResult(
title="Movie Title",
imdb_id="tt0000000",
poster=URL("https://example.com/poster.jpg"),
title="Title",
year=2000,
poster=URL("https://example.com/poster.jpg"),
)
def test_series_search_result_parse():
"""Tests the `SeriesSearchResult.parse` class method."""
result = SeriesSearchResult.parse(mock_series_search_result)
result = SeriesSearchResult.parse(
{
"Type": "series",
"imdbID": "tt0000000",
"Title": "Title",
"Year": "20002010",
"Poster": "https://example.com/poster.jpg",
}
)
assert result == SeriesSearchResult(
title="Series Title",
title="Title",
imdb_id="tt0000000",
poster=URL("https://example.com/poster.jpg"),
year_start=2000,
@@ -152,16 +113,15 @@ def test_rating_nfo_element():
def test_extended_result_parse():
"""Tests the `ExtendedResult.parse` class method."""
movie_result = ExtendedResult.parse(mock_movie_extended_result)
assert movie_result == MovieExtendedResult.parse(mock_movie_extended_result)
with patch("omdb.result.MovieExtendedResult.parse") as mock_parse:
ExtendedResult.parse({"Type": "movie"})
mock_parse.assert_called()
series_result = ExtendedResult.parse(mock_series_extended_result)
assert series_result == SeriesExtendedResult.parse(mock_series_extended_result)
with patch("omdb.result.SeriesExtendedResult.parse") as mock_parse:
ExtendedResult.parse({"Type": "series"})
mock_parse.assert_called()
with pytest.raises(
NotImplementedError,
match='Result type "unknown" is not supported.',
):
with pytest.raises(NotImplementedError):
ExtendedResult.parse({"Type": "unknown"})
@@ -230,29 +190,62 @@ def test_extended_result_nfo_subelements():
def test_movie_extended_result_parse():
"""Tests the `MovieExtendedResult.parse` class method."""
result = MovieExtendedResult.parse(mock_movie_extended_result)
result = MovieExtendedResult.parse(
{
"imdbID": "tt0000000",
"Title": "Title",
"Year": "2000",
"Rated": "R",
"Released": "01 Jan 2000",
"Genre": "Action, Adventure, Comedy",
"Plot": "Plot",
"Language": "English, Esperanto",
"Country": "United States, Canada",
"Poster": "https://example.com/poster.jpg",
"Ratings": [{"Source": "Internet Movie Database", "Value": "10.0/10"}],
"Runtime": "123 min",
"Director": "Alan Smithee",
"Writer": "Alan Smithee",
"Actors": "Jenna Maroney, Tracy Jordan",
}
)
assert result == MovieExtendedResult(
title="Movie Title",
title="Title",
imdb_id="tt0000000",
mpaa_rating="R",
release_date=date(2000, 1, 1),
genres={"Action", "Adventure", "Comedy"},
plot="Example movie",
plot="Plot",
language={"English", "Esperanto"},
countries={"United States", "Molossia"},
countries={"United States", "Canada"},
poster=URL("https://example.com/poster.jpg"),
ratings=[Rating("imdb", 10.0, 10)],
runtime=123,
directors={"Alan Smithee"},
writers={"Alan Smithee"},
actors={"Jack Nicholson", "Robert De Niro", "Anthony Hopkins"},
actors={"Jenna Maroney", "Tracy Jordan"},
)
def test_movie_extended_result_nfo_element():
"""Tests the `MovieExtendedResult.nfo_element` method."""
result = MovieExtendedResult.parse(mock_movie_extended_result)
result = MovieExtendedResult(
title="Title",
imdb_id="tt0000000",
mpaa_rating="R",
release_date=date(2000, 1, 1),
genres={"Action", "Adventure", "Comedy"},
plot="Plot",
language={"English", "Esperanto"},
countries={"United States", "Canada"},
poster=URL("https://example.com/poster.jpg"),
ratings=[Rating("imdb", 10.0, 10)],
runtime=123,
directors={"Alan Smithee"},
writers={"Alan Smithee"},
actors={"Jenna Maroney", "Tracy Jordan"},
)
element = result.nfo_element()
assert element.tag == "movie"
@@ -281,9 +274,9 @@ def test_movie_extended_result_nfo_element():
assert credits_elements[0].text == "Alan Smithee"
actor_elements = element.findall("actor")
assert len(actor_elements) == 3
assert len(actor_elements) == 2
assert any(
actor_element[0].tag == "name" and actor_element[0].text == "Anthony Hopkins"
actor_element[0].tag == "name" and actor_element[0].text == "Jenna Maroney"
for actor_element in actor_elements
)
@@ -291,16 +284,31 @@ def test_movie_extended_result_nfo_element():
def test_series_extended_result_parse():
"""Tests the `SeriesExtendedResult.parse` class method."""
series_extended_result = SeriesExtendedResult.parse(mock_series_extended_result)
assert series_extended_result == SeriesExtendedResult(
title="Series Title",
result = SeriesExtendedResult.parse(
{
"imdbID": "tt0000000",
"Title": "Title",
"Year": "2000",
"Rated": "R",
"Released": "01 Jan 2000",
"Genre": "Action, Adventure, Comedy",
"Plot": "Plot",
"Language": "English, Esperanto",
"Country": "United States, Canada",
"Poster": "https://example.com/poster.jpg",
"Ratings": [{"Source": "Internet Movie Database", "Value": "10.0/10"}],
"totalSeasons": 10,
}
)
assert result == SeriesExtendedResult(
title="Title",
imdb_id="tt0000000",
mpaa_rating="TV-MA",
mpaa_rating="R",
release_date=date(2000, 1, 1),
genres={"Action", "Adventure", "Comedy"},
plot="Example series",
plot="Plot",
language={"English", "Esperanto"},
countries={"United States", "Molossia"},
countries={"United States", "Canada"},
poster=URL("https://example.com/poster.jpg"),
ratings=[Rating("imdb", 10.0, 10)],
total_seasons=10,
@@ -310,7 +318,19 @@ def test_series_extended_result_parse():
def test_series_extended_result_nfo():
"""Tests the `SeriesExtendedResult.nfo_element` method."""
result = SeriesExtendedResult.parse(mock_series_extended_result)
result = SeriesExtendedResult(
title="Title",
imdb_id="tt0000000",
mpaa_rating="R",
release_date=date(2000, 1, 1),
genres={"Action", "Adventure", "Comedy"},
plot="Plot",
language={"English", "Esperanto"},
countries={"United States", "Canada"},
poster=URL("https://example.com/poster.jpg"),
ratings=[Rating("imdb", 10.0, 10)],
total_seasons=10,
)
element = result.nfo_element()
assert element.tag == "tvshow"