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:
115
omdb/api.py
Normal file
115
omdb/api.py
Normal 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
10
omdb/error.py
Normal 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)
|
@@ -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
130
tests/api_test.py
Normal 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
9
tests/error_test.py
Normal 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"
|
@@ -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": "2000–2010",
|
||||
"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": "2000–2010",
|
||||
"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"
|
||||
|
||||
|
Reference in New Issue
Block a user