simple-justwatch-python-api 1.1.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple-justwatch-python-api
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: A simple JustWatch Python API
5
5
  Keywords: justwatch,api,graphql
6
6
  Author: Electronic Mango
@@ -3,7 +3,7 @@ name = "simple-justwatch-python-api"
3
3
  authors = [
4
4
  { name = "Electronic Mango", email = "78230210+Electronic-Mango@users.noreply.github.com" },
5
5
  ]
6
- version = "1.1.0"
6
+ version = "1.3.0"
7
7
  description = "A simple JustWatch Python API"
8
8
  readme = "README.md"
9
9
  license = "MIT"
@@ -30,7 +30,7 @@ dev = [
30
30
  "pytest>=9.0.3",
31
31
  "pytest-mock>=3.15.1",
32
32
  "ruff>=0.15.10",
33
- "zensical>=0.0.32",
33
+ "zensical>=0.0.37",
34
34
  ]
35
35
 
36
36
  [project.urls]
@@ -40,4 +40,15 @@ class JustWatchHttpError(JustWatchError):
40
40
 
41
41
  This is a general exception for any HTTP-related errors, such as non-`2xx` status
42
42
  codes, network errors, timeouts, etc.
43
+
44
+ Attributes:
45
+ msg (str): Error message describing the HTTP error.
46
+ response (str | None): Optional text of the HTTP response, if available.
47
+ Usucally contains JSON with error responses from the API.
48
+
43
49
  """
50
+
51
+ def __init__(self, msg: str, response: str | None = None) -> None:
52
+ """Init JustWatchHttpError with error message and optional response text."""
53
+ super().__init__(msg)
54
+ self.response = response
@@ -9,11 +9,11 @@ prepared GraphQL query.
9
9
  Most functions have a number of common arguments (in addition to function-specific
10
10
  ones, like `title` to search for):
11
11
 
12
- | Name | Description |
13
- |-------------|-------------|
14
- | `country` | 2-letter country code for which offers are selected, (e.g., `US`, \
12
+ | Name | Description |
13
+ |------|-------------|
14
+ | `country` | 2-letter country code for which offers are selected, (e.g., `US`, \
15
15
  `GB`, `DE`). |
16
- | `language` | Code for language in responses. It consists of 2 lowercase letters \
16
+ | `language` | Code for language in responses. It consists of 2 lowercase letters \
17
17
  with optional uppercase alphanumeric suffix (e.g., `en`, `en-US`, \
18
18
  `de`, `de-CH1901`). |
19
19
  | `best_only` | Whether to return only "best" offers for each provider instead of, \
@@ -22,19 +22,23 @@ ones, like `title` to search for):
22
22
  Functions returning data for multiple titles
23
23
  ([`search`][simplejustwatchapi.justwatch.search],
24
24
  [`popular`][simplejustwatchapi.justwatch.popular])
25
- also allow for specifying number of elements, basic pagination, and filtering for
26
- specific providers:
25
+ also allow for specifying number of elements, basic pagination, and additional
26
+ filtering:
27
27
 
28
- | Name | Description |
29
- |-------------|-------------|
30
- | `count` | How many entries should be returned. |
31
- | `offset` | Basic "pagination". Offset for the first returned result, i.e. how \
28
+ | Name | Description |
29
+ |------|-------------|
30
+ | `count` | How many entries should be returned. |
31
+ | `offset` | Basic "pagination". Offset for the first returned result, i.e. how \
32
32
  many first entries should be skipped. Everything is handled on API \
33
33
  side, this library isn't doing any filtering. |
34
34
  | `providers` | Providers (like Netflix, Amazon Prime Video) for which offers should \
35
35
  returned. Requires 3-letter "short name". Check \
36
36
  [`providers`][simplejustwatchapi.justwatch.providers] for an example \
37
- of how you can get that value.
37
+ of how you can get that value. |
38
+ | `min_release_year` | Minimum release year of returned titles. |
39
+ | `max_release_year` | Maximum release year of returned titles. |
40
+ | `object_types` | Types of objects to filter for. It seems that only `SHOW` and \
41
+ `MOVIE` are useful, but it's not strictly enforced. |
38
42
 
39
43
  Each function can raise two exceptions:
40
44
 
@@ -47,7 +51,7 @@ Each function can raise two exceptions:
47
51
  country code. |
48
52
  """
49
53
 
50
- from httpx import HTTPError, post
54
+ from httpx import HTTPError, HTTPStatusError, post
51
55
 
52
56
  from simplejustwatchapi.exceptions import JustWatchHttpError
53
57
  from simplejustwatchapi.query import (
@@ -79,6 +83,9 @@ def search(
79
83
  best_only: bool = True,
80
84
  offset: int = 0,
81
85
  providers: list[str] | str | None = None,
86
+ min_release_year: int | None = None,
87
+ max_release_year: int | None = None,
88
+ object_types: list[str] | str | None = None,
82
89
  ) -> list[MediaEntry]:
83
90
  """
84
91
  Search JustWatch for the given title.
@@ -148,18 +155,50 @@ def search(
148
155
  You can look up values through [`providers`]
149
156
  [simplejustwatchapi.justwatch.providers] function.
150
157
 
158
+ min_release_year (int | None): Minimum release year of returned titles.
159
+
160
+ If `None` (the default value), no filtering is done.
161
+
162
+ max_release_year (int | None): Maximum release year of returned titles.
163
+
164
+ If `None` (the default value), no filtering is done.
165
+
166
+ object_types (list[str] | str | None): Types of objects to filter for, like
167
+ `SHOW` or `MOVIE`.
168
+
169
+ It seems that only `SHOW` and `MOVIE` are useful, but it's not strictly
170
+ enforced. Types like `SHOW_EPISODE`, or `SHOW_SEASON` can be used, but they
171
+ seem to return TV shows, same as `SHOW`.
172
+
173
+ While the type value is not enforced, it **must** be a valid type, otherwise
174
+ API will respond with HTTP status code 422.
175
+
176
+ For single type it can be a single string, or a list with one string.
177
+
178
+ If `None` (the default value), no filtering is done.
179
+
151
180
  Returns:
152
181
  (list[MediaEntry]): List of tuples with details of search results.
153
182
 
154
183
  Raises:
155
184
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
156
185
  due to invalid language or country code.
186
+
157
187
  exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
158
188
  responded with non-`2xx` status code.
159
189
 
160
190
  """
161
191
  request = prepare_search_request(
162
- title, country, language, count, best_only, offset, providers
192
+ title,
193
+ country,
194
+ language,
195
+ count,
196
+ best_only,
197
+ offset,
198
+ providers,
199
+ min_release_year,
200
+ max_release_year,
201
+ object_types,
163
202
  )
164
203
  response = _post_to_jw_graphql_api(request)
165
204
  return parse_search_response(response)
@@ -172,6 +211,9 @@ def popular(
172
211
  best_only: bool = True,
173
212
  offset: int = 0,
174
213
  providers: list[str] | str | None = None,
214
+ min_release_year: int | None = None,
215
+ max_release_year: int | None = None,
216
+ object_types: list[str] | str | None = None,
175
217
  ) -> list[MediaEntry]:
176
218
  """
177
219
  Look up all currently popular titles on JustWatch.
@@ -233,18 +275,47 @@ def popular(
233
275
  You can look up values through [`providers`]
234
276
  [simplejustwatchapi.justwatch.providers] function.
235
277
 
278
+ min_release_year (int | None): Minimum release year of returned titles.
279
+ If `None` (the default value), no filtering is done.
280
+
281
+ max_release_year (int | None): Maximum release year of returned titles.
282
+ If `None` (the default value), no filtering is done.
283
+
284
+ object_types (list[str] | str | None): Types of objects to filter for, like
285
+ `SHOW` or `MOVIE`.
286
+
287
+ It seems that only `SHOW` and `MOVIE` are useful, but it's not strictly
288
+ enforced. Types like `SHOW_EPISODE`, or `SHOW_SEASON` can be used, but they
289
+ seem to return TV shows, same as `SHOW`.
290
+
291
+ While the type value is not enforced, it **must** be a valid type, otherwise
292
+ API will respond with HTTP status code 422.
293
+
294
+ For single type it can be a single string, or a list with one string.
295
+
296
+ If `None` (the default value), no filtering is done.
297
+
236
298
  Returns:
237
299
  (list[MediaEntry]): List of tuples with details of popular titles.
238
300
 
239
301
  Raises:
240
302
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
241
303
  due to invalid language or country code.
304
+
242
305
  exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
243
306
  responded with non-`2xx` status code.
244
307
 
245
308
  """
246
309
  request = prepare_popular_request(
247
- country, language, count, best_only, offset, providers
310
+ country,
311
+ language,
312
+ count,
313
+ best_only,
314
+ offset,
315
+ providers,
316
+ min_release_year,
317
+ max_release_year,
318
+ object_types,
248
319
  )
249
320
  response = _post_to_jw_graphql_api(request)
250
321
  return parse_popular_response(response)
@@ -309,6 +380,7 @@ def details(
309
380
  Raises:
310
381
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
311
382
  due to invalid language or country code.
383
+
312
384
  exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
313
385
  responded with non-`2xx` status code.
314
386
 
@@ -358,6 +430,7 @@ def seasons(
358
430
  Raises:
359
431
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
360
432
  due to invalid language or country code.
433
+
361
434
  exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
362
435
  responded with non-`2xx` status code.
363
436
 
@@ -408,6 +481,7 @@ def episodes(
408
481
  Raises:
409
482
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
410
483
  due to invalid language or country code.
484
+
411
485
  exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
412
486
  responded with non-`2xx` status code.
413
487
 
@@ -477,6 +551,7 @@ def offers_for_countries(
477
551
  Raises:
478
552
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
479
553
  due to invalid language or country code.
554
+
480
555
  exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
481
556
  responded with non-`2xx` status code.
482
557
 
@@ -538,6 +613,8 @@ def _post_to_jw_graphql_api(request_json: dict) -> dict:
538
613
  try:
539
614
  response = post(_GRAPHQL_API_URL, json=request_json)
540
615
  response.raise_for_status()
616
+ return response.json()
617
+ except HTTPStatusError as e:
618
+ raise JustWatchHttpError(str(e), e.response.text) from e
541
619
  except HTTPError as e:
542
620
  raise JustWatchHttpError(str(e)) from e
543
- return response.json()
@@ -35,10 +35,6 @@ from simplejustwatchapi.tuples import (
35
35
  _DETAILS_URL = "https://justwatch.com"
36
36
  _IMAGES_URL = "https://images.justwatch.com"
37
37
 
38
- # TODO: Left here for posterity, remove once formats are well documented.
39
- _COUNTRY_CODE_REGEX = r"^[A-Z]{2}$"
40
- _LANGUAGE_CODE_REGEX = r"^[a-z]{2}(-[0-9A-Z]+)?$"
41
-
42
38
 
43
39
  def prepare_search_request(
44
40
  title: str,
@@ -48,6 +44,9 @@ def prepare_search_request(
48
44
  best_only: bool,
49
45
  offset: int,
50
46
  providers: list[str] | str | None,
47
+ min_release_year: int | None,
48
+ max_release_year: int | None,
49
+ object_types: list[str] | str | None,
51
50
  ) -> dict[str, Any]:
52
51
  """
53
52
  Prepare search request for JustWatch GraphQL API.
@@ -70,6 +69,10 @@ def prepare_search_request(
70
69
  offset (int): Search results offset.
71
70
  providers (list[str] | str | None): 3-letter service identifier(s),
72
71
  or `None` for all providers.
72
+ min_release_year (int | None): Minimum release year of returned titles.
73
+ max_release_year (int | None): Maximum release year of returned titles.
74
+ object_types (list[str] | str | None): Types of objects to filter for, it seems
75
+ that only "SHOW" and "MOVIE" make sense.
73
76
 
74
77
  Returns:
75
78
  (dict[str, Any]): JSON with GraphQL POST body.
@@ -79,7 +82,11 @@ def prepare_search_request(
79
82
  "operationName": "GetSearchTitles",
80
83
  "variables": {
81
84
  "first": count,
82
- "searchTitlesFilter": {"searchQuery": title, "packages": providers},
85
+ "searchTitlesFilter": {
86
+ "searchQuery": title,
87
+ "packages": providers,
88
+ **_filter_variables(min_release_year, max_release_year, object_types),
89
+ },
83
90
  **_common_variables(best_only),
84
91
  **_locale_variables(country, language),
85
92
  "offset": offset or None,
@@ -122,6 +129,9 @@ def prepare_popular_request(
122
129
  best_only: bool,
123
130
  offset: int,
124
131
  providers: list[str] | str | None,
132
+ min_release_year: int | None,
133
+ max_release_year: int | None,
134
+ object_types: list[str] | str | None,
125
135
  ) -> dict[str, Any]:
126
136
  """
127
137
  Prepare "get popular" request for JustWatch GraphQL API.
@@ -143,6 +153,10 @@ def prepare_popular_request(
143
153
  offset (int): Search results offset.
144
154
  providers (list[str] | str | None): 3-letter service identifier(s),
145
155
  or `None` for all providers.
156
+ min_release_year (int | None): Minimum release year of returned titles.
157
+ max_release_year (int | None): Maximum release year of returned titles.
158
+ object_types (list[str] | str | None): Types of objects to filter for, it seems
159
+ that only "SHOW" and "MOVIE" make sense.
146
160
 
147
161
  Returns:
148
162
  (dict[str, Any]): JSON with GraphQL POST body.
@@ -152,7 +166,10 @@ def prepare_popular_request(
152
166
  "operationName": "GetPopularTitles",
153
167
  "variables": {
154
168
  "first": count,
155
- "popularTitlesFilter": {"packages": providers},
169
+ "popularTitlesFilter": {
170
+ "packages": providers,
171
+ **_filter_variables(min_release_year, max_release_year, object_types),
172
+ },
156
173
  **_common_variables(best_only),
157
174
  **_locale_variables(country, language),
158
175
  "offset": offset or None,
@@ -511,6 +528,19 @@ def _locale_variables(country: str, language: str) -> dict[str, str]:
511
528
  return {"country": country.upper(), "language": language}
512
529
 
513
530
 
531
+ def _filter_variables(
532
+ min_release_year: int | None,
533
+ max_release_year: int | None,
534
+ object_types: list[str] | str | None,
535
+ ) -> dict[str, Any]:
536
+ """Return dict with variables related to looking up lists of titles."""
537
+ return {
538
+ "includeTitlesWithoutUrl": True,
539
+ "objectTypes": object_types,
540
+ "releaseYear": {"min": min_release_year, "max": max_release_year},
541
+ }
542
+
543
+
514
544
  def _raise_for_errors_in_response(json: dict[str, Any]) -> None:
515
545
  """Raise JustWatchApiError if given JSON contains `errors` key."""
516
546
  if "errors" in json:
@@ -222,7 +222,8 @@ class MediaEntry(NamedTuple):
222
222
  object_id (int): Object ID, the numeric part of full entry ID.
223
223
  object_type (str): Type of entry, e.g. `MOVIE`, `SHOW`.
224
224
  title (str): Full title.
225
- url (str): URL to JustWatch with details for this entry.
225
+ url (str | None): URL to JustWatch with details for this entry. Some entries
226
+ are missing dedicated JustWatch pages, for them this field is `None`.
226
227
  release_year (int): Release year as a number.
227
228
  release_date (str): Full release date as a string, e.g. `2013-12-16`.
228
229
  runtime_minutes (int): Runtime in minutes.
@@ -255,7 +256,7 @@ class MediaEntry(NamedTuple):
255
256
  object_id: int
256
257
  object_type: str
257
258
  title: str
258
- url: str
259
+ url: str | None
259
260
  release_year: int
260
261
  release_date: str
261
262
  runtime_minutes: int