From 7ebc3c955aaaca94ba6e0ba43f42503cbc8267e7 Mon Sep 17 00:00:00 2001 From: Dom Batten Date: Thu, 18 Nov 2021 13:42:43 +0000 Subject: [PATCH 1/6] add tmdb test client --- README.md | 4 +- src/phylm/clients/__init__.py | 1 + src/phylm/clients/tmdb.py | 37 +++++ .../clients/tmdb/no_results.yaml | 55 ++++++++ .../clients/tmdb/the_matrix.yaml | 130 ++++++++++++++++++ tests/unit/clients/__init__.py | 0 tests/unit/clients/test_tmdb.py | 43 ++++++ 7 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 src/phylm/clients/__init__.py create mode 100644 src/phylm/clients/tmdb.py create mode 100644 tests/fixtures/vcr_cassettes/clients/tmdb/no_results.yaml create mode 100644 tests/fixtures/vcr_cassettes/clients/tmdb/the_matrix.yaml create mode 100644 tests/unit/clients/__init__.py create mode 100644 tests/unit/clients/test_tmdb.py diff --git a/README.md b/README.md index 6f0571d..dcfef8e 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,13 @@ pip install phylm 'title': 'The Matrix', 'kind': 'movie', 'year': 1999, - 'cover url': 'https://some-url.com', + 'cover_photo': 'https://some-url.com', 'imdb_id': '0133093', }, { 'title': 'The Matrix Reloaded', 'kind': 'movie', 'year': 2003, - 'cover url': 'https://some-url.com', + 'cover_photo': 'https://some-url.com', 'imdb_id': '0234215', }, { ... diff --git a/src/phylm/clients/__init__.py b/src/phylm/clients/__init__.py new file mode 100644 index 0000000..73a5ea4 --- /dev/null +++ b/src/phylm/clients/__init__.py @@ -0,0 +1 @@ +"""Clients.""" diff --git a/src/phylm/clients/tmdb.py b/src/phylm/clients/tmdb.py new file mode 100644 index 0000000..94d4024 --- /dev/null +++ b/src/phylm/clients/tmdb.py @@ -0,0 +1,37 @@ +"""Client to interact with The Movie DB (TMDB).""" +from typing import Any + +from requests import Session + + +class TmdbClient(Session): + """Class to abstract to the Tmdb API.""" + + def __init__(self, api_key: str) -> None: + """Initialize the client. + + Args: + api_key: an api_key for authentication + """ + super().__init__() + self.api_key = api_key + self._base = "https://api.themoviedb.org/3" + + def search_movies(self, query: str) -> Any: + """Search for movies. + + Args: + query: the search query + + Returns: + Any: the search results + """ + payload = { + "api_key": self.api_key, + "language": "en-GB", + "query": query, + "include_adult": False, + "region": "GB", + } + res = self.get(f"{self._base}/search/movie", params=payload) # type: ignore + return res.json()["results"] diff --git a/tests/fixtures/vcr_cassettes/clients/tmdb/no_results.yaml b/tests/fixtures/vcr_cassettes/clients/tmdb/no_results.yaml new file mode 100644 index 0000000..265733c --- /dev/null +++ b/tests/fixtures/vcr_cassettes/clients/tmdb/no_results.yaml @@ -0,0 +1,55 @@ +interactions: + - request: + body: null + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.26.0 + method: GET + uri: https://api.themoviedb.org/3/search/movie?include_adult=False&language=en-GB&query=aslkdjaskldjaslkdjaslkdjasd®ion=GB + response: + body: + string: !!binary | + H4sIAAAAAAAAA6tWKkhMT1WyMtRRKkotLs0pKVayio7VUSrJL0nMiQfJAQUMYHy4EoNaALNPV645 + AAAA + headers: + Access-Control-Allow-Methods: + - GET, HEAD, POST, PUT, DELETE, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, + Content-Length, Content-Range + Cache-Control: + - public, max-age=600 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json;charset=utf-8 + Date: + - Thu, 18 Nov 2021 13:36:12 GMT + Server: + - openresty + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + Via: + - 1.1 f781469e78b7a441c6f692b1629e1519.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - ZzoH_Hbughh50zblladdE7sdS_RFUstsVkCVQHAykhgmOgpaqP1jmQ== + X-Amz-Cf-Pop: + - LHR50-C1 + X-Cache: + - Miss from cloudfront + status: + code: 200 + message: OK +version: 1 diff --git a/tests/fixtures/vcr_cassettes/clients/tmdb/the_matrix.yaml b/tests/fixtures/vcr_cassettes/clients/tmdb/the_matrix.yaml new file mode 100644 index 0000000..45c04b0 --- /dev/null +++ b/tests/fixtures/vcr_cassettes/clients/tmdb/the_matrix.yaml @@ -0,0 +1,130 @@ +interactions: + - request: + body: null + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.26.0 + method: GET + uri: https://api.themoviedb.org/3/search/movie?include_adult=False&language=en-GB&query=The+Matrix®ion=GB + response: + body: + string: !!binary | + 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 + headers: + Access-Control-Allow-Methods: + - GET, HEAD, POST, PUT, DELETE, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, + Content-Length, Content-Range + Age: + - "54" + Cache-Control: + - public, max-age=600 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json;charset=utf-8 + Date: + - Thu, 18 Nov 2021 13:32:48 GMT + Server: + - openresty + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + Via: + - 1.1 a4ba6141247f3b441c87ee1a49ec2851.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - gF2BvyhPhl7nmCbKnW6QPgVc4yCb8q2BUsuosLPc8u1HFv3dMG1wAw== + X-Amz-Cf-Pop: + - LHR52-C1 + X-Cache: + - Hit from cloudfront + status: + code: 200 + message: OK +version: 1 diff --git a/tests/unit/clients/__init__.py b/tests/unit/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/clients/test_tmdb.py b/tests/unit/clients/test_tmdb.py new file mode 100644 index 0000000..6cdb503 --- /dev/null +++ b/tests/unit/clients/test_tmdb.py @@ -0,0 +1,43 @@ +"""Tests for the Tmdb client.""" +import vcr +from tests.conftest import FIXTURES_DIR + +from phylm.clients.tmdb import TmdbClient + +VCR_FIXTURES_DIR = f"{FIXTURES_DIR}/clients/tmdb" + + +class TestSearchMovies: + """Tests for the `search_movies` method.""" + + @vcr.use_cassette( + f"{VCR_FIXTURES_DIR}/the_matrix.yaml", filter_query_parameters=["api_key"] + ) + def test_results(self) -> None: + """ + Given a search query, + When the `search_movies` method is invoked with the query, + Then search results are returned from the api + """ + client = TmdbClient(api_key="dummy_key") + + results = client.search_movies(query="The Matrix") + + assert len(results) + assert results[0]["title"] == "The Matrix" + assert results[0]["release_date"] == "1999-06-11" + + @vcr.use_cassette( + f"{VCR_FIXTURES_DIR}/no_results.yaml", filter_query_parameters=["api_key"] + ) + def test_no_results(self) -> None: + """ + Given a search query with no results, + When the `search_movies` method is invoked with the query, + Then an empty list is returned from the api + """ + client = TmdbClient(api_key="dummy_key") + + results = client.search_movies(query="aslkdjaskldjaslkdjaslkdjasd") + + assert len(results) == 0 From eabbf0eb274081373914bbe70f1616c4c6bd069f Mon Sep 17 00:00:00 2001 From: Dom Batten Date: Thu, 18 Nov 2021 14:11:01 +0000 Subject: [PATCH 2/6] add `search_tmdb_movies` tool function --- src/phylm/clients/tmdb.py | 7 +++- src/phylm/errors.py | 4 ++ src/phylm/tools.py | 31 ++++++++++++++++ tests/unit/tools/test_tools.py | 67 ++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/phylm/clients/tmdb.py b/src/phylm/clients/tmdb.py index 94d4024..6b977f3 100644 --- a/src/phylm/clients/tmdb.py +++ b/src/phylm/clients/tmdb.py @@ -1,5 +1,7 @@ """Client to interact with The Movie DB (TMDB).""" from typing import Any +from typing import Dict +from typing import List from requests import Session @@ -17,7 +19,7 @@ def __init__(self, api_key: str) -> None: self.api_key = api_key self._base = "https://api.themoviedb.org/3" - def search_movies(self, query: str) -> Any: + def search_movies(self, query: str) -> List[Dict[str, Any]]: """Search for movies. Args: @@ -34,4 +36,5 @@ def search_movies(self, query: str) -> Any: "region": "GB", } res = self.get(f"{self._base}/search/movie", params=payload) # type: ignore - return res.json()["results"] + results: List[Dict[str, Any]] = res.json()["results"] + return results diff --git a/src/phylm/errors.py b/src/phylm/errors.py index b937912..9cbd28b 100644 --- a/src/phylm/errors.py +++ b/src/phylm/errors.py @@ -7,3 +7,7 @@ class UnrecognizedSourceError(Exception): class SourceNotLoadedError(Exception): """Raised when data from an unloaded source is retreived.""" + + +class NoTMDbApiKeyError(Exception): + """Raised when requests are made to TMDb but no api_key has be provided.""" diff --git a/src/phylm/tools.py b/src/phylm/tools.py index e09d445..9692e0c 100644 --- a/src/phylm/tools.py +++ b/src/phylm/tools.py @@ -1,10 +1,15 @@ """Module to hold `phylm` tools.""" +import os +from typing import Any from typing import Dict from typing import List +from typing import Optional from typing import Union from imdb.Movie import Movie +from phylm.clients.tmdb import TmdbClient +from phylm.errors import NoTMDbApiKeyError from phylm.sources.imdb import ia @@ -29,3 +34,29 @@ def search_movies(query: str) -> List[Dict[str, Union[str, int]]]: } for r in results ] + + +def search_tmdb_movies( + query: str, api_key: Optional[str] = None +) -> List[Dict[str, Any]]: + """Search for movies on TMDb. + + Args: + query: the query string + api_key: an api_key can either be provided here or through a TMDB_API_KEY env + var + + Raises: + NoTMDbApiKeyError: when no api_key has been provided + + Returns: + List[Dict[str, Any]]: the search results + """ + tmdb_api_key = api_key or os.environ.get("TMDB_API_KEY") + + if not tmdb_api_key: + raise NoTMDbApiKeyError("An `api_key` must be provided to use this service") + + client = TmdbClient(api_key=tmdb_api_key) + + return client.search_movies(query=query) diff --git a/tests/unit/tools/test_tools.py b/tests/unit/tools/test_tools.py index 61c5438..958e04b 100644 --- a/tests/unit/tools/test_tools.py +++ b/tests/unit/tools/test_tools.py @@ -1,4 +1,5 @@ """Tests for the `tools` module.""" +import os from typing import List from unittest.mock import MagicMock from unittest.mock import patch @@ -7,7 +8,9 @@ from imdb import IMDb from imdb.Movie import Movie +from phylm.errors import NoTMDbApiKeyError from phylm.tools import search_movies +from phylm.tools import search_tmdb_movies ia = IMDb() @@ -40,3 +43,67 @@ def test_results(self, mock_ia: MagicMock, the_matrix: List[Movie]) -> None: assert result[0]["title"] == "The Matrix" assert result[0]["year"] == 1999 assert result[0]["cover_photo"] + + +class TestSearchTmdbMovies: + """Tests for the `search_tmdb_movies` method.""" + + def test_no_api_key(self) -> None: + """ + Given no api key, + When the `search_tmdb_movies` function is invoked, + Then a NoTMDbApiKeyError is raised + """ + with pytest.raises(NoTMDbApiKeyError): + search_tmdb_movies(query="The Matrix") + + @patch(f"{TOOLS_MODULE_PATH}.TmdbClient", autospec=True) + def test_with_api_key_as_arg(self, mock_tmdb_client_class: MagicMock) -> None: + """ + Given an api_key supplied as an arg, + When the `search_tmdb_movies` function is invoked, + Then the api_key is used in the client + """ + api_key = "nice_key" + mock_tmdb_client = mock_tmdb_client_class.return_value + mock_tmdb_client.search_movies.return_value = [{"title": "The Matrix"}] + + results = search_tmdb_movies(query="The Matrix", api_key=api_key) + + assert results == [{"title": "The Matrix"}] + mock_tmdb_client_class.assert_called_once_with(api_key=api_key) + + @patch(f"{TOOLS_MODULE_PATH}.TmdbClient", autospec=True) + @patch.dict(os.environ, {"TMDB_API_KEY": "nice_key"}, clear=True) + def test_with_api_key_as_env_var(self, mock_tmdb_client_class: MagicMock) -> None: + """ + Given an api_key supplied as an env var, + When the `search_tmdb_movies` function is invoked, + Then the api_key is used in the client + """ + mock_tmdb_client = mock_tmdb_client_class.return_value + mock_tmdb_client.search_movies.return_value = [{"title": "The Matrix"}] + + results = search_tmdb_movies(query="The Matrix") + + assert results == [{"title": "The Matrix"}] + mock_tmdb_client_class.assert_called_once_with(api_key="nice_key") + + @patch(f"{TOOLS_MODULE_PATH}.TmdbClient", autospec=True) + @patch.dict(os.environ, {"TMDB_API_KEY": "nice_key"}, clear=True) + def test_with_api_key_arg_preferred( + self, mock_tmdb_client_class: MagicMock + ) -> None: + """ + Given an api_key supplied as an env var and one as an arg, + When the `search_tmdb_movies` function is invoked, + Then the api_key as an arg is used in the client + """ + api_key = "better_key" + mock_tmdb_client = mock_tmdb_client_class.return_value + mock_tmdb_client.search_movies.return_value = [{"title": "The Matrix"}] + + results = search_tmdb_movies(query="The Matrix", api_key=api_key) + + assert results == [{"title": "The Matrix"}] + mock_tmdb_client_class.assert_called_once_with(api_key=api_key) From d1c4a8b4dff631f291c0d06cdee23c8d703bb663 Mon Sep 17 00:00:00 2001 From: Dom Batten Date: Thu, 18 Nov 2021 14:13:23 +0000 Subject: [PATCH 3/6] tidy up imdb tool test class --- tests/unit/tools/test_tools.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit/tools/test_tools.py b/tests/unit/tools/test_tools.py index 958e04b..5fb9a98 100644 --- a/tests/unit/tools/test_tools.py +++ b/tests/unit/tools/test_tools.py @@ -12,16 +12,19 @@ from phylm.tools import search_movies from phylm.tools import search_tmdb_movies -ia = IMDb() +TOOLS_MODULE_PATH = "phylm.tools" -TOOLS_MODULE_PATH = "phylm.tools" +@pytest.fixture(scope="module", name="imdb_ia") +def imdb_ia_fixture() -> IMDb: + """Return the IMDb class.""" + return IMDb() @pytest.fixture(scope="module", name="the_matrix") -def the_matrix_fixture() -> List[Movie]: +def the_matrix_fixture(imdb_ia: IMDb) -> List[Movie]: """Return The Matrix IMDb Movie object.""" - return list(ia.search_movie("The Matrix")) + return list(imdb_ia.search_movie("The Matrix")) class TestSearchMovies: From f22228288bd406da3986c52c209611140f2eca88 Mon Sep 17 00:00:00 2001 From: Dom Batten Date: Thu, 18 Nov 2021 14:27:31 +0000 Subject: [PATCH 4/6] add `search_tmdb_movies` to the docs --- docs/tools.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- mkdocs.yml | 1 + 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index 226fb54..91f9e44 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -3,7 +3,7 @@ # Search movies For a given movie title query you can return a list of search results from `IMDb` -through `get_suggestions`: +through `search_movies`: ```python >>> from phylm.tools import search_movies @@ -12,13 +12,13 @@ through `get_suggestions`: 'title': 'The Matrix', 'kind': 'movie', 'year': 1999, - 'cover url': 'https://some-url.com', + 'cover_photo': 'https://some-url.com', 'imdb_id': '0133093', }, { 'title': 'The Matrix Reloaded', 'kind': 'movie', 'year': 2003, - 'cover url': 'https://some-url.com', + 'cover_photo': 'https://some-url.com', 'imdb_id': '0234215', }, { ... @@ -28,3 +28,47 @@ through `get_suggestions`: rendering: show_signature_annotations: true heading_level: 2 + +# TMDB + +`phylm` also provides tools to interact with [The Movie Database](https://www.themoviedb.org/) (TMDb). + +!!! info "" + To use TMDB tools you'll need to sign up for an API key, instructions [here](https://developers.themoviedb.org/3). + Once you have your key, export it as an env var called `TMDB_API_KEY` so that it's + available to use in these tools. You also have the option of passing in the key as + an argument to each function. + +## Search movies + +For a given movie title query you can return a list of search results from `TMDb` +through `search_tmdb_movies`. Note that this search performs a lot quicker than the +`imdb` `search_movies`. + +```python +>>> from phylm.tools import search_tmdb_movies +>>> search_tmdb_movies("The Matrix", api_key="abc") # the api key can be provided as an env var instead +[{ + 'adult': False, + 'backdrop_path': '/fNG7i7RqMErkcqhohV2a6cV1Ehy.jpg', + 'genre_ids': [28, 878], + 'id': 603, + 'original_language': 'en', + 'original_title': 'The Matrix', + 'overview': 'Set in the 22nd century, The Matrix tells the story of a computer hacker...' + 'popularity': 79.956, + 'poster_path': '/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg', + 'release_date': '1999-06-11', + 'title': 'The Matrix', + 'video': False, + 'vote_average': 8.2, + 'vote_count': 20216, +}, { + ... +} +``` + +::: phylm.tools.search_tmdb_movies + rendering: + show_signature_annotations: true + heading_level: 3 diff --git a/mkdocs.yml b/mkdocs.yml index affaf41..cb54a20 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ plugins: rendering: show_root_heading: true heading_level: 1 + show_source: false watch: - src - autolinks From 0ff323901a84b6ed91bc8d6dc0fcd6214c23f859 Mon Sep 17 00:00:00 2001 From: Dom Batten Date: Thu, 18 Nov 2021 14:34:40 +0000 Subject: [PATCH 5/6] remove mypy ignore --- src/phylm/clients/tmdb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/phylm/clients/tmdb.py b/src/phylm/clients/tmdb.py index 6b977f3..d7da3d0 100644 --- a/src/phylm/clients/tmdb.py +++ b/src/phylm/clients/tmdb.py @@ -6,7 +6,7 @@ from requests import Session -class TmdbClient(Session): +class TmdbClient: """Class to abstract to the Tmdb API.""" def __init__(self, api_key: str) -> None: @@ -16,6 +16,7 @@ def __init__(self, api_key: str) -> None: api_key: an api_key for authentication """ super().__init__() + self.session = Session() self.api_key = api_key self._base = "https://api.themoviedb.org/3" @@ -35,6 +36,6 @@ def search_movies(self, query: str) -> List[Dict[str, Any]]: "include_adult": False, "region": "GB", } - res = self.get(f"{self._base}/search/movie", params=payload) # type: ignore + res = self.session.get(f"{self._base}/search/movie", params=payload) results: List[Dict[str, Any]] = res.json()["results"] return results From 27e5c0e946420b49c6af114d76b9425f33ed1276 Mon Sep 17 00:00:00 2001 From: Dom Batten Date: Thu, 18 Nov 2021 14:37:46 +0000 Subject: [PATCH 6/6] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8df638a..f074a2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "phylm" -version = "4.1.0" +version = "4.2.0" description = "Phylm" authors = ["Dom Batten "] license = "MIT"