From 670003d5d77a5056d51c90c7a8dfe3201fa210ee Mon Sep 17 00:00:00 2001 From: Mitch Woollatt <21175347+MitchellAW@users.noreply.github.com> Date: Wed, 27 May 2026 21:05:27 +0930 Subject: [PATCH 1/6] Support optional params in endpoints --- compuglobal/api/endpoint.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/compuglobal/api/endpoint.py b/compuglobal/api/endpoint.py index 55e1e67..71d1512 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 + 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 @@ -65,6 +67,7 @@ class Endpoint: path: str method: RequestMethod = RequestMethod.GET query_params: frozenset[str] = frozenset() + optional_query_params: frozenset[str] = frozenset() body_model: type[BaseModel] | None = None def build_url( @@ -179,7 +182,7 @@ def validate_query(self, query: dict[str, Any]) -> None: """ missing = set(self.query_params - query.keys()) - unexpected = query.keys() - self.query_params + unexpected = query.keys() - (self.query_params | self.optional_query_params) if missing: raise ValueError( From 84e8a5007aa6f0d1080f8e52b5d781e05fe23034 Mon Sep 17 00:00:00 2001 From: Mitch Woollatt <21175347+MitchellAW@users.noreply.github.com> Date: Wed, 27 May 2026 21:05:53 +0930 Subject: [PATCH 2/6] Add optional smin/smax query params to endpoints --- compuglobal/api/discovery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compuglobal/api/discovery.py b/compuglobal/api/discovery.py index 12bef83..8470c9b 100644 --- a/compuglobal/api/discovery.py +++ b/compuglobal/api/discovery.py @@ -37,6 +37,7 @@ class DiscoveryAPI: RANDOM = Endpoint( path="/api/random", method=RequestMethod.GET, + optional_query_params=frozenset({"smin", "smax"}), ) NAVIGATOR = Endpoint( @@ -48,6 +49,7 @@ class DiscoveryAPI: path="/api/search", method=RequestMethod.GET, query_params=frozenset({"q"}), + optional_query_params=frozenset({"smin", "smax"}), ) FRAMES = Endpoint( From 55871b4c09180e38b8a094fcc9b185a1ab4808df Mon Sep 17 00:00:00 2001 From: Mitch Woollatt <21175347+MitchellAW@users.noreply.github.com> Date: Wed, 27 May 2026 21:08:41 +0930 Subject: [PATCH 3/6] Refactor query params to required_query_params --- compuglobal/api/discovery.py | 4 ++-- compuglobal/api/endpoint.py | 8 ++++---- compuglobal/api/media.py | 6 +++--- compuglobal/api/metadata.py | 2 +- tests/api/test_discovery.py | 12 ++++++------ tests/api/test_endpoint.py | 26 +++++++++++++------------- tests/api/test_media.py | 12 ++++++------ tests/api/test_metadata.py | 4 ++-- 8 files changed, 37 insertions(+), 37 deletions(-) diff --git a/compuglobal/api/discovery.py b/compuglobal/api/discovery.py index 8470c9b..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( @@ -48,7 +48,7 @@ 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"}), ) diff --git a/compuglobal/api/endpoint.py b/compuglobal/api/endpoint.py index 71d1512..92e1bf9 100644 --- a/compuglobal/api/endpoint.py +++ b/compuglobal/api/endpoint.py @@ -55,7 +55,7 @@ class Endpoint: 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 + 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 @@ -66,7 +66,7 @@ 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 @@ -181,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 | self.optional_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..351fb8c 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,7 +58,7 @@ 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() @@ -68,7 +68,7 @@ def test_discovery_navigator_expected_path() -> None: def test_discovery_navigator_expected_params() -> None: - params = DiscoveryAPI.NAVIGATOR.query_params + params = DiscoveryAPI.NAVIGATOR.required_query_params assert params == frozenset() @@ -78,7 +78,7 @@ 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"})) @@ -89,5 +89,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..1227e6b 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,19 @@ 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_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 +135,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 +171,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"})) From 03d6398faa0d591d45dca1ac53bfce5fd1b9809e Mon Sep 17 00:00:00 2001 From: Mitch Woollatt <21175347+MitchellAW@users.noreply.github.com> Date: Wed, 27 May 2026 21:26:31 +0930 Subject: [PATCH 4/6] Add some tests for validating optional params --- tests/api/test_discovery.py | 10 ++++++++++ tests/api/test_endpoint.py | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/api/test_discovery.py b/tests/api/test_discovery.py index 351fb8c..940d242 100644 --- a/tests/api/test_discovery.py +++ b/tests/api/test_discovery.py @@ -62,6 +62,11 @@ def test_discovery_random_expected_params() -> None: 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" @@ -82,6 +87,11 @@ def test_discovery_search_expected_params() -> None: 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") diff --git a/tests/api/test_endpoint.py b/tests/api/test_endpoint.py index 1227e6b..b27f3b3 100644 --- a/tests/api/test_endpoint.py +++ b/tests/api/test_endpoint.py @@ -109,6 +109,18 @@ def test_endpoint_build_encoded_url_exists() -> None: 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", required_query_params=frozenset({"a"})) with pytest.raises(ValueError, match="Missing query params"): From 685bad6b6dd2fa558f95bfdc200d3361ba23d14b Mon Sep 17 00:00:00 2001 From: Mitch Woollatt <21175347+MitchellAW@users.noreply.github.com> Date: Wed, 27 May 2026 21:43:48 +0930 Subject: [PATCH 5/6] Add season filters to search and random api calls --- compuglobal/aio.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) 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) From 83beaa888776bf06cae1297921a73f4ca1e067af Mon Sep 17 00:00:00 2001 From: Mitch Woollatt <21175347+MitchellAW@users.noreply.github.com> Date: Thu, 28 May 2026 11:39:25 +0930 Subject: [PATCH 6/6] Add integration test for season minimum/maximum --- tests/integration/test_aio_integration.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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: