From 4e8206a0e02af0e6a4f4a22ab87ee454e7390d56 Mon Sep 17 00:00:00 2001 From: Jim Stuhlmacher Date: Sat, 2 Nov 2024 13:18:23 -0500 Subject: [PATCH 1/2] add seasons function --- README.md | 31 +- examples/seasons_output.py | 91 +++++ src/simplejustwatchapi/justwatch.py | 22 ++ src/simplejustwatchapi/query.py | 159 ++++++++ test/simplejustwatchapi/test_justwatch.py | 12 +- test/simplejustwatchapi/test_parser.py | 428 ++++++++++++++++++++++ test/simplejustwatchapi/test_request.py | 80 ++++ 7 files changed, 821 insertions(+), 2 deletions(-) create mode 100644 examples/seasons_output.py diff --git a/README.md b/README.md index b51f226..3bb21e1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A simple unofficial JustWatch Python API which uses [`GraphQL`](https://graphql. * [Usage](#usage) * [Search](#search) * [Details](#details) + * [Seasons](#seasons) * [Offers for countries](#offers-for-countries) * [Return data structures](#return-data-structures) * [Locale, language, country](#locale-language-country) @@ -35,10 +36,11 @@ pip install simple-justwatch-python-api ## Usage -This Python API has 3 functions: +This Python API has 4 functions: - `search` - search for entries based on title - `details` - get details for entry based on its node ID + - `seasons` - get season details for show entry based on its node ID - `offers_for_countries` - get offers for entry based on its node ID, can look for offers in multiple countries @@ -112,6 +114,33 @@ Returned value is a single [`MediaEntry`](#return-data-structures) object. Example command and its output is in [`examples/details_output.py`](examples/details_output.py). +### Seasons + +Seasons function allows for looking up season and episode information for a single show entry via its node ID. +Node ID can be taken from output of the [`search`](#search) command. + + +```python +from simplejustwatchapi.justwatch import seasons + +results = seasons("nodeID", "US", "en") +``` + +Only the first argument is required - the node ID of a show element to look up details for. + +| | Argument | Type | Required | Default value | Description | +|---|-------------|--------|----------|---------------|--------------------------------------------------------| +| 1 | `node_id` | `str` | **YES** | - | Node ID to look up | +| 2 | `country` | `str` | NO | `"US"` | Country to search for offers | +| 3 | `language` | `str` | NO | `"en"` | Language of responses | + +General usage of these arguments matches the [`search`](#search) command. + +Returned value is a single [`SeasonsEntry`](#return-data-structures) object. + +Example command and its output is in [`examples/seasons_output.py`](examples/seasons_output.py). + + ### Offers for countries This function allows looking up offers for entry by given node ID. diff --git a/examples/seasons_output.py b/examples/seasons_output.py new file mode 100644 index 0000000..1dda21e --- /dev/null +++ b/examples/seasons_output.py @@ -0,0 +1,91 @@ +# Output from command: +# seasons("ts85167", "US", "en") + +from simplejustwatchapi.query import ( + SeasonsEntry, + Season, + Episode, +) + +result = SeasonsEntry( + entry_id='ts85167', + seasons=[ + Season(seasonNumber=1, + episodes=[ + Episode(seasonNumber=1, episodeNumber=1, title='Pilot'), + Episode(seasonNumber=1, episodeNumber=2, title='City Council'), + Episode(seasonNumber=1, episodeNumber=3, title='Werewolf Feud'), + Episode(seasonNumber=1, episodeNumber=4, title='Manhattan Night Club'), + Episode(seasonNumber=1, episodeNumber=5, title='Animal Control'), + Episode(seasonNumber=1, episodeNumber=6, title="Baron's Night Out"), + Episode(seasonNumber=1, episodeNumber=7, title='The Trial'), + Episode(seasonNumber=1, episodeNumber=8, title='Citizenship'), + Episode(seasonNumber=1, episodeNumber=9, title='The Orgy'), + Episode(seasonNumber=1, episodeNumber=10, title='Ancestry') + ]), + Season(seasonNumber=2, + episodes=[ + Episode(seasonNumber=2, episodeNumber=1, title='Resurrection'), + Episode(seasonNumber=2, episodeNumber=2, title='Ghosts'), + Episode(seasonNumber=2, episodeNumber=3, title='Brain Scramblies'), + Episode(seasonNumber=2, episodeNumber=4, title='The Curse'), + Episode(seasonNumber=2, episodeNumber=5, title="Colin's Promotion"), + Episode(seasonNumber=2, episodeNumber=6, title='On the Run'), + Episode(seasonNumber=2, episodeNumber=7, title='The Return'), + Episode(seasonNumber=2, episodeNumber=8, title='Collaboration'), + Episode(seasonNumber=2, episodeNumber=9, title='Witches'), + Episode(seasonNumber=2, episodeNumber=10, title='Nouveau Théâtre des Vampires') + ]), + Season(seasonNumber=3, + episodes=[ + Episode(seasonNumber=3, episodeNumber=1, title='The Prisoner'), + Episode(seasonNumber=3, episodeNumber=2, title='The Cloak of Duplication'), + Episode(seasonNumber=3, episodeNumber=3, title='Gail'), + Episode(seasonNumber=3, episodeNumber=4, title='The Casino'), + Episode(seasonNumber=3, episodeNumber=5, title='The Chamber of Judgement'), + Episode(seasonNumber=3, episodeNumber=6, title='The Escape'), + Episode(seasonNumber=3, episodeNumber=7, title='The Siren'), + Episode(seasonNumber=3, episodeNumber=8, title='The Wellness Center'), + Episode(seasonNumber=3, episodeNumber=9, title='A Farewell'), + Episode(seasonNumber=3, episodeNumber=10, title='The Portrait') + ]), + Season(seasonNumber=4, + episodes=[Episode(seasonNumber=4, episodeNumber=1, title='Reunited'), + Episode(seasonNumber=4, episodeNumber=2, title='The Lamp'), + Episode(seasonNumber=4, episodeNumber=3, title='The Grand Opening'), + Episode(seasonNumber=4, episodeNumber=4, title='The Night Market'), + Episode(seasonNumber=4, episodeNumber=5, title='Private School'), + Episode(seasonNumber=4, episodeNumber=6, title='The Wedding'), + Episode(seasonNumber=4, episodeNumber=7, title='Pine Barrens'), + Episode(seasonNumber=4, episodeNumber=8, title='Go Flip Yourself'), + Episode(seasonNumber=4, episodeNumber=9, title='Freddie'), + Episode(seasonNumber=4, episodeNumber=10, title='Sunrise, Sunset') + ]), + Season(seasonNumber=5, + episodes=[Episode(seasonNumber=5, episodeNumber=1, title='The Mall'), + Episode(seasonNumber=5, episodeNumber=2, title='A Night Out with the Guys'), + Episode(seasonNumber=5, episodeNumber=3, title='Pride Parade'), + Episode(seasonNumber=5, episodeNumber=4, title='The Campaign'), + Episode(seasonNumber=5, episodeNumber=5, title='Local News'), + Episode(seasonNumber=5, episodeNumber=6, title='Urgent Care'), + Episode(seasonNumber=5, episodeNumber=7, title='Hybrid Creatures'), + Episode(seasonNumber=5, episodeNumber=8, title='The Roast'), + Episode(seasonNumber=5, episodeNumber=9, title='A Weekend at Morrigan Manor'), + Episode(seasonNumber=5, episodeNumber=10, title='Exit Interview') + ]), + Season(seasonNumber=6, + episodes=[ + Episode(seasonNumber=6, episodeNumber=1, title='Episode 1'), + Episode(seasonNumber=6, episodeNumber=2, title='Episode 2'), + Episode(seasonNumber=6, episodeNumber=3, title='Episode 3'), + Episode(seasonNumber=6, episodeNumber=4, title='Episode 4'), + Episode(seasonNumber=6, episodeNumber=5, title='Episode 5'), + Episode(seasonNumber=6, episodeNumber=6, title='Episode 6'), + Episode(seasonNumber=6, episodeNumber=7, title='Episode 7'), + Episode(seasonNumber=6, episodeNumber=8, title='Episode 8'), + Episode(seasonNumber=6, episodeNumber=9, title='Episode 9'), + Episode(seasonNumber=6, episodeNumber=10, title='Episode 10'), + Episode(seasonNumber=6, episodeNumber=11, title='Episode 11') + ]) + ] +) diff --git a/src/simplejustwatchapi/justwatch.py b/src/simplejustwatchapi/justwatch.py index bfdc532..7c36901 100644 --- a/src/simplejustwatchapi/justwatch.py +++ b/src/simplejustwatchapi/justwatch.py @@ -4,11 +4,14 @@ from simplejustwatchapi.query import ( MediaEntry, + SeasonsEntry, Offer, parse_details_response, + parse_seasons_response, parse_offers_for_countries_response, parse_search_response, prepare_details_request, + prepare_seasons_request, prepare_offers_for_countries_request, prepare_search_request, ) @@ -67,6 +70,25 @@ def details( return parse_details_response(response.json()) +def seasons( + node_id: str, country: str = "US", language: str = "en" +) -> SeasonsEntry: + """Get show seasons for a given ID. + + Args: + node_id: ID of entry to look up + country: country to search for offers, ``US`` by default + language: language of responses, ``en`` by default + + Returns: + ``SeasonsEntry`` NamedTuple with data about requested entry. + """ + request = prepare_seasons_request(node_id, country, language) + response = post(_GRAPHQL_API_URL, json=request) + response.raise_for_status() + return parse_seasons_response(response.json()) + + def offers_for_countries( node_id: str, countries: set[str], language: str = "en", best_only: bool = True ) -> dict[str, list[Offer]]: diff --git a/src/simplejustwatchapi/query.py b/src/simplejustwatchapi/query.py index eededd7..db34ffe 100644 --- a/src/simplejustwatchapi/query.py +++ b/src/simplejustwatchapi/query.py @@ -6,6 +6,47 @@ _DETAILS_URL = "https://justwatch.com" _IMAGES_URL = "https://images.justwatch.com" +_GRAPHQL_SEASONS_QUERY = """ +fragment Episode on Episode { + __typename + id + content(country: $country, language: $language) { + title + seasonNumber + episodeNumber + } +} +fragment Season on Season { + __typename + id + content(country: $country, language: $language) { + seasonNumber + } + episodes { + ...Episode + } +} +fragment Show on Show { + __typename + id + seasons { + ...Season + } +} +fragment Node on Node { + __typename + id + ...Episode + ...Season + ...Show +} +query GetNodeById($nodeId: ID!, $country: Country!, $language: Language!) { + node(id: $nodeId) { + ...Node + } +} +""" + _GRAPHQL_DETAILS_QUERY = """ query GetTitleNode( $nodeId: ID!, @@ -385,6 +426,39 @@ class MediaEntry(NamedTuple): """List of available offers for this entry, empty if there are no available offers.""" +class Episode(NamedTuple): + """Parsed response from JustWatch GraphQL API for "GetNodeById" query for an episode.""" + + seasonNumber: int + """Season Number of show episode.""" + + episodeNumber: int + """Episode Number of show episode.""" + + title: str + """Title of show episode.""" + + +class Season(NamedTuple): + """Parsed response from JustWatch GraphQL API for "GetNodeById" query for a season.""" + + seasonNumber: int + """Season Number of show.""" + + episodes: list[Episode] + """List of season's episodes. """ + + +class SeasonsEntry(NamedTuple): + """Parsed response from JustWatch GraphQL API for "GetNodeById" query for show seasons.""" + + entry_id: str + """Entry ID, contains type code and numeric ID.""" + + seasons: list[Season] + """List of show seasons. """ + + def prepare_search_request( title: str, country: str, language: str, count: int, best_only: bool ) -> dict: @@ -493,6 +567,52 @@ def parse_details_response(json: any) -> MediaEntry | None: return _parse_entry(json["data"]["node"]) if "errors" not in json else None +def prepare_seasons_request(node_id: str, country: str, language: str) -> dict: + """Prepare a seasons request for specified node ID to JustWatch GraphQL API. + Creates a ``GetNodeById`` GraphQL query. + + Country code should be two uppercase letters, however it will be auto-converted to uppercase. + + Meant to be used together with :func:`parse_seasons_response`. + + Args: + node_id: node ID of entry to get seasons for + country: country to search for offers + language: language of responses + + Returns: + JSON/dict with GraphQL POST body + """ + _assert_country_code_is_valid(country) + return { + "operationName": "GetNodeById", + "variables": { + "nodeId": node_id, + "language": language, + "country": country.upper(), + }, + "query": _GRAPHQL_SEASONS_QUERY, + } + +def parse_seasons_response(json: any) -> SeasonsEntry | None: + """Parse response from seasons query from JustWatch GraphQL API. + Parses response for ``GetNodeById`` query. + + If API responded with an internal error (mostly due to not found node ID), + then ``None`` will be returned instead. + + Meant to be used together with :func:`prepare_seasons_request`. + + Args: + json: JSON returned by JustWatch GraphQL API + + Returns: + Parsed received JSON as a ``SeasonsEntry`` NamedTuple, + or ``None`` in case data for a given node ID was not found + """ + return _parse_seasons(json["data"]["node"]) if "errors" not in json else None + + def prepare_offers_for_countries_request( node_id: str, countries: set[str], language: str, best_only: bool ) -> dict: @@ -617,6 +737,45 @@ def _parse_entry(json: any) -> MediaEntry: ) +def _parse_seasons(json: any) -> SeasonsEntry: + if not json: + return None + entry_id = json.get("id") + seasons = [_parse_season(edge) for edge in json.get("seasons", [])] + + return SeasonsEntry( + entry_id, + seasons, + ) + + +def _parse_season(json: any) -> Season: + if not json: + return None + content = json.get("content") + seasonNumber = content.get("seasonNumber") + episodes = [_parse_episode(edge["content"]) for edge in json.get("episodes", [])] + + return Season( + seasonNumber, + episodes, + ) + + +def _parse_episode(json: any) -> Episode: + if not json: + return None + seasonNumber = json.get("seasonNumber") + episodeNumber = json.get("episodeNumber") + title = json.get("title") + + return Episode( + seasonNumber, + episodeNumber, + title, + ) + + def _parse_scores(json: any) -> Scoring | None: if not json: return None diff --git a/test/simplejustwatchapi/test_justwatch.py b/test/simplejustwatchapi/test_justwatch.py index ab0f618..59734ec 100644 --- a/test/simplejustwatchapi/test_justwatch.py +++ b/test/simplejustwatchapi/test_justwatch.py @@ -2,12 +2,13 @@ from pytest import fixture -from simplejustwatchapi.justwatch import details, offers_for_countries, search +from simplejustwatchapi.justwatch import details, seasons, offers_for_countries, search JUSTWATCH_GRAPHQL_URL = "https://apis.justwatch.com/graphql" SEARCH_INPUT = ("TITLE", "COUNTRY", "LANGUAGE", 5, True) DETAILS_INPUT = ("NODE ID", "COUNTRY", "LANGUAGE", False) +SEASONS_INPUT = ("NODE ID", "COUNTRY", "LANGUAGE") OFFERS_COUNTRIES_INPUT = {"COUNTRY1", "COUNTRY2", "COUNTRY3"} OFFERS_INPUT = ("NODE ID", OFFERS_COUNTRIES_INPUT, "LANGUAGE", True) @@ -43,6 +44,15 @@ def test_details(requests_mock, parser_mock, httpx_post_mock): assert results == DUMMY_ENTRIES +@patch("simplejustwatchapi.justwatch.parse_seasons_response", return_value=DUMMY_ENTRIES) +@patch("simplejustwatchapi.justwatch.prepare_seasons_request", return_value=DUMMY_REQUEST) +def test_seasons(requests_mock, parser_mock, httpx_post_mock): + results = seasons(*SEASONS_INPUT) + requests_mock.assert_called_with(*SEASONS_INPUT) + parser_mock.assert_called_with(DUMMY_RESPONSE) + assert results == DUMMY_ENTRIES + + @patch( "simplejustwatchapi.justwatch.parse_offers_for_countries_response", return_value=DUMMY_ENTRIES ) diff --git a/test/simplejustwatchapi/test_parser.py b/test/simplejustwatchapi/test_parser.py index 90f8b40..ebf2366 100644 --- a/test/simplejustwatchapi/test_parser.py +++ b/test/simplejustwatchapi/test_parser.py @@ -3,11 +3,15 @@ from simplejustwatchapi.query import ( Interactions, MediaEntry, + SeasonsEntry, + Season, + Episode, Offer, OfferPackage, Scoring, StreamingCharts, parse_details_response, + parse_seasons_response, parse_offers_for_countries_response, parse_search_response, ) @@ -368,6 +372,418 @@ [], ) +PARSED_NODE_4 = SeasonsEntry( + entry_id='ts85167', + seasons=[ + Season(seasonNumber=1, + episodes=[ + Episode(seasonNumber=1, episodeNumber=1, title='Pilot'), + Episode(seasonNumber=1, episodeNumber=2, title='City Council'), + Episode(seasonNumber=1, episodeNumber=3, title='Werewolf Feud'), + Episode(seasonNumber=1, episodeNumber=4, title='Manhattan Night Club'), + Episode(seasonNumber=1, episodeNumber=5, title='Animal Control'), + Episode(seasonNumber=1, episodeNumber=6, title="Baron's Night Out"), + Episode(seasonNumber=1, episodeNumber=7, title='The Trial'), + Episode(seasonNumber=1, episodeNumber=8, title='Citizenship'), + Episode(seasonNumber=1, episodeNumber=9, title='The Orgy'), + Episode(seasonNumber=1, episodeNumber=10, title='Ancestry') + ]), + Season(seasonNumber=2, + episodes=[ + Episode(seasonNumber=2, episodeNumber=1, title='Resurrection'), + Episode(seasonNumber=2, episodeNumber=2, title='Ghosts'), + Episode(seasonNumber=2, episodeNumber=3, title='Brain Scramblies'), + Episode(seasonNumber=2, episodeNumber=4, title='The Curse'), + Episode(seasonNumber=2, episodeNumber=5, title="Colin's Promotion"), + Episode(seasonNumber=2, episodeNumber=6, title='On the Run'), + Episode(seasonNumber=2, episodeNumber=7, title='The Return'), + Episode(seasonNumber=2, episodeNumber=8, title='Collaboration'), + Episode(seasonNumber=2, episodeNumber=9, title='Witches'), + Episode(seasonNumber=2, episodeNumber=10, title='Nouveau Théâtre des Vampires') + ]), + Season(seasonNumber=3, + episodes=[ + Episode(seasonNumber=3, episodeNumber=1, title='The Prisoner'), + Episode(seasonNumber=3, episodeNumber=2, title='The Cloak of Duplication'), + Episode(seasonNumber=3, episodeNumber=3, title='Gail'), + Episode(seasonNumber=3, episodeNumber=4, title='The Casino'), + Episode(seasonNumber=3, episodeNumber=5, title='The Chamber of Judgement'), + Episode(seasonNumber=3, episodeNumber=6, title='The Escape'), + Episode(seasonNumber=3, episodeNumber=7, title='The Siren'), + Episode(seasonNumber=3, episodeNumber=8, title='The Wellness Center'), + Episode(seasonNumber=3, episodeNumber=9, title='A Farewell'), + Episode(seasonNumber=3, episodeNumber=10, title='The Portrait') + ]), + Season(seasonNumber=4, + episodes=[Episode(seasonNumber=4, episodeNumber=1, title='Reunited'), + Episode(seasonNumber=4, episodeNumber=2, title='The Lamp'), + Episode(seasonNumber=4, episodeNumber=3, title='The Grand Opening'), + Episode(seasonNumber=4, episodeNumber=4, title='The Night Market'), + Episode(seasonNumber=4, episodeNumber=5, title='Private School'), + Episode(seasonNumber=4, episodeNumber=6, title='The Wedding'), + Episode(seasonNumber=4, episodeNumber=7, title='Pine Barrens'), + Episode(seasonNumber=4, episodeNumber=8, title='Go Flip Yourself'), + Episode(seasonNumber=4, episodeNumber=9, title='Freddie'), + Episode(seasonNumber=4, episodeNumber=10, title='Sunrise, Sunset') + ]), + Season(seasonNumber=5, + episodes=[Episode(seasonNumber=5, episodeNumber=1, title='The Mall'), + Episode(seasonNumber=5, episodeNumber=2, title='A Night Out with the Guys'), + Episode(seasonNumber=5, episodeNumber=3, title='Pride Parade'), + Episode(seasonNumber=5, episodeNumber=4, title='The Campaign'), + Episode(seasonNumber=5, episodeNumber=5, title='Local News'), + Episode(seasonNumber=5, episodeNumber=6, title='Urgent Care'), + Episode(seasonNumber=5, episodeNumber=7, title='Hybrid Creatures'), + Episode(seasonNumber=5, episodeNumber=8, title='The Roast'), + Episode(seasonNumber=5, episodeNumber=9, title='A Weekend at Morrigan Manor'), + Episode(seasonNumber=5, episodeNumber=10, title='Exit Interview') + ]), + Season(seasonNumber=6, + episodes=[ + Episode(seasonNumber=6, episodeNumber=1, title='Episode 1'), + Episode(seasonNumber=6, episodeNumber=2, title='Episode 2'), + Episode(seasonNumber=6, episodeNumber=3, title='Episode 3'), + Episode(seasonNumber=6, episodeNumber=4, title='Episode 4'), + Episode(seasonNumber=6, episodeNumber=5, title='Episode 5'), + Episode(seasonNumber=6, episodeNumber=6, title='Episode 6'), + Episode(seasonNumber=6, episodeNumber=7, title='Episode 7'), + Episode(seasonNumber=6, episodeNumber=8, title='Episode 8'), + Episode(seasonNumber=6, episodeNumber=9, title='Episode 9'), + Episode(seasonNumber=6, episodeNumber=10, title='Episode 10'), + Episode(seasonNumber=6, episodeNumber=11, title='Episode 11') + ]) + ] +) +RESPONSE_NODE_4 = { + '__typename': 'Show', + 'id': 'ts85167', + 'seasons': [{'__typename': 'Season', + 'content': {'seasonNumber': 1}, + 'episodes': [{'__typename': 'Episode', + 'content': {'episodeNumber': 1, + 'seasonNumber': 1, + 'title': 'Pilot'}, + 'id': 'tse1631894'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 2, + 'seasonNumber': 1, + 'title': 'City Council'}, + 'id': 'tse1747980'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 3, + 'seasonNumber': 1, + 'title': 'Werewolf Feud'}, + 'id': 'tse1747981'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 4, + 'seasonNumber': 1, + 'title': 'Manhattan Night Club'}, + 'id': 'tse1747982'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 5, + 'seasonNumber': 1, + 'title': 'Animal Control'}, + 'id': 'tse1747983'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 6, + 'seasonNumber': 1, + 'title': "Baron's Night Out"}, + 'id': 'tse1769781'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 7, + 'seasonNumber': 1, + 'title': 'The Trial'}, + 'id': 'tse1777016'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 8, + 'seasonNumber': 1, + 'title': 'Citizenship'}, + 'id': 'tse1797942'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 9, + 'seasonNumber': 1, + 'title': 'The Orgy'}, + 'id': 'tse1797943'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 10, + 'seasonNumber': 1, + 'title': 'Ancestry'}, + 'id': 'tse1797944'}], + 'id': 'tss98319'}, + {'__typename': 'Season', + 'content': {'seasonNumber': 2}, + 'episodes': [{'__typename': 'Episode', + 'content': {'episodeNumber': 1, + 'seasonNumber': 2, + 'title': 'Resurrection'}, + 'id': 'tse3177541'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 2, + 'seasonNumber': 2, + 'title': 'Ghosts'}, + 'id': 'tse4343145'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 3, + 'seasonNumber': 2, + 'title': 'Brain Scramblies'}, + 'id': 'tse4366752'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 4, + 'seasonNumber': 2, + 'title': 'The Curse'}, + 'id': 'tse4366760'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 5, + 'seasonNumber': 2, + 'title': "Colin's Promotion"}, + 'id': 'tse4366763'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 6, + 'seasonNumber': 2, + 'title': 'On the Run'}, + 'id': 'tse4366757'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 7, + 'seasonNumber': 2, + 'title': 'The Return'}, + 'id': 'tse4366754'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 8, + 'seasonNumber': 2, + 'title': 'Collaboration'}, + 'id': 'tse4366751'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 9, + 'seasonNumber': 2, + 'title': 'Witches'}, + 'id': 'tse4366755'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 10, + 'seasonNumber': 2, + 'title': 'Nouveau Théâtre des ' + 'Vampires'}, + 'id': 'tse4366769'}], + 'id': 'tss200608'}, + {'__typename': 'Season', + 'content': {'seasonNumber': 3}, + 'episodes': [{'__typename': 'Episode', + 'content': {'episodeNumber': 1, + 'seasonNumber': 3, + 'title': 'The Prisoner'}, + 'id': 'tse5761373'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 2, + 'seasonNumber': 3, + 'title': 'The Cloak of Duplication'}, + 'id': 'tse5761374'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 3, + 'seasonNumber': 3, + 'title': 'Gail'}, + 'id': 'tse5863479'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 4, + 'seasonNumber': 3, + 'title': 'The Casino'}, + 'id': 'tse5863485'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 5, + 'seasonNumber': 3, + 'title': 'The Chamber of Judgement'}, + 'id': 'tse5863482'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 6, + 'seasonNumber': 3, + 'title': 'The Escape'}, + 'id': 'tse5863480'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 7, + 'seasonNumber': 3, + 'title': 'The Siren'}, + 'id': 'tse5863486'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 8, + 'seasonNumber': 3, + 'title': 'The Wellness Center'}, + 'id': 'tse5863484'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 9, + 'seasonNumber': 3, + 'title': 'A Farewell'}, + 'id': 'tse5863483'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 10, + 'seasonNumber': 3, + 'title': 'The Portrait'}, + 'id': 'tse5863481'}], + 'id': 'tss306291'}, + {'__typename': 'Season', + 'content': {'seasonNumber': 4}, + 'episodes': [{'__typename': 'Episode', + 'content': {'episodeNumber': 1, + 'seasonNumber': 4, + 'title': 'Reunited'}, + 'id': 'tse6569917'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 2, + 'seasonNumber': 4, + 'title': 'The Lamp'}, + 'id': 'tse6569916'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 3, + 'seasonNumber': 4, + 'title': 'The Grand Opening'}, + 'id': 'tse6569915'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 4, + 'seasonNumber': 4, + 'title': 'The Night Market'}, + 'id': 'tse6638945'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 5, + 'seasonNumber': 4, + 'title': 'Private School'}, + 'id': 'tse6638946'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 6, + 'seasonNumber': 4, + 'title': 'The Wedding'}, + 'id': 'tse6638941'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 7, + 'seasonNumber': 4, + 'title': 'Pine Barrens'}, + 'id': 'tse6638942'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 8, + 'seasonNumber': 4, + 'title': 'Go Flip Yourself'}, + 'id': 'tse6638947'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 9, + 'seasonNumber': 4, + 'title': 'Freddie'}, + 'id': 'tse6638944'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 10, + 'seasonNumber': 4, + 'title': 'Sunrise, Sunset'}, + 'id': 'tse6638943'}], + 'id': 'tss357215'}, + {'__typename': 'Season', + 'content': {'seasonNumber': 5}, + 'episodes': [{'__typename': 'Episode', + 'content': {'episodeNumber': 1, + 'seasonNumber': 5, + 'title': 'The Mall'}, + 'id': 'tse7422284'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 2, + 'seasonNumber': 5, + 'title': 'A Night Out with the Guys'}, + 'id': 'tse7422285'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 3, + 'seasonNumber': 5, + 'title': 'Pride Parade'}, + 'id': 'tse7482502'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 4, + 'seasonNumber': 5, + 'title': 'The Campaign'}, + 'id': 'tse7528347'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 5, + 'seasonNumber': 5, + 'title': 'Local News'}, + 'id': 'tse7528354'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 6, + 'seasonNumber': 5, + 'title': 'Urgent Care'}, + 'id': 'tse7528351'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 7, + 'seasonNumber': 5, + 'title': 'Hybrid Creatures'}, + 'id': 'tse7528350'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 8, + 'seasonNumber': 5, + 'title': 'The Roast'}, + 'id': 'tse7528352'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 9, + 'seasonNumber': 5, + 'title': 'A Weekend at Morrigan Manor'}, + 'id': 'tse7528353'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 10, + 'seasonNumber': 5, + 'title': 'Exit Interview'}, + 'id': 'tse7528349'}], + 'id': 'tss361882'}, + {'__typename': 'Season', + 'content': {'seasonNumber': 6}, + 'episodes': [{'__typename': 'Episode', + 'content': {'episodeNumber': 1, + 'seasonNumber': 6, + 'title': 'Episode 1'}, + 'id': 'tse7528348'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 2, + 'seasonNumber': 6, + 'title': 'Episode 2'}, + 'id': 'tse8544433'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 3, + 'seasonNumber': 6, + 'title': 'Episode 3'}, + 'id': 'tse8544439'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 4, + 'seasonNumber': 6, + 'title': 'Episode 4'}, + 'id': 'tse8544449'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 5, + 'seasonNumber': 6, + 'title': 'Episode 5'}, + 'id': 'tse8544457'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 6, + 'seasonNumber': 6, + 'title': 'Episode 6'}, + 'id': 'tse8544465'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 7, + 'seasonNumber': 6, + 'title': 'Episode 7'}, + 'id': 'tse8544476'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 8, + 'seasonNumber': 6, + 'title': 'Episode 8'}, + 'id': 'tse8544483'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 9, + 'seasonNumber': 6, + 'title': 'Episode 9'}, + 'id': 'tse8544494'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 10, + 'seasonNumber': 6, + 'title': 'Episode 10'}, + 'id': 'tse8544502'}, + {'__typename': 'Episode', + 'content': {'episodeNumber': 11, + 'seasonNumber': 6, + 'title': 'Episode 11'}, + 'id': 'tse8544521'}], + 'id': 'tss361883'} + ] +} + API_SEARCH_RESPONSE_JSON = { "data": { "popularTitles": { @@ -409,6 +825,18 @@ def test_parse_details_response(response_json: dict, expected_output: MediaEntry assert parsed_entries == expected_output +@mark.parametrize( + argnames=["response_json", "expected_output"], + argvalues=[ + ({"data": {"node": RESPONSE_NODE_4}}, PARSED_NODE_4), + ({"errors": [], "data": {"node": None}}, None), + ], +) +def test_parse_seasons_response(response_json: dict, expected_output: SeasonsEntry) -> None: + parsed_entries = parse_seasons_response(response_json) + assert parsed_entries == expected_output + + @mark.parametrize( argnames=["response_json", "countries", "expected_output"], argvalues=[ diff --git a/test/simplejustwatchapi/test_request.py b/test/simplejustwatchapi/test_request.py index 44f02bc..f118edd 100644 --- a/test/simplejustwatchapi/test_request.py +++ b/test/simplejustwatchapi/test_request.py @@ -2,6 +2,7 @@ from simplejustwatchapi.query import ( prepare_details_request, + prepare_seasons_request, prepare_offers_for_countries_request, prepare_search_request, ) @@ -25,6 +26,47 @@ } """ +GRAPHQL_SEASONS_QUERY = """ +fragment Episode on Episode { + __typename + id + content(country: $country, language: $language) { + title + seasonNumber + episodeNumber + } +} +fragment Season on Season { + __typename + id + content(country: $country, language: $language) { + seasonNumber + } + episodes { + ...Episode + } +} +fragment Show on Show { + __typename + id + seasons { + ...Season + } +} +fragment Node on Node { + __typename + id + ...Episode + ...Season + ...Show +} +query GetNodeById($nodeId: ID!, $country: Country!, $language: Language!) { + node(id: $nodeId) { + ...Node + } +} +""" + GRAPHQL_SEARCH_QUERY = """ query GetSearchTitles( $searchTitlesFilter: TitleFilter!, @@ -268,6 +310,44 @@ def test_prepare_details_request_asserts_on_invalid_country_code(invalid_code: s assert str(error.value) == expected_error_message +@mark.parametrize( + argnames=["node_id", "country", "language"], + argvalues=[ + ("NODE ID 1", "US", "language 1"), + ("NODE ID 1", "gb", "language 2"), + ], +) +def test_prepare_seasons_request( + node_id: str, country: str, language: str +) -> None: + expected_request = { + "operationName": "GetNodeById", + "variables": { + "nodeId": node_id, + "language": language, + "country": country.upper(), + }, + "query": GRAPHQL_SEASONS_QUERY, + } + request = prepare_seasons_request(node_id, country, language) + assert expected_request == request + + +@mark.parametrize( + argnames=["invalid_code"], + argvalues=[ + ("United Stated of America",), # too long + ("usa",), # too long + ("u",), # too short + ], +) +def test_prepare_seasons_request_asserts_on_invalid_country_code(invalid_code: str) -> None: + expected_error_message = f"Invalid country code: {invalid_code}, code must be 2 characters long" + with raises(AssertionError) as error: + prepare_seasons_request("", invalid_code, "") + assert str(error.value) == expected_error_message + + @mark.parametrize( argnames=["node_id", "countries", "language", "best_only"], argvalues=[ From 360d702c957aaa9554c82a42da120ebd2984d93d Mon Sep 17 00:00:00 2001 From: Jim Stuhlmacher Date: Sat, 2 Nov 2024 14:14:28 -0500 Subject: [PATCH 2/2] formatting --- src/simplejustwatchapi/justwatch.py | 10 +- src/simplejustwatchapi/query.py | 3 +- test/simplejustwatchapi/test_justwatch.py | 2 +- test/simplejustwatchapi/test_parser.py | 898 ++++++++++++---------- test/simplejustwatchapi/test_request.py | 6 +- 5 files changed, 497 insertions(+), 422 deletions(-) diff --git a/src/simplejustwatchapi/justwatch.py b/src/simplejustwatchapi/justwatch.py index 7c36901..0f97fbf 100644 --- a/src/simplejustwatchapi/justwatch.py +++ b/src/simplejustwatchapi/justwatch.py @@ -4,16 +4,16 @@ from simplejustwatchapi.query import ( MediaEntry, - SeasonsEntry, Offer, + SeasonsEntry, parse_details_response, - parse_seasons_response, parse_offers_for_countries_response, parse_search_response, + parse_seasons_response, prepare_details_request, - prepare_seasons_request, prepare_offers_for_countries_request, prepare_search_request, + prepare_seasons_request, ) _GRAPHQL_API_URL = "https://apis.justwatch.com/graphql" @@ -70,9 +70,7 @@ def details( return parse_details_response(response.json()) -def seasons( - node_id: str, country: str = "US", language: str = "en" -) -> SeasonsEntry: +def seasons(node_id: str, country: str = "US", language: str = "en") -> SeasonsEntry: """Get show seasons for a given ID. Args: diff --git a/src/simplejustwatchapi/query.py b/src/simplejustwatchapi/query.py index db34ffe..6ded63d 100644 --- a/src/simplejustwatchapi/query.py +++ b/src/simplejustwatchapi/query.py @@ -458,7 +458,7 @@ class SeasonsEntry(NamedTuple): seasons: list[Season] """List of show seasons. """ - + def prepare_search_request( title: str, country: str, language: str, count: int, best_only: bool ) -> dict: @@ -594,6 +594,7 @@ def prepare_seasons_request(node_id: str, country: str, language: str) -> dict: "query": _GRAPHQL_SEASONS_QUERY, } + def parse_seasons_response(json: any) -> SeasonsEntry | None: """Parse response from seasons query from JustWatch GraphQL API. Parses response for ``GetNodeById`` query. diff --git a/test/simplejustwatchapi/test_justwatch.py b/test/simplejustwatchapi/test_justwatch.py index 59734ec..eb889b0 100644 --- a/test/simplejustwatchapi/test_justwatch.py +++ b/test/simplejustwatchapi/test_justwatch.py @@ -2,7 +2,7 @@ from pytest import fixture -from simplejustwatchapi.justwatch import details, seasons, offers_for_countries, search +from simplejustwatchapi.justwatch import details, offers_for_countries, search, seasons JUSTWATCH_GRAPHQL_URL = "https://apis.justwatch.com/graphql" diff --git a/test/simplejustwatchapi/test_parser.py b/test/simplejustwatchapi/test_parser.py index ebf2366..236b5e5 100644 --- a/test/simplejustwatchapi/test_parser.py +++ b/test/simplejustwatchapi/test_parser.py @@ -1,19 +1,19 @@ from pytest import mark from simplejustwatchapi.query import ( + Episode, Interactions, MediaEntry, - SeasonsEntry, - Season, - Episode, Offer, OfferPackage, Scoring, + Season, + SeasonsEntry, StreamingCharts, parse_details_response, - parse_seasons_response, parse_offers_for_countries_response, parse_search_response, + parse_seasons_response, ) DETAILS_URL = "https://justwatch.com" @@ -373,415 +373,493 @@ ) PARSED_NODE_4 = SeasonsEntry( - entry_id='ts85167', + entry_id="ts85167", seasons=[ - Season(seasonNumber=1, - episodes=[ - Episode(seasonNumber=1, episodeNumber=1, title='Pilot'), - Episode(seasonNumber=1, episodeNumber=2, title='City Council'), - Episode(seasonNumber=1, episodeNumber=3, title='Werewolf Feud'), - Episode(seasonNumber=1, episodeNumber=4, title='Manhattan Night Club'), - Episode(seasonNumber=1, episodeNumber=5, title='Animal Control'), - Episode(seasonNumber=1, episodeNumber=6, title="Baron's Night Out"), - Episode(seasonNumber=1, episodeNumber=7, title='The Trial'), - Episode(seasonNumber=1, episodeNumber=8, title='Citizenship'), - Episode(seasonNumber=1, episodeNumber=9, title='The Orgy'), - Episode(seasonNumber=1, episodeNumber=10, title='Ancestry') - ]), - Season(seasonNumber=2, - episodes=[ - Episode(seasonNumber=2, episodeNumber=1, title='Resurrection'), - Episode(seasonNumber=2, episodeNumber=2, title='Ghosts'), - Episode(seasonNumber=2, episodeNumber=3, title='Brain Scramblies'), - Episode(seasonNumber=2, episodeNumber=4, title='The Curse'), - Episode(seasonNumber=2, episodeNumber=5, title="Colin's Promotion"), - Episode(seasonNumber=2, episodeNumber=6, title='On the Run'), - Episode(seasonNumber=2, episodeNumber=7, title='The Return'), - Episode(seasonNumber=2, episodeNumber=8, title='Collaboration'), - Episode(seasonNumber=2, episodeNumber=9, title='Witches'), - Episode(seasonNumber=2, episodeNumber=10, title='Nouveau Théâtre des Vampires') - ]), - Season(seasonNumber=3, - episodes=[ - Episode(seasonNumber=3, episodeNumber=1, title='The Prisoner'), - Episode(seasonNumber=3, episodeNumber=2, title='The Cloak of Duplication'), - Episode(seasonNumber=3, episodeNumber=3, title='Gail'), - Episode(seasonNumber=3, episodeNumber=4, title='The Casino'), - Episode(seasonNumber=3, episodeNumber=5, title='The Chamber of Judgement'), - Episode(seasonNumber=3, episodeNumber=6, title='The Escape'), - Episode(seasonNumber=3, episodeNumber=7, title='The Siren'), - Episode(seasonNumber=3, episodeNumber=8, title='The Wellness Center'), - Episode(seasonNumber=3, episodeNumber=9, title='A Farewell'), - Episode(seasonNumber=3, episodeNumber=10, title='The Portrait') - ]), - Season(seasonNumber=4, - episodes=[Episode(seasonNumber=4, episodeNumber=1, title='Reunited'), - Episode(seasonNumber=4, episodeNumber=2, title='The Lamp'), - Episode(seasonNumber=4, episodeNumber=3, title='The Grand Opening'), - Episode(seasonNumber=4, episodeNumber=4, title='The Night Market'), - Episode(seasonNumber=4, episodeNumber=5, title='Private School'), - Episode(seasonNumber=4, episodeNumber=6, title='The Wedding'), - Episode(seasonNumber=4, episodeNumber=7, title='Pine Barrens'), - Episode(seasonNumber=4, episodeNumber=8, title='Go Flip Yourself'), - Episode(seasonNumber=4, episodeNumber=9, title='Freddie'), - Episode(seasonNumber=4, episodeNumber=10, title='Sunrise, Sunset') - ]), - Season(seasonNumber=5, - episodes=[Episode(seasonNumber=5, episodeNumber=1, title='The Mall'), - Episode(seasonNumber=5, episodeNumber=2, title='A Night Out with the Guys'), - Episode(seasonNumber=5, episodeNumber=3, title='Pride Parade'), - Episode(seasonNumber=5, episodeNumber=4, title='The Campaign'), - Episode(seasonNumber=5, episodeNumber=5, title='Local News'), - Episode(seasonNumber=5, episodeNumber=6, title='Urgent Care'), - Episode(seasonNumber=5, episodeNumber=7, title='Hybrid Creatures'), - Episode(seasonNumber=5, episodeNumber=8, title='The Roast'), - Episode(seasonNumber=5, episodeNumber=9, title='A Weekend at Morrigan Manor'), - Episode(seasonNumber=5, episodeNumber=10, title='Exit Interview') - ]), - Season(seasonNumber=6, - episodes=[ - Episode(seasonNumber=6, episodeNumber=1, title='Episode 1'), - Episode(seasonNumber=6, episodeNumber=2, title='Episode 2'), - Episode(seasonNumber=6, episodeNumber=3, title='Episode 3'), - Episode(seasonNumber=6, episodeNumber=4, title='Episode 4'), - Episode(seasonNumber=6, episodeNumber=5, title='Episode 5'), - Episode(seasonNumber=6, episodeNumber=6, title='Episode 6'), - Episode(seasonNumber=6, episodeNumber=7, title='Episode 7'), - Episode(seasonNumber=6, episodeNumber=8, title='Episode 8'), - Episode(seasonNumber=6, episodeNumber=9, title='Episode 9'), - Episode(seasonNumber=6, episodeNumber=10, title='Episode 10'), - Episode(seasonNumber=6, episodeNumber=11, title='Episode 11') - ]) - ] + Season( + seasonNumber=1, + episodes=[ + Episode(seasonNumber=1, episodeNumber=1, title="Pilot"), + Episode(seasonNumber=1, episodeNumber=2, title="City Council"), + Episode(seasonNumber=1, episodeNumber=3, title="Werewolf Feud"), + Episode(seasonNumber=1, episodeNumber=4, title="Manhattan Night Club"), + Episode(seasonNumber=1, episodeNumber=5, title="Animal Control"), + Episode(seasonNumber=1, episodeNumber=6, title="Baron's Night Out"), + Episode(seasonNumber=1, episodeNumber=7, title="The Trial"), + Episode(seasonNumber=1, episodeNumber=8, title="Citizenship"), + Episode(seasonNumber=1, episodeNumber=9, title="The Orgy"), + Episode(seasonNumber=1, episodeNumber=10, title="Ancestry"), + ], + ), + Season( + seasonNumber=2, + episodes=[ + Episode(seasonNumber=2, episodeNumber=1, title="Resurrection"), + Episode(seasonNumber=2, episodeNumber=2, title="Ghosts"), + Episode(seasonNumber=2, episodeNumber=3, title="Brain Scramblies"), + Episode(seasonNumber=2, episodeNumber=4, title="The Curse"), + Episode(seasonNumber=2, episodeNumber=5, title="Colin's Promotion"), + Episode(seasonNumber=2, episodeNumber=6, title="On the Run"), + Episode(seasonNumber=2, episodeNumber=7, title="The Return"), + Episode(seasonNumber=2, episodeNumber=8, title="Collaboration"), + Episode(seasonNumber=2, episodeNumber=9, title="Witches"), + Episode(seasonNumber=2, episodeNumber=10, title="Nouveau Théâtre des Vampires"), + ], + ), + Season( + seasonNumber=3, + episodes=[ + Episode(seasonNumber=3, episodeNumber=1, title="The Prisoner"), + Episode(seasonNumber=3, episodeNumber=2, title="The Cloak of Duplication"), + Episode(seasonNumber=3, episodeNumber=3, title="Gail"), + Episode(seasonNumber=3, episodeNumber=4, title="The Casino"), + Episode(seasonNumber=3, episodeNumber=5, title="The Chamber of Judgement"), + Episode(seasonNumber=3, episodeNumber=6, title="The Escape"), + Episode(seasonNumber=3, episodeNumber=7, title="The Siren"), + Episode(seasonNumber=3, episodeNumber=8, title="The Wellness Center"), + Episode(seasonNumber=3, episodeNumber=9, title="A Farewell"), + Episode(seasonNumber=3, episodeNumber=10, title="The Portrait"), + ], + ), + Season( + seasonNumber=4, + episodes=[ + Episode(seasonNumber=4, episodeNumber=1, title="Reunited"), + Episode(seasonNumber=4, episodeNumber=2, title="The Lamp"), + Episode(seasonNumber=4, episodeNumber=3, title="The Grand Opening"), + Episode(seasonNumber=4, episodeNumber=4, title="The Night Market"), + Episode(seasonNumber=4, episodeNumber=5, title="Private School"), + Episode(seasonNumber=4, episodeNumber=6, title="The Wedding"), + Episode(seasonNumber=4, episodeNumber=7, title="Pine Barrens"), + Episode(seasonNumber=4, episodeNumber=8, title="Go Flip Yourself"), + Episode(seasonNumber=4, episodeNumber=9, title="Freddie"), + Episode(seasonNumber=4, episodeNumber=10, title="Sunrise, Sunset"), + ], + ), + Season( + seasonNumber=5, + episodes=[ + Episode(seasonNumber=5, episodeNumber=1, title="The Mall"), + Episode(seasonNumber=5, episodeNumber=2, title="A Night Out with the Guys"), + Episode(seasonNumber=5, episodeNumber=3, title="Pride Parade"), + Episode(seasonNumber=5, episodeNumber=4, title="The Campaign"), + Episode(seasonNumber=5, episodeNumber=5, title="Local News"), + Episode(seasonNumber=5, episodeNumber=6, title="Urgent Care"), + Episode(seasonNumber=5, episodeNumber=7, title="Hybrid Creatures"), + Episode(seasonNumber=5, episodeNumber=8, title="The Roast"), + Episode(seasonNumber=5, episodeNumber=9, title="A Weekend at Morrigan Manor"), + Episode(seasonNumber=5, episodeNumber=10, title="Exit Interview"), + ], + ), + Season( + seasonNumber=6, + episodes=[ + Episode(seasonNumber=6, episodeNumber=1, title="Episode 1"), + Episode(seasonNumber=6, episodeNumber=2, title="Episode 2"), + Episode(seasonNumber=6, episodeNumber=3, title="Episode 3"), + Episode(seasonNumber=6, episodeNumber=4, title="Episode 4"), + Episode(seasonNumber=6, episodeNumber=5, title="Episode 5"), + Episode(seasonNumber=6, episodeNumber=6, title="Episode 6"), + Episode(seasonNumber=6, episodeNumber=7, title="Episode 7"), + Episode(seasonNumber=6, episodeNumber=8, title="Episode 8"), + Episode(seasonNumber=6, episodeNumber=9, title="Episode 9"), + Episode(seasonNumber=6, episodeNumber=10, title="Episode 10"), + Episode(seasonNumber=6, episodeNumber=11, title="Episode 11"), + ], + ), + ], ) RESPONSE_NODE_4 = { - '__typename': 'Show', - 'id': 'ts85167', - 'seasons': [{'__typename': 'Season', - 'content': {'seasonNumber': 1}, - 'episodes': [{'__typename': 'Episode', - 'content': {'episodeNumber': 1, - 'seasonNumber': 1, - 'title': 'Pilot'}, - 'id': 'tse1631894'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 2, - 'seasonNumber': 1, - 'title': 'City Council'}, - 'id': 'tse1747980'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 3, - 'seasonNumber': 1, - 'title': 'Werewolf Feud'}, - 'id': 'tse1747981'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 4, - 'seasonNumber': 1, - 'title': 'Manhattan Night Club'}, - 'id': 'tse1747982'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 5, - 'seasonNumber': 1, - 'title': 'Animal Control'}, - 'id': 'tse1747983'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 6, - 'seasonNumber': 1, - 'title': "Baron's Night Out"}, - 'id': 'tse1769781'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 7, - 'seasonNumber': 1, - 'title': 'The Trial'}, - 'id': 'tse1777016'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 8, - 'seasonNumber': 1, - 'title': 'Citizenship'}, - 'id': 'tse1797942'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 9, - 'seasonNumber': 1, - 'title': 'The Orgy'}, - 'id': 'tse1797943'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 10, - 'seasonNumber': 1, - 'title': 'Ancestry'}, - 'id': 'tse1797944'}], - 'id': 'tss98319'}, - {'__typename': 'Season', - 'content': {'seasonNumber': 2}, - 'episodes': [{'__typename': 'Episode', - 'content': {'episodeNumber': 1, - 'seasonNumber': 2, - 'title': 'Resurrection'}, - 'id': 'tse3177541'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 2, - 'seasonNumber': 2, - 'title': 'Ghosts'}, - 'id': 'tse4343145'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 3, - 'seasonNumber': 2, - 'title': 'Brain Scramblies'}, - 'id': 'tse4366752'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 4, - 'seasonNumber': 2, - 'title': 'The Curse'}, - 'id': 'tse4366760'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 5, - 'seasonNumber': 2, - 'title': "Colin's Promotion"}, - 'id': 'tse4366763'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 6, - 'seasonNumber': 2, - 'title': 'On the Run'}, - 'id': 'tse4366757'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 7, - 'seasonNumber': 2, - 'title': 'The Return'}, - 'id': 'tse4366754'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 8, - 'seasonNumber': 2, - 'title': 'Collaboration'}, - 'id': 'tse4366751'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 9, - 'seasonNumber': 2, - 'title': 'Witches'}, - 'id': 'tse4366755'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 10, - 'seasonNumber': 2, - 'title': 'Nouveau Théâtre des ' - 'Vampires'}, - 'id': 'tse4366769'}], - 'id': 'tss200608'}, - {'__typename': 'Season', - 'content': {'seasonNumber': 3}, - 'episodes': [{'__typename': 'Episode', - 'content': {'episodeNumber': 1, - 'seasonNumber': 3, - 'title': 'The Prisoner'}, - 'id': 'tse5761373'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 2, - 'seasonNumber': 3, - 'title': 'The Cloak of Duplication'}, - 'id': 'tse5761374'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 3, - 'seasonNumber': 3, - 'title': 'Gail'}, - 'id': 'tse5863479'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 4, - 'seasonNumber': 3, - 'title': 'The Casino'}, - 'id': 'tse5863485'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 5, - 'seasonNumber': 3, - 'title': 'The Chamber of Judgement'}, - 'id': 'tse5863482'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 6, - 'seasonNumber': 3, - 'title': 'The Escape'}, - 'id': 'tse5863480'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 7, - 'seasonNumber': 3, - 'title': 'The Siren'}, - 'id': 'tse5863486'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 8, - 'seasonNumber': 3, - 'title': 'The Wellness Center'}, - 'id': 'tse5863484'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 9, - 'seasonNumber': 3, - 'title': 'A Farewell'}, - 'id': 'tse5863483'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 10, - 'seasonNumber': 3, - 'title': 'The Portrait'}, - 'id': 'tse5863481'}], - 'id': 'tss306291'}, - {'__typename': 'Season', - 'content': {'seasonNumber': 4}, - 'episodes': [{'__typename': 'Episode', - 'content': {'episodeNumber': 1, - 'seasonNumber': 4, - 'title': 'Reunited'}, - 'id': 'tse6569917'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 2, - 'seasonNumber': 4, - 'title': 'The Lamp'}, - 'id': 'tse6569916'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 3, - 'seasonNumber': 4, - 'title': 'The Grand Opening'}, - 'id': 'tse6569915'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 4, - 'seasonNumber': 4, - 'title': 'The Night Market'}, - 'id': 'tse6638945'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 5, - 'seasonNumber': 4, - 'title': 'Private School'}, - 'id': 'tse6638946'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 6, - 'seasonNumber': 4, - 'title': 'The Wedding'}, - 'id': 'tse6638941'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 7, - 'seasonNumber': 4, - 'title': 'Pine Barrens'}, - 'id': 'tse6638942'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 8, - 'seasonNumber': 4, - 'title': 'Go Flip Yourself'}, - 'id': 'tse6638947'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 9, - 'seasonNumber': 4, - 'title': 'Freddie'}, - 'id': 'tse6638944'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 10, - 'seasonNumber': 4, - 'title': 'Sunrise, Sunset'}, - 'id': 'tse6638943'}], - 'id': 'tss357215'}, - {'__typename': 'Season', - 'content': {'seasonNumber': 5}, - 'episodes': [{'__typename': 'Episode', - 'content': {'episodeNumber': 1, - 'seasonNumber': 5, - 'title': 'The Mall'}, - 'id': 'tse7422284'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 2, - 'seasonNumber': 5, - 'title': 'A Night Out with the Guys'}, - 'id': 'tse7422285'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 3, - 'seasonNumber': 5, - 'title': 'Pride Parade'}, - 'id': 'tse7482502'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 4, - 'seasonNumber': 5, - 'title': 'The Campaign'}, - 'id': 'tse7528347'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 5, - 'seasonNumber': 5, - 'title': 'Local News'}, - 'id': 'tse7528354'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 6, - 'seasonNumber': 5, - 'title': 'Urgent Care'}, - 'id': 'tse7528351'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 7, - 'seasonNumber': 5, - 'title': 'Hybrid Creatures'}, - 'id': 'tse7528350'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 8, - 'seasonNumber': 5, - 'title': 'The Roast'}, - 'id': 'tse7528352'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 9, - 'seasonNumber': 5, - 'title': 'A Weekend at Morrigan Manor'}, - 'id': 'tse7528353'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 10, - 'seasonNumber': 5, - 'title': 'Exit Interview'}, - 'id': 'tse7528349'}], - 'id': 'tss361882'}, - {'__typename': 'Season', - 'content': {'seasonNumber': 6}, - 'episodes': [{'__typename': 'Episode', - 'content': {'episodeNumber': 1, - 'seasonNumber': 6, - 'title': 'Episode 1'}, - 'id': 'tse7528348'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 2, - 'seasonNumber': 6, - 'title': 'Episode 2'}, - 'id': 'tse8544433'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 3, - 'seasonNumber': 6, - 'title': 'Episode 3'}, - 'id': 'tse8544439'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 4, - 'seasonNumber': 6, - 'title': 'Episode 4'}, - 'id': 'tse8544449'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 5, - 'seasonNumber': 6, - 'title': 'Episode 5'}, - 'id': 'tse8544457'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 6, - 'seasonNumber': 6, - 'title': 'Episode 6'}, - 'id': 'tse8544465'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 7, - 'seasonNumber': 6, - 'title': 'Episode 7'}, - 'id': 'tse8544476'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 8, - 'seasonNumber': 6, - 'title': 'Episode 8'}, - 'id': 'tse8544483'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 9, - 'seasonNumber': 6, - 'title': 'Episode 9'}, - 'id': 'tse8544494'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 10, - 'seasonNumber': 6, - 'title': 'Episode 10'}, - 'id': 'tse8544502'}, - {'__typename': 'Episode', - 'content': {'episodeNumber': 11, - 'seasonNumber': 6, - 'title': 'Episode 11'}, - 'id': 'tse8544521'}], - 'id': 'tss361883'} - ] + "__typename": "Show", + "id": "ts85167", + "seasons": [ + { + "__typename": "Season", + "content": {"seasonNumber": 1}, + "episodes": [ + { + "__typename": "Episode", + "content": {"episodeNumber": 1, "seasonNumber": 1, "title": "Pilot"}, + "id": "tse1631894", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 2, "seasonNumber": 1, "title": "City Council"}, + "id": "tse1747980", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 3, "seasonNumber": 1, "title": "Werewolf Feud"}, + "id": "tse1747981", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 4, + "seasonNumber": 1, + "title": "Manhattan Night Club", + }, + "id": "tse1747982", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 5, "seasonNumber": 1, "title": "Animal Control"}, + "id": "tse1747983", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 6, + "seasonNumber": 1, + "title": "Baron's Night Out", + }, + "id": "tse1769781", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 7, "seasonNumber": 1, "title": "The Trial"}, + "id": "tse1777016", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 8, "seasonNumber": 1, "title": "Citizenship"}, + "id": "tse1797942", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 9, "seasonNumber": 1, "title": "The Orgy"}, + "id": "tse1797943", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 10, "seasonNumber": 1, "title": "Ancestry"}, + "id": "tse1797944", + }, + ], + "id": "tss98319", + }, + { + "__typename": "Season", + "content": {"seasonNumber": 2}, + "episodes": [ + { + "__typename": "Episode", + "content": {"episodeNumber": 1, "seasonNumber": 2, "title": "Resurrection"}, + "id": "tse3177541", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 2, "seasonNumber": 2, "title": "Ghosts"}, + "id": "tse4343145", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 3, "seasonNumber": 2, "title": "Brain Scramblies"}, + "id": "tse4366752", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 4, "seasonNumber": 2, "title": "The Curse"}, + "id": "tse4366760", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 5, + "seasonNumber": 2, + "title": "Colin's Promotion", + }, + "id": "tse4366763", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 6, "seasonNumber": 2, "title": "On the Run"}, + "id": "tse4366757", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 7, "seasonNumber": 2, "title": "The Return"}, + "id": "tse4366754", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 8, "seasonNumber": 2, "title": "Collaboration"}, + "id": "tse4366751", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 9, "seasonNumber": 2, "title": "Witches"}, + "id": "tse4366755", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 10, + "seasonNumber": 2, + "title": "Nouveau Théâtre des " "Vampires", + }, + "id": "tse4366769", + }, + ], + "id": "tss200608", + }, + { + "__typename": "Season", + "content": {"seasonNumber": 3}, + "episodes": [ + { + "__typename": "Episode", + "content": {"episodeNumber": 1, "seasonNumber": 3, "title": "The Prisoner"}, + "id": "tse5761373", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 2, + "seasonNumber": 3, + "title": "The Cloak of Duplication", + }, + "id": "tse5761374", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 3, "seasonNumber": 3, "title": "Gail"}, + "id": "tse5863479", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 4, "seasonNumber": 3, "title": "The Casino"}, + "id": "tse5863485", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 5, + "seasonNumber": 3, + "title": "The Chamber of Judgement", + }, + "id": "tse5863482", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 6, "seasonNumber": 3, "title": "The Escape"}, + "id": "tse5863480", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 7, "seasonNumber": 3, "title": "The Siren"}, + "id": "tse5863486", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 8, + "seasonNumber": 3, + "title": "The Wellness Center", + }, + "id": "tse5863484", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 9, "seasonNumber": 3, "title": "A Farewell"}, + "id": "tse5863483", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 10, "seasonNumber": 3, "title": "The Portrait"}, + "id": "tse5863481", + }, + ], + "id": "tss306291", + }, + { + "__typename": "Season", + "content": {"seasonNumber": 4}, + "episodes": [ + { + "__typename": "Episode", + "content": {"episodeNumber": 1, "seasonNumber": 4, "title": "Reunited"}, + "id": "tse6569917", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 2, "seasonNumber": 4, "title": "The Lamp"}, + "id": "tse6569916", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 3, + "seasonNumber": 4, + "title": "The Grand Opening", + }, + "id": "tse6569915", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 4, "seasonNumber": 4, "title": "The Night Market"}, + "id": "tse6638945", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 5, "seasonNumber": 4, "title": "Private School"}, + "id": "tse6638946", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 6, "seasonNumber": 4, "title": "The Wedding"}, + "id": "tse6638941", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 7, "seasonNumber": 4, "title": "Pine Barrens"}, + "id": "tse6638942", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 8, "seasonNumber": 4, "title": "Go Flip Yourself"}, + "id": "tse6638947", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 9, "seasonNumber": 4, "title": "Freddie"}, + "id": "tse6638944", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 10, "seasonNumber": 4, "title": "Sunrise, Sunset"}, + "id": "tse6638943", + }, + ], + "id": "tss357215", + }, + { + "__typename": "Season", + "content": {"seasonNumber": 5}, + "episodes": [ + { + "__typename": "Episode", + "content": {"episodeNumber": 1, "seasonNumber": 5, "title": "The Mall"}, + "id": "tse7422284", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 2, + "seasonNumber": 5, + "title": "A Night Out with the Guys", + }, + "id": "tse7422285", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 3, "seasonNumber": 5, "title": "Pride Parade"}, + "id": "tse7482502", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 4, "seasonNumber": 5, "title": "The Campaign"}, + "id": "tse7528347", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 5, "seasonNumber": 5, "title": "Local News"}, + "id": "tse7528354", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 6, "seasonNumber": 5, "title": "Urgent Care"}, + "id": "tse7528351", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 7, "seasonNumber": 5, "title": "Hybrid Creatures"}, + "id": "tse7528350", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 8, "seasonNumber": 5, "title": "The Roast"}, + "id": "tse7528352", + }, + { + "__typename": "Episode", + "content": { + "episodeNumber": 9, + "seasonNumber": 5, + "title": "A Weekend at Morrigan Manor", + }, + "id": "tse7528353", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 10, "seasonNumber": 5, "title": "Exit Interview"}, + "id": "tse7528349", + }, + ], + "id": "tss361882", + }, + { + "__typename": "Season", + "content": {"seasonNumber": 6}, + "episodes": [ + { + "__typename": "Episode", + "content": {"episodeNumber": 1, "seasonNumber": 6, "title": "Episode 1"}, + "id": "tse7528348", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 2, "seasonNumber": 6, "title": "Episode 2"}, + "id": "tse8544433", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 3, "seasonNumber": 6, "title": "Episode 3"}, + "id": "tse8544439", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 4, "seasonNumber": 6, "title": "Episode 4"}, + "id": "tse8544449", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 5, "seasonNumber": 6, "title": "Episode 5"}, + "id": "tse8544457", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 6, "seasonNumber": 6, "title": "Episode 6"}, + "id": "tse8544465", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 7, "seasonNumber": 6, "title": "Episode 7"}, + "id": "tse8544476", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 8, "seasonNumber": 6, "title": "Episode 8"}, + "id": "tse8544483", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 9, "seasonNumber": 6, "title": "Episode 9"}, + "id": "tse8544494", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 10, "seasonNumber": 6, "title": "Episode 10"}, + "id": "tse8544502", + }, + { + "__typename": "Episode", + "content": {"episodeNumber": 11, "seasonNumber": 6, "title": "Episode 11"}, + "id": "tse8544521", + }, + ], + "id": "tss361883", + }, + ], } API_SEARCH_RESPONSE_JSON = { diff --git a/test/simplejustwatchapi/test_request.py b/test/simplejustwatchapi/test_request.py index f118edd..645706f 100644 --- a/test/simplejustwatchapi/test_request.py +++ b/test/simplejustwatchapi/test_request.py @@ -2,9 +2,9 @@ from simplejustwatchapi.query import ( prepare_details_request, - prepare_seasons_request, prepare_offers_for_countries_request, prepare_search_request, + prepare_seasons_request, ) GRAPHQL_DETAILS_QUERY = """ @@ -317,9 +317,7 @@ def test_prepare_details_request_asserts_on_invalid_country_code(invalid_code: s ("NODE ID 1", "gb", "language 2"), ], ) -def test_prepare_seasons_request( - node_id: str, country: str, language: str -) -> None: +def test_prepare_seasons_request(node_id: str, country: str, language: str) -> None: expected_request = { "operationName": "GetNodeById", "variables": {