diff --git a/compuglobal/aio.py b/compuglobal/aio.py index afed884..e83a849 100644 --- a/compuglobal/aio.py +++ b/compuglobal/aio.py @@ -91,13 +91,22 @@ async def get_screencap( caption = await self.client.handle_request(request) return Screencap.model_validate(caption) - async def search(self, search_text: str) -> list[FrameResult]: + async def search( + self, + search_text: str, + season_minimum: int | None = None, + season_maximum: int | None = None, + ) -> list[FrameResult]: """Perform a search of the given search text and returns a list of all the Frames. Parameters ---------- search_text : str The search text to query + season_minimum: int | None, optional + The minimum season allowed in the search results + season_maximum: int | None, optional + The maximum season allowed in the search results Returns ------- @@ -110,9 +119,11 @@ async def search(self, search_text: str) -> list[FrameResult]: Raises an error if no search results are found """ - params = {"q": search_text} + optional_params = {"smin": season_minimum, "smax": season_maximum} + query = {"q": search_text} + query |= {k: v for k, v in optional_params.items() if v is not None} - request = self.discovery.SEARCH.build_request(self.client.base_url, query=params) + request = self.discovery.SEARCH.build_request(self.client.base_url, query=query) search_results = await self.client.handle_request(request) if len(search_results) > 0: @@ -120,13 +131,22 @@ async def search(self, search_text: str) -> list[FrameResult]: raise NoSearchResultsFoundError - async def search_for_screencap(self, search_text: str) -> Screencap: + async def search_for_screencap( + self, + search_text: str, + season_minimum: int | None = None, + season_maximum: int | None = None, + ) -> Screencap: """Perform a search of the given search text and returns the top result. Parameters ---------- search_text : str The search text to query + season_minimum: int | None, optional + The minimum season allowed in the search + season_maximum: int | None, optional + The maximum season allowed in the search Returns ------- @@ -134,11 +154,15 @@ async def search_for_screencap(self, search_text: str) -> Screencap: The screencap of the top search result """ - search_results = await self.search(search_text) + search_results = await self.search(search_text, season_minimum=season_minimum, season_maximum=season_maximum) result = search_results[0] return await self.get_screencap(result.key, result.timestamp) - async def get_random_screencap(self) -> Screencap: + async def get_random_screencap( + self, + season_minimum: int | None = None, + season_maximum: int | None = None, + ) -> Screencap: """Get a random TV Show screencap. Returns @@ -147,7 +171,9 @@ async def get_random_screencap(self) -> Screencap: A random screencap object. """ - request = self.discovery.RANDOM.build_request(self.client.base_url) + optional_params = {"smin": season_minimum, "smax": season_maximum} + query = {k: v for k, v in optional_params.items() if v is not None} + request = self.discovery.RANDOM.build_request(self.client.base_url, query=query) random = await self.client.handle_request(request) return Screencap.model_validate(random) diff --git a/compuglobal/api/discovery.py b/compuglobal/api/discovery.py index 12bef83..07d181d 100644 --- a/compuglobal/api/discovery.py +++ b/compuglobal/api/discovery.py @@ -26,7 +26,7 @@ class DiscoveryAPI: CAPTION = Endpoint( path="/api/caption", method=RequestMethod.GET, - query_params=frozenset({"e", "t", "nearby"}), + required_query_params=frozenset({"e", "t", "nearby"}), ) DISCOVER = Endpoint( @@ -37,6 +37,7 @@ class DiscoveryAPI: RANDOM = Endpoint( path="/api/random", method=RequestMethod.GET, + optional_query_params=frozenset({"smin", "smax"}), ) NAVIGATOR = Endpoint( @@ -47,7 +48,8 @@ class DiscoveryAPI: SEARCH = Endpoint( path="/api/search", method=RequestMethod.GET, - query_params=frozenset({"q"}), + required_query_params=frozenset({"q"}), + optional_query_params=frozenset({"smin", "smax"}), ) FRAMES = Endpoint( diff --git a/compuglobal/api/endpoint.py b/compuglobal/api/endpoint.py index 55e1e67..92e1bf9 100644 --- a/compuglobal/api/endpoint.py +++ b/compuglobal/api/endpoint.py @@ -52,11 +52,13 @@ class Endpoint: Attributes ---------- path: str - The url path + The url path containing any path parameter names method: RequestMethod, optional The HTTP RequestMethod for the request (GET/POST/PUT) - query_params: dict[str, Any] | None, optional - The query parameters to use in the request + required_query_params: dict[str, Any] | None, optional + The required query parameters to use in the request + optional_query_params: dict[str, Any] | None, optional + Optional query params that can be used in the request body_model: type[BaseModel], optional The pydantic model for the json body @@ -64,7 +66,8 @@ class Endpoint: path: str method: RequestMethod = RequestMethod.GET - query_params: frozenset[str] = frozenset() + required_query_params: frozenset[str] = frozenset() + optional_query_params: frozenset[str] = frozenset() body_model: type[BaseModel] | None = None def build_url( @@ -178,8 +181,8 @@ def validate_query(self, query: dict[str, Any]) -> None: Raises error if contains missing or unexpected params from the definition of the endpoint """ - missing = set(self.query_params - query.keys()) - unexpected = query.keys() - self.query_params + missing = set(self.required_query_params - query.keys()) + unexpected = query.keys() - (self.required_query_params | self.optional_query_params) if missing: raise ValueError( diff --git a/compuglobal/api/media.py b/compuglobal/api/media.py index 986e210..1e4e685 100644 --- a/compuglobal/api/media.py +++ b/compuglobal/api/media.py @@ -33,13 +33,13 @@ class MediaAPI: COMIC_PANEL = Endpoint( path="/comic/img", method=RequestMethod.GET, - query_params=frozenset({"b64"}), + required_query_params=frozenset({"b64"}), ) COMIC_STRIP = Endpoint( path="/comic/img", method=RequestMethod.GET, - query_params=frozenset({"b64", "layout"}), + required_query_params=frozenset({"b64", "layout"}), ) RENDER_GIF = Endpoint( @@ -56,7 +56,7 @@ class MediaAPI: DETECT_LOOP = Endpoint( path="/api/detect-loop", method=RequestMethod.GET, - query_params=frozenset( + required_query_params=frozenset( {"episode", "start", "end"}, ), ) diff --git a/compuglobal/api/metadata.py b/compuglobal/api/metadata.py index 17ea7fb..e741fa6 100644 --- a/compuglobal/api/metadata.py +++ b/compuglobal/api/metadata.py @@ -23,5 +23,5 @@ class MetadataAPI: TRANSCRIPT = Endpoint( path="/api/transcript", method=RequestMethod.GET, - query_params=frozenset({"e", "t"}), + required_query_params=frozenset({"e", "t"}), ) diff --git a/tests/api/test_discovery.py b/tests/api/test_discovery.py index 2c65292..940d242 100644 --- a/tests/api/test_discovery.py +++ b/tests/api/test_discovery.py @@ -38,7 +38,7 @@ def test_discovery_caption_expected_path() -> None: def test_discovery_caption_expected_params() -> None: - params = DiscoveryAPI.CAPTION.query_params + params = DiscoveryAPI.CAPTION.required_query_params assert params == snapshot(frozenset({"e", "nearby", "t"})) @@ -48,7 +48,7 @@ def test_discovery_discover_expected_path() -> None: def test_discovery_discover_expected_params() -> None: - params = DiscoveryAPI.DISCOVER.query_params + params = DiscoveryAPI.DISCOVER.required_query_params assert params == frozenset() @@ -58,17 +58,22 @@ def test_discovery_random_expected_path() -> None: def test_discovery_random_expected_params() -> None: - params = DiscoveryAPI.RANDOM.query_params + params = DiscoveryAPI.RANDOM.required_query_params assert params == frozenset() +def test_discovery_random_optional_params() -> None: + params = DiscoveryAPI.RANDOM.optional_query_params + assert params == snapshot(frozenset({"smax", "smin"})) + + def test_discovery_navigator_expected_path() -> None: path = DiscoveryAPI.NAVIGATOR.path assert path == "/api/navigator" def test_discovery_navigator_expected_params() -> None: - params = DiscoveryAPI.NAVIGATOR.query_params + params = DiscoveryAPI.NAVIGATOR.required_query_params assert params == frozenset() @@ -78,10 +83,15 @@ def test_discovery_search_expected_path() -> None: def test_discovery_search_expected_params() -> None: - params = DiscoveryAPI.SEARCH.query_params + params = DiscoveryAPI.SEARCH.required_query_params assert params == snapshot(frozenset({"q"})) +def test_discovery_search_optional_params() -> None: + params = DiscoveryAPI.SEARCH.optional_query_params + assert params == snapshot(frozenset({"smax", "smin"})) + + def test_discovery_frames_expected_path() -> None: path = DiscoveryAPI.FRAMES.path assert path.startswith("/api/frames") @@ -89,5 +99,5 @@ def test_discovery_frames_expected_path() -> None: def test_discovery_frames_expected_params() -> None: - params = DiscoveryAPI.FRAMES.query_params + params = DiscoveryAPI.FRAMES.required_query_params assert params == frozenset() diff --git a/tests/api/test_endpoint.py b/tests/api/test_endpoint.py index fd29816..b27f3b3 100644 --- a/tests/api/test_endpoint.py +++ b/tests/api/test_endpoint.py @@ -48,7 +48,7 @@ def test_prepared_request_params_is_none() -> None: def test_endpoint_defaults() -> None: endpoint = Endpoint(path="/example") - expected = Endpoint(path="/example", method=RequestMethod.GET, query_params=frozenset(), body_model=None) + expected = Endpoint(path="/example", method=RequestMethod.GET, required_query_params=frozenset(), body_model=None) assert endpoint == expected @@ -59,16 +59,16 @@ class ExampleModel(BaseModel): endpoint = Endpoint( path="/example", method=RequestMethod.POST, - query_params=frozenset({"foo"}), + required_query_params=frozenset({"foo"}), body_model=ExampleModel, ) assert endpoint.path == "/example" - assert endpoint.query_params == frozenset({"foo"}) + assert endpoint.required_query_params == frozenset({"foo"}) assert endpoint.body_model == ExampleModel def test_endpoint_build_url() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"a"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"})) url = endpoint.build_url("https://example.com", query={"a": 1}, path_params={}) assert url == "https://example.com/example" @@ -80,7 +80,7 @@ def test_endpoint_build_url_path_params() -> None: def test_endpoint_build_url_missing_or_unexpected_params() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"a"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"})) with pytest.raises(ValueError, match="Missing query params"): endpoint.build_url("https://example.com", query={"b": 1}, path_params={}) @@ -98,19 +98,31 @@ def test_endpoint_build_url_missing_or_unexpected_path_params() -> None: def test_endpoint_build_encoded_url() -> None: - endpoint = Endpoint(path="/path/{key}", query_params=frozenset({"q"})) + endpoint = Endpoint(path="/path/{key}", required_query_params=frozenset({"q"})) url = endpoint.build_encoded_url("https://example.com", query={"q": 1}, path_params={"key": "search"}) assert url == "https://example.com/path/search?q=1" def test_endpoint_build_encoded_url_exists() -> None: - endpoint = Endpoint(path="/path/", query_params=frozenset()) + endpoint = Endpoint(path="/path/", required_query_params=frozenset()) url = endpoint.build_encoded_url("https://example.com", query={}, path_params={}) assert url is not None +def test_endpoint_build_encoded_url_with_unused_optional_params() -> None: + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"}), optional_query_params=frozenset({"b"})) + url = endpoint.build_encoded_url("https://example.com", query={"a": 1}, path_params={}) + assert url == "https://example.com/example?a=1" + + +def test_endpoint_build_encoded_url_with_used_optional_params() -> None: + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"}), optional_query_params=frozenset({"b"})) + url = endpoint.build_encoded_url("https://example.com", query={"a": 1, "b": 2}, path_params={}) + assert url == "https://example.com/example?a=1&b=2" + + def test_endpoint_build_encoded_url_missing_or_unexpected_params() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"a"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"})) with pytest.raises(ValueError, match="Missing query params"): endpoint.build_encoded_url("https://example.com", query={}, path_params={}) @@ -135,20 +147,20 @@ def test_endpoint_build_request() -> None: def test_endpoint_build_request_overrides() -> None: - endpoint = Endpoint(path="/path/{key}", query_params=frozenset({"q"})) + endpoint = Endpoint(path="/path/{key}", required_query_params=frozenset({"q"})) request = endpoint.build_request("https://example.com", query={"q": 1}, path_params={"key": "search"}) expected = PreparedRequest(url="https://example.com/path/search", params={"q": 1}, body=None) assert request == expected def test_endpoint_build_request_missing_params() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"q"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"q"})) with pytest.raises(ValueError, match="Missing query params"): endpoint.build_request("https://example.com", query={}, path_params={}) def test_endpoint_build_request_unexpected_params() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"q"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"q"})) with pytest.raises(ValueError, match="Unexpected query params"): endpoint.build_request("https://example.com", query={"q": 1, "_unexpected_": True}, path_params={}) @@ -171,13 +183,13 @@ def test_endpoint_validate_query() -> None: def test_endpoint_validate_query_missing_query_params() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"a"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"})) with pytest.raises(ValueError, match="Missing query params"): endpoint.validate_query(query={}) def test_endpoint_validate_query_unexpected_query_params() -> None: - endpoint = Endpoint(path="/example", query_params=frozenset({"a"})) + endpoint = Endpoint(path="/example", required_query_params=frozenset({"a"})) with pytest.raises(ValueError, match="Unexpected query params"): endpoint.validate_query(query={"a": 1, "b": 1}) diff --git a/tests/api/test_media.py b/tests/api/test_media.py index 3f32e7e..185ad3c 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -44,7 +44,7 @@ def test_media_image_expected_path() -> None: def test_media_image_expected_params() -> None: - params = MediaAPI.IMAGE.query_params + params = MediaAPI.IMAGE.required_query_params assert params == frozenset() @@ -54,7 +54,7 @@ def test_media_comic_panel_expected_path() -> None: def test_media_comic_panel_expected_params() -> None: - params = MediaAPI.COMIC_PANEL.query_params + params = MediaAPI.COMIC_PANEL.required_query_params assert params == snapshot(frozenset({"b64"})) @@ -64,7 +64,7 @@ def test_media_comic_strip_expected_path() -> None: def test_media_comic_strip_expected_params() -> None: - params = MediaAPI.COMIC_STRIP.query_params + params = MediaAPI.COMIC_STRIP.required_query_params assert params == snapshot(frozenset({"b64", "layout"})) @@ -74,7 +74,7 @@ def test_media_render_gif_expected_path() -> None: def test_media_render_gif_expected_params() -> None: - params = MediaAPI.RENDER_GIF.query_params + params = MediaAPI.RENDER_GIF.required_query_params assert params == frozenset() @@ -89,7 +89,7 @@ def test_media_render_mp4_expected_path() -> None: def test_media_render_mp4_expected_params() -> None: - params = MediaAPI.RENDER_MP4.query_params + params = MediaAPI.RENDER_MP4.required_query_params assert params == frozenset() @@ -104,5 +104,5 @@ def test_media_detect_loop_expected_path() -> None: def test_media_detect_loop_expected_params() -> None: - params = MediaAPI.DETECT_LOOP.query_params + params = MediaAPI.DETECT_LOOP.required_query_params assert params == snapshot(frozenset({"end", "episode", "start"})) diff --git a/tests/api/test_metadata.py b/tests/api/test_metadata.py index 012dbcc..c429f00 100644 --- a/tests/api/test_metadata.py +++ b/tests/api/test_metadata.py @@ -25,7 +25,7 @@ def test_metadata_episode_has_expected_path() -> None: def test_metadata_episode_has_expected_params() -> None: - params = MetadataAPI.EPISODE.query_params + params = MetadataAPI.EPISODE.required_query_params assert params == frozenset() @@ -35,5 +35,5 @@ def test_metadata_transcript_has_expected_path() -> None: def test_metadata_transcript_has_expected_params() -> None: - params = MetadataAPI.TRANSCRIPT.query_params + params = MetadataAPI.TRANSCRIPT.required_query_params assert params == snapshot(frozenset({"e", "t"})) diff --git a/tests/integration/test_aio_integration.py b/tests/integration/test_aio_integration.py index b25b4f0..2f18d81 100644 --- a/tests/integration/test_aio_integration.py +++ b/tests/integration/test_aio_integration.py @@ -104,6 +104,15 @@ async def test_api_search(api: AsyncCompuGlobalAPI) -> None: assert isinstance(result, FrameResult) +@pytest.mark.asyncio +@pytest.mark.integration +async def test_api_search_filtered(api: AsyncCompuGlobalAPI) -> None: + results = await api.search("the", season_minimum=1, season_maximum=1) + assert len(results) > 0 + for result in results: + assert result.key.startswith("S01") + + @pytest.mark.asyncio @pytest.mark.integration async def test_api_search_for_screencap(api: AsyncCompuGlobalAPI) -> None: