imdbio 0.0.1__py3-none-any.whl
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.
- imdbio/__init__.py +57 -0
- imdbio/exceptions.py +129 -0
- imdbio/locale.py +68 -0
- imdbio/models.py +704 -0
- imdbio/parsers.py +867 -0
- imdbio/services.py +804 -0
- imdbio/transformers.py +74 -0
- imdbio-0.0.1.dist-info/METADATA +106 -0
- imdbio-0.0.1.dist-info/RECORD +11 -0
- imdbio-0.0.1.dist-info/WHEEL +4 -0
- imdbio-0.0.1.dist-info/licenses/LICENSE +21 -0
imdbio/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .services import (
|
|
4
|
+
get_movie,
|
|
5
|
+
search_title,
|
|
6
|
+
get_name,
|
|
7
|
+
get_episodes,
|
|
8
|
+
get_all_episodes,
|
|
9
|
+
get_season_episodes,
|
|
10
|
+
get_akas,
|
|
11
|
+
get_reviews,
|
|
12
|
+
get_trivia,
|
|
13
|
+
get_parental_guide,
|
|
14
|
+
get_filmography,
|
|
15
|
+
get_all_interests,
|
|
16
|
+
get_media_gallery,
|
|
17
|
+
TitleType,
|
|
18
|
+
)
|
|
19
|
+
from .models import (
|
|
20
|
+
TitleMediaGallery,
|
|
21
|
+
TitleMediaItem,
|
|
22
|
+
)
|
|
23
|
+
from .exceptions import (
|
|
24
|
+
ImdbioError,
|
|
25
|
+
HTTPError,
|
|
26
|
+
WAFError,
|
|
27
|
+
GraphQLError,
|
|
28
|
+
ParseError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"get_movie",
|
|
33
|
+
"search_title",
|
|
34
|
+
"get_name",
|
|
35
|
+
"get_episodes",
|
|
36
|
+
"get_all_episodes",
|
|
37
|
+
"get_season_episodes",
|
|
38
|
+
"get_akas",
|
|
39
|
+
"get_reviews",
|
|
40
|
+
"get_trivia",
|
|
41
|
+
"get_parental_guide",
|
|
42
|
+
"get_filmography",
|
|
43
|
+
"get_all_interests",
|
|
44
|
+
"get_media_gallery",
|
|
45
|
+
"TitleMediaGallery",
|
|
46
|
+
"TitleMediaItem",
|
|
47
|
+
"TitleType",
|
|
48
|
+
# exceptions
|
|
49
|
+
"ImdbioError",
|
|
50
|
+
"HTTPError",
|
|
51
|
+
"WAFError",
|
|
52
|
+
"GraphQLError",
|
|
53
|
+
"ParseError",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# setup library logging
|
|
57
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
imdbio/exceptions.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imdbio exception hierarchy
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
ImdbioError
|
|
6
|
+
├── HTTPError — any non-200 HTTP response (status_code, url, response_text)
|
|
7
|
+
│ └── WAFError — HTTP 202 from AWS WAF enforcement
|
|
8
|
+
├── GraphQLError — non-200 or {"errors": …} from the GraphQL endpoint
|
|
9
|
+
└── ParseError — __NEXT_DATA__ script not found in HTML response
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional, List, Dict, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ImdbioError(Exception):
|
|
16
|
+
"""Base class for all imdbio exceptions."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTPError(ImdbioError):
|
|
20
|
+
"""Raised when the IMDb HTML endpoint returns a non-200 status code.
|
|
21
|
+
|
|
22
|
+
Attributes
|
|
23
|
+
----------
|
|
24
|
+
status_code : int
|
|
25
|
+
The HTTP status code returned by the server.
|
|
26
|
+
url : str
|
|
27
|
+
The full URL that was requested.
|
|
28
|
+
response_text : str
|
|
29
|
+
The first 500 characters of the response body (may be empty).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self, message: str, status_code: int, url: str, response_text: str = ""
|
|
34
|
+
):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status_code: int = status_code
|
|
37
|
+
self.url: str = url
|
|
38
|
+
self.response_text: str = response_text
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return (
|
|
42
|
+
f"{self.__class__.__name__}("
|
|
43
|
+
f"status_code={self.status_code!r}, "
|
|
44
|
+
f"url={self.url!r}, "
|
|
45
|
+
f"message={str(self)!r})"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WAFError(HTTPError):
|
|
50
|
+
"""Raised when AWS WAF returns HTTP 202, blocking the request.
|
|
51
|
+
|
|
52
|
+
This is a subclass of :class:`HTTPError` so callers that catch
|
|
53
|
+
``HTTPError`` will also catch ``WAFError``. Callers that want to handle
|
|
54
|
+
WAF enforcement specifically (e.g. to rotate proxies or back off) can
|
|
55
|
+
catch ``WAFError`` directly.
|
|
56
|
+
|
|
57
|
+
Attributes
|
|
58
|
+
----------
|
|
59
|
+
status_code : int
|
|
60
|
+
Always ``202`` for WAF-blocked responses.
|
|
61
|
+
url : str
|
|
62
|
+
The full URL that was blocked.
|
|
63
|
+
response_text : str
|
|
64
|
+
The first 500 characters of the WAF challenge response body.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GraphQLError(ImdbioError):
|
|
69
|
+
"""Raised when the IMDb GraphQL API returns a non-200 status or
|
|
70
|
+
an ``{"errors": […]}`` payload.
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
url : str
|
|
75
|
+
The GraphQL endpoint URL.
|
|
76
|
+
query_term : str
|
|
77
|
+
The search term / entity ID used in the query.
|
|
78
|
+
status_code : Optional[int]
|
|
79
|
+
HTTP status code when the failure is transport-level; ``None`` when
|
|
80
|
+
the response was 200 but contained GraphQL ``errors``.
|
|
81
|
+
errors : List[Dict[str, Any]]
|
|
82
|
+
Parsed GraphQL error objects from the response body (empty list for
|
|
83
|
+
transport-level failures).
|
|
84
|
+
response_text : str
|
|
85
|
+
First 500 characters of the raw response body.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
message: str,
|
|
91
|
+
url: str,
|
|
92
|
+
query_term: str,
|
|
93
|
+
status_code: Optional[int] = None,
|
|
94
|
+
errors: Optional[List[Dict[str, Any]]] = None,
|
|
95
|
+
response_text: str = "",
|
|
96
|
+
):
|
|
97
|
+
super().__init__(message)
|
|
98
|
+
self.url: str = url
|
|
99
|
+
self.query_term: str = query_term
|
|
100
|
+
self.status_code: Optional[int] = status_code
|
|
101
|
+
self.errors: List[Dict[str, Any]] = errors or []
|
|
102
|
+
self.response_text: str = response_text
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
return (
|
|
106
|
+
f"{self.__class__.__name__}("
|
|
107
|
+
f"status_code={self.status_code!r}, "
|
|
108
|
+
f"url={self.url!r}, "
|
|
109
|
+
f"query_term={self.query_term!r}, "
|
|
110
|
+
f"message={str(self)!r})"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ParseError(ImdbioError):
|
|
115
|
+
"""Raised when the expected ``__NEXT_DATA__`` JSON script tag is absent
|
|
116
|
+
from the IMDb HTML response.
|
|
117
|
+
|
|
118
|
+
Attributes
|
|
119
|
+
----------
|
|
120
|
+
url : str
|
|
121
|
+
The URL whose response could not be parsed.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, message: str, url: str = ""):
|
|
125
|
+
super().__init__(message)
|
|
126
|
+
self.url: str = url
|
|
127
|
+
|
|
128
|
+
def __repr__(self) -> str:
|
|
129
|
+
return f"{self.__class__.__name__}(url={self.url!r}, message={str(self)!r})"
|
imdbio/locale.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
SUPPORTED_LOCALES = ("en", "fr-ca", "fr", "hi", "de", "it", "es", "pt", "es-es")
|
|
6
|
+
LOCALE_TO_COUNTRY_CODE = {
|
|
7
|
+
"en": "EN",
|
|
8
|
+
"fr-ca": "FR",
|
|
9
|
+
"fr": "FR",
|
|
10
|
+
"hi": "IN",
|
|
11
|
+
"de": "DE",
|
|
12
|
+
"it": "IT",
|
|
13
|
+
"es": "ES",
|
|
14
|
+
"pt": "PT",
|
|
15
|
+
"es-es": "ES",
|
|
16
|
+
}
|
|
17
|
+
DEFAULT_LOCALE = "en"
|
|
18
|
+
_configured_locale = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_locale(locale: str):
|
|
22
|
+
global _configured_locale
|
|
23
|
+
# accept only a single supported locale string
|
|
24
|
+
if not isinstance(locale, str):
|
|
25
|
+
logger.warning(
|
|
26
|
+
"Invalid locale type: %r. Locale must be a string. Falling back to default '%s'.",
|
|
27
|
+
locale,
|
|
28
|
+
DEFAULT_LOCALE,
|
|
29
|
+
)
|
|
30
|
+
_configured_locale = DEFAULT_LOCALE
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
l = locale.strip()
|
|
34
|
+
if l not in SUPPORTED_LOCALES:
|
|
35
|
+
logger.warning(
|
|
36
|
+
"Locale '%s' is not supported. Falling back to default '%s'.",
|
|
37
|
+
l,
|
|
38
|
+
DEFAULT_LOCALE,
|
|
39
|
+
)
|
|
40
|
+
_configured_locale = DEFAULT_LOCALE
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
_configured_locale = l
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _normalize_locale(lcl: str):
|
|
47
|
+
if lcl not in SUPPORTED_LOCALES:
|
|
48
|
+
logger.warning("Locale '%s' is not supported. Using '%s'", lcl, DEFAULT_LOCALE)
|
|
49
|
+
return DEFAULT_LOCALE
|
|
50
|
+
return lcl
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_locale():
|
|
54
|
+
lcl = _configured_locale or DEFAULT_LOCALE
|
|
55
|
+
lcl = _normalize_locale(lcl)
|
|
56
|
+
return "" if lcl == DEFAULT_LOCALE else lcl
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _retrieve_url_lang(locale=None):
|
|
60
|
+
lcl = locale or _configured_locale or DEFAULT_LOCALE
|
|
61
|
+
lcl = _normalize_locale(lcl)
|
|
62
|
+
return "" if lcl == DEFAULT_LOCALE else lcl
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_country_code_from_lang_locale(locale=None):
|
|
66
|
+
lcl = locale or _configured_locale or DEFAULT_LOCALE
|
|
67
|
+
lcl = _normalize_locale(lcl)
|
|
68
|
+
return LOCALE_TO_COUNTRY_CODE.get(lcl, LOCALE_TO_COUNTRY_CODE[DEFAULT_LOCALE])
|