compuglobal 0.4.2__tar.gz → 0.5.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.
- {compuglobal-0.4.2 → compuglobal-0.5.0}/PKG-INFO +2 -2
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/__init__.py +3 -2
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/aio.py +29 -22
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/client.py +57 -10
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/endpoint.py +12 -12
- compuglobal-0.5.0/compuglobal/errors.py +42 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/comic.py +7 -7
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/episode.py +6 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/font.py +2 -2
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/frame.py +6 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/overlay.py +6 -7
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/screencap.py +6 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/stream.py +4 -4
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/subtitle.py +1 -1
- {compuglobal-0.4.2 → compuglobal-0.5.0}/pyproject.toml +5 -5
- compuglobal-0.4.2/compuglobal/errors.py +0 -24
- {compuglobal-0.4.2 → compuglobal-0.5.0}/LICENSE +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/README.rst +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/__init__.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/config.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/discovery.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/media.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/metadata.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/__init__.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/base.py +0 -0
- {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/timestamp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compuglobal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python wrapper for the CGHMC API (Frinkiac, Morbotron Master Of All Science and more.
|
|
5
5
|
Keywords: The Simpsons,Futurama,Rick and Morty,West Wing,Frinkiac,Morbotron,Master of all Science,Capital Beat,API,API Wrapper,CompuGlobal,CompuGlobalHyperMegaNet,CGHMC
|
|
6
6
|
Author: MitchellAW
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: aiohttp>=3.14.
|
|
15
|
+
Requires-Dist: aiohttp>=3.14.1
|
|
16
16
|
Requires-Dist: pydantic>=2.13.0
|
|
17
17
|
Project-URL: Documentation, https://compuglobal.readthedocs.io/en/latest/
|
|
18
18
|
Project-URL: Source, https://github.com/MitchellAW/CompuGlobal
|
|
@@ -8,7 +8,7 @@ from compuglobal.aio import (
|
|
|
8
8
|
MasterOfAllScience,
|
|
9
9
|
Morbotron,
|
|
10
10
|
)
|
|
11
|
-
from compuglobal.errors import APIPageStatusError, NoSearchResultsFoundError
|
|
11
|
+
from compuglobal.errors import APIPageStatusError, MaximumRetriesExceededError, NoSearchResultsFoundError
|
|
12
12
|
from compuglobal.models.comic import ComicLayout, ComicOverlay, ComicPanel, ComicStrip
|
|
13
13
|
from compuglobal.models.episode import Episode, EpisodeMetadata, EpisodeSummary
|
|
14
14
|
from compuglobal.models.font import FontAlignment, FontColor, FontFamily
|
|
@@ -22,7 +22,7 @@ from compuglobal.models.timestamp import Timestamp
|
|
|
22
22
|
__title__ = "compuglobal"
|
|
23
23
|
__author__ = "MitchellAW"
|
|
24
24
|
__license__ = "MIT"
|
|
25
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.5.0"
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"APIPageStatusError",
|
|
@@ -42,6 +42,7 @@ __all__ = [
|
|
|
42
42
|
"FrameResult",
|
|
43
43
|
"Frinkiac",
|
|
44
44
|
"MasterOfAllScience",
|
|
45
|
+
"MaximumRetriesExceededError",
|
|
45
46
|
"Morbotron",
|
|
46
47
|
"NoSearchResultsFoundError",
|
|
47
48
|
"OverlayFormat",
|
|
@@ -35,8 +35,10 @@ class AsyncCompuGlobalAPI:
|
|
|
35
35
|
----------
|
|
36
36
|
session : aiohttp.ClientSession
|
|
37
37
|
The client session to use for all API calls
|
|
38
|
-
default_format : OverlayFormat | None
|
|
38
|
+
default_format : OverlayFormat | None
|
|
39
39
|
The default overlay format to use for all overlays/subtitles
|
|
40
|
+
max_retries : int
|
|
41
|
+
The maximum number of retries for all API requests
|
|
40
42
|
|
|
41
43
|
Attributes
|
|
42
44
|
----------
|
|
@@ -64,20 +66,25 @@ class AsyncCompuGlobalAPI:
|
|
|
64
66
|
media: MediaAPI = MediaAPI()
|
|
65
67
|
metadata: MetadataAPI = MetadataAPI()
|
|
66
68
|
|
|
67
|
-
def __init__(
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
session: aiohttp.ClientSession,
|
|
72
|
+
default_format: OverlayFormat | None = None,
|
|
73
|
+
max_retries: int = 0,
|
|
74
|
+
) -> None:
|
|
68
75
|
extra_fonts = list(self.EXTRA_FONTS)
|
|
69
76
|
if default_format is None:
|
|
70
77
|
chosen_font = extra_fonts[0] if len(extra_fonts) > 0 else FontFamily.IMPACT
|
|
71
78
|
default_format = OverlayFormat(font_family=chosen_font)
|
|
72
79
|
|
|
73
|
-
allowed_fonts = FontFamily.universal_fonts()
|
|
80
|
+
allowed_fonts = extra_fonts + FontFamily.universal_fonts()
|
|
74
81
|
|
|
75
82
|
self.config = CompuGlobalAPIConfig(
|
|
76
83
|
title=self.TITLE,
|
|
77
84
|
allowed_fonts=allowed_fonts,
|
|
78
85
|
default_format=default_format,
|
|
79
86
|
)
|
|
80
|
-
self.client = CompuGlobalAPIClient(base_url=self.BASE_URL, session=session)
|
|
87
|
+
self.client = CompuGlobalAPIClient(base_url=self.BASE_URL, session=session, max_retries=max_retries)
|
|
81
88
|
|
|
82
89
|
async def get_screencap(
|
|
83
90
|
self,
|
|
@@ -89,9 +96,9 @@ class AsyncCompuGlobalAPI:
|
|
|
89
96
|
|
|
90
97
|
Parameters
|
|
91
98
|
----------
|
|
92
|
-
episode : str | None
|
|
99
|
+
episode : str | None
|
|
93
100
|
An episode key
|
|
94
|
-
timestamp : int | None
|
|
101
|
+
timestamp : int | None
|
|
95
102
|
A timestamp of the screencap
|
|
96
103
|
|
|
97
104
|
Returns
|
|
@@ -118,9 +125,9 @@ class AsyncCompuGlobalAPI:
|
|
|
118
125
|
----------
|
|
119
126
|
search_text : str
|
|
120
127
|
The search text to query
|
|
121
|
-
season_minimum : int | None
|
|
128
|
+
season_minimum : int | None
|
|
122
129
|
The minimum season allowed in the search results
|
|
123
|
-
season_maximum : int | None
|
|
130
|
+
season_maximum : int | None
|
|
124
131
|
The maximum season allowed in the search results
|
|
125
132
|
|
|
126
133
|
Returns
|
|
@@ -165,9 +172,9 @@ class AsyncCompuGlobalAPI:
|
|
|
165
172
|
----------
|
|
166
173
|
search_text : str
|
|
167
174
|
The search text to query
|
|
168
|
-
season_minimum : int | None
|
|
175
|
+
season_minimum : int | None
|
|
169
176
|
The minimum season allowed in the search
|
|
170
|
-
season_maximum : int | None
|
|
177
|
+
season_maximum : int | None
|
|
171
178
|
The maximum season allowed in the search
|
|
172
179
|
|
|
173
180
|
Returns
|
|
@@ -189,9 +196,9 @@ class AsyncCompuGlobalAPI:
|
|
|
189
196
|
|
|
190
197
|
Parameters
|
|
191
198
|
----------
|
|
192
|
-
season_minimum : int | None
|
|
199
|
+
season_minimum : int | None
|
|
193
200
|
Minimum season number allowed in random result
|
|
194
|
-
season_maximum : int | None
|
|
201
|
+
season_maximum : int | None
|
|
195
202
|
Maximum season number allowed in random result
|
|
196
203
|
|
|
197
204
|
Returns
|
|
@@ -329,9 +336,9 @@ class AsyncCompuGlobalAPI:
|
|
|
329
336
|
----------
|
|
330
337
|
screencap : Screencap
|
|
331
338
|
The screencap to use in the comic panel
|
|
332
|
-
subtitles : list[Subtitle] | None
|
|
339
|
+
subtitles : list[Subtitle] | None
|
|
333
340
|
A list of subtitles to overlay in the comic panel
|
|
334
|
-
overlay_format : OverlayFormat | None
|
|
341
|
+
overlay_format : OverlayFormat | None
|
|
335
342
|
The formatting to use in the comic panel overlay (subtitle)
|
|
336
343
|
|
|
337
344
|
Returns
|
|
@@ -359,9 +366,9 @@ class AsyncCompuGlobalAPI:
|
|
|
359
366
|
----------
|
|
360
367
|
screencap : Screencap
|
|
361
368
|
The screencap to use in the comic strip
|
|
362
|
-
subtitles : list[Subtitle] | None
|
|
369
|
+
subtitles : list[Subtitle] | None
|
|
363
370
|
The subtitles to overlay in the comic strip
|
|
364
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
371
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
365
372
|
The formatting to use in the comic strip overlays (subtitleS). See :meth:`OverlayFormat.normalise` for
|
|
366
373
|
full details on how formats are resolved.
|
|
367
374
|
|
|
@@ -389,9 +396,9 @@ class AsyncCompuGlobalAPI:
|
|
|
389
396
|
----------
|
|
390
397
|
screencap : Screencap
|
|
391
398
|
The screencap to make the comic with
|
|
392
|
-
subtitles : list[Subtitle] | None
|
|
399
|
+
subtitles : list[Subtitle] | None
|
|
393
400
|
The subtitles to override in the comic maker, by default None
|
|
394
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
401
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
395
402
|
The subtitle/overlay formatting to override in the comic maker, by default None
|
|
396
403
|
|
|
397
404
|
Returns
|
|
@@ -423,9 +430,9 @@ class AsyncCompuGlobalAPI:
|
|
|
423
430
|
----------
|
|
424
431
|
screencap : Screencap
|
|
425
432
|
The screencap to use for the gif
|
|
426
|
-
subtitles : list[Subtitle] | None
|
|
433
|
+
subtitles : list[Subtitle] | None
|
|
427
434
|
The subtitles to overlay in the gif
|
|
428
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
435
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
429
436
|
The formatting to use in the gif overlays (subtitles). See :meth:`OverlayFormat.normalise` for
|
|
430
437
|
full details on how formats are resolved.
|
|
431
438
|
|
|
@@ -463,9 +470,9 @@ class AsyncCompuGlobalAPI:
|
|
|
463
470
|
----------
|
|
464
471
|
screencap : Screencap
|
|
465
472
|
The screencap to make the comic with
|
|
466
|
-
subtitles : list[Subtitle] | None
|
|
473
|
+
subtitles : list[Subtitle] | None
|
|
467
474
|
The subtitles to override in the comic maker, by default None
|
|
468
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
475
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
469
476
|
The subtitle/overlay formatting to override in the comic maker, by default None
|
|
470
477
|
|
|
471
478
|
Returns
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Module for handling API requests to CGHMC APIs."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
from http import HTTPStatus
|
|
5
6
|
from typing import Any
|
|
@@ -7,7 +8,7 @@ from typing import Any
|
|
|
7
8
|
from aiohttp import ClientSession
|
|
8
9
|
|
|
9
10
|
from compuglobal.api.endpoint import PreparedRequest, RequestMethod
|
|
10
|
-
from compuglobal.errors import APIPageStatusError
|
|
11
|
+
from compuglobal.errors import APIPageStatusError, MaximumRetriesExceededError
|
|
11
12
|
|
|
12
13
|
log = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -21,12 +22,15 @@ class CompuGlobalAPIClient:
|
|
|
21
22
|
The base URL of the API (e.g. https://frinkiac.com)
|
|
22
23
|
session : ClientSession
|
|
23
24
|
The client session to use for all API requests
|
|
25
|
+
max_retries : int
|
|
26
|
+
The maximum number of retries for each request before raising an :class:`APIPageStatusError`
|
|
24
27
|
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
|
-
def __init__(self, base_url: str, session: ClientSession) -> None:
|
|
30
|
+
def __init__(self, base_url: str, session: ClientSession, max_retries: int = 0) -> None:
|
|
28
31
|
self.base_url = base_url
|
|
29
32
|
self.session = session
|
|
33
|
+
self.max_retries = max_retries
|
|
30
34
|
|
|
31
35
|
async def get(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any] | list[Any]:
|
|
32
36
|
"""Get the JSON response from the API using the given url, and params.
|
|
@@ -35,7 +39,7 @@ class CompuGlobalAPIClient:
|
|
|
35
39
|
----------
|
|
36
40
|
url : str
|
|
37
41
|
The url to use in the request
|
|
38
|
-
params : dict[str, Any] | None
|
|
42
|
+
params : dict[str, Any] | None
|
|
39
43
|
The query params to use in the request
|
|
40
44
|
|
|
41
45
|
Returns
|
|
@@ -54,7 +58,11 @@ class CompuGlobalAPIClient:
|
|
|
54
58
|
log.debug("Response %s | GET %s", response.status, url)
|
|
55
59
|
return await response.json()
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
if response.status == HTTPStatus.TOO_MANY_REQUESTS:
|
|
62
|
+
retry_after = int(response.headers.get("Retry-After", 60))
|
|
63
|
+
raise APIPageStatusError(response.status, self.base_url, retry_after=retry_after)
|
|
64
|
+
|
|
65
|
+
log.error("Non-2xx response %s | POST %s | Headers %s", response.status, url, response.headers)
|
|
58
66
|
raise APIPageStatusError(response.status, self.base_url)
|
|
59
67
|
|
|
60
68
|
async def post_data(self, url: str, json: dict[str, Any] | list[Any] | None) -> str:
|
|
@@ -83,7 +91,11 @@ class CompuGlobalAPIClient:
|
|
|
83
91
|
log.debug("Response %s | POST %s", response.status, url)
|
|
84
92
|
return await response.text()
|
|
85
93
|
|
|
86
|
-
log.error("Non-2xx response %s | POST %s")
|
|
94
|
+
log.error("Non-2xx response %s | POST %s | Headers %s", response.status, url, response.headers)
|
|
95
|
+
if response.status == HTTPStatus.TOO_MANY_REQUESTS:
|
|
96
|
+
retry_after = int(response.headers.get("Retry-After", 60))
|
|
97
|
+
raise APIPageStatusError(response.status, self.base_url, retry_after=retry_after)
|
|
98
|
+
|
|
87
99
|
raise APIPageStatusError(response.status, self.base_url)
|
|
88
100
|
|
|
89
101
|
async def handle_request(self, request: PreparedRequest) -> str | dict[str, Any] | list[Any]:
|
|
@@ -99,9 +111,44 @@ class CompuGlobalAPIClient:
|
|
|
99
111
|
str | dict[str, Any] | list[Any]
|
|
100
112
|
The json response from the API
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
Raises
|
|
115
|
+
------
|
|
116
|
+
APIPageStatusError
|
|
117
|
+
Raised immediately for non-2xx responses that are not retryable (e.g. anything other than a 429).
|
|
118
|
+
MaximumRetriesExceededError
|
|
119
|
+
Raised when the maximum number of retries is exceeded.
|
|
106
120
|
|
|
107
|
-
|
|
121
|
+
"""
|
|
122
|
+
for attempt in range(self.max_retries + 1):
|
|
123
|
+
try:
|
|
124
|
+
log.debug(
|
|
125
|
+
"%s %s | params=%s | body=%s",
|
|
126
|
+
request.method.value,
|
|
127
|
+
request.url,
|
|
128
|
+
request.params,
|
|
129
|
+
request.body,
|
|
130
|
+
)
|
|
131
|
+
if request.method == RequestMethod.POST:
|
|
132
|
+
return await self.post_data(request.url, json=request.body)
|
|
133
|
+
|
|
134
|
+
return await self.get(request.url, params=request.params)
|
|
135
|
+
|
|
136
|
+
except APIPageStatusError as error:
|
|
137
|
+
if error.page_status != HTTPStatus.TOO_MANY_REQUESTS or attempt >= self.max_retries:
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
log.warning(
|
|
141
|
+
"Rate limited (429) | %s %s | retrying in %.2fs (attempt %d/%d)",
|
|
142
|
+
request.method.value,
|
|
143
|
+
request.url,
|
|
144
|
+
error.retry_after,
|
|
145
|
+
attempt + 1,
|
|
146
|
+
self.max_retries + 1,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if error.retry_after is not None:
|
|
150
|
+
# Add a buffer of 50-1000ms depending on retry amount
|
|
151
|
+
buffer = max(0.05, min(1, error.retry_after * 0.02))
|
|
152
|
+
await asyncio.sleep(error.retry_after + buffer)
|
|
153
|
+
|
|
154
|
+
raise MaximumRetriesExceededError
|
|
@@ -34,11 +34,11 @@ class PreparedRequest:
|
|
|
34
34
|
----------
|
|
35
35
|
url: str
|
|
36
36
|
The url of the request
|
|
37
|
-
method: RequestMethod
|
|
37
|
+
method: RequestMethod
|
|
38
38
|
The HTTP RequestMethod for the request (GET/POST/PUT)
|
|
39
|
-
params: dict[str, Any] | None
|
|
39
|
+
params: dict[str, Any] | None
|
|
40
40
|
The query parameters to use in the request
|
|
41
|
-
body: dict[str, Any] | list[Any] | None
|
|
41
|
+
body: dict[str, Any] | list[Any] | None
|
|
42
42
|
The JSON request body to post
|
|
43
43
|
|
|
44
44
|
"""
|
|
@@ -61,13 +61,13 @@ class Endpoint:
|
|
|
61
61
|
----------
|
|
62
62
|
path : str
|
|
63
63
|
The url path containing any path parameter names
|
|
64
|
-
method : RequestMethod
|
|
64
|
+
method : RequestMethod
|
|
65
65
|
The HTTP RequestMethod for the request (GET/POST/PUT)
|
|
66
|
-
required_query_params : frozenset[str]
|
|
66
|
+
required_query_params : frozenset[str]
|
|
67
67
|
The required query parameters to use in the request
|
|
68
|
-
optional_query_params : frozenset[str]
|
|
68
|
+
optional_query_params : frozenset[str]
|
|
69
69
|
Optional query params that can be used in the request
|
|
70
|
-
body_model : type[BaseModel] | None
|
|
70
|
+
body_model : type[BaseModel] | None
|
|
71
71
|
The pydantic model for the json body
|
|
72
72
|
|
|
73
73
|
"""
|
|
@@ -120,9 +120,9 @@ class Endpoint:
|
|
|
120
120
|
----------
|
|
121
121
|
base_url : str
|
|
122
122
|
The base url of the API
|
|
123
|
-
query : dict[str, Any] | None
|
|
123
|
+
query : dict[str, Any] | None
|
|
124
124
|
The query params to use in the url
|
|
125
|
-
path_params : dict[str, Any] | None
|
|
125
|
+
path_params : dict[str, Any] | None
|
|
126
126
|
The path params to use in the url
|
|
127
127
|
|
|
128
128
|
Returns
|
|
@@ -154,11 +154,11 @@ class Endpoint:
|
|
|
154
154
|
----------
|
|
155
155
|
base_url : str
|
|
156
156
|
The base url of the API
|
|
157
|
-
query : dict[str, Any] | None
|
|
157
|
+
query : dict[str, Any] | None
|
|
158
158
|
The query params to use in the params of the request
|
|
159
|
-
path_params : dict[str, Any] | None
|
|
159
|
+
path_params : dict[str, Any] | None
|
|
160
160
|
The path params to use in the path of the request
|
|
161
|
-
body : BaseModel | None
|
|
161
|
+
body : BaseModel | None
|
|
162
162
|
The json body model to use in the request
|
|
163
163
|
|
|
164
164
|
Returns
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""All errors/exceptions returned by CompuGlobal APIs."""
|
|
2
|
+
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NoSearchResultsFoundError(Exception):
|
|
7
|
+
"""Raised when no search results are found."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str = "No search results found.") -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MaximumRetriesExceededError(Exception):
|
|
14
|
+
"""Raised when the maximum number of retries is exceeded."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str = "Maximum number of retries exceeded.") -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class APIPageStatusError(Exception):
|
|
21
|
+
"""Raised when the status code for the API is not 200.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
page_status : int
|
|
26
|
+
The page status code returned by the API.
|
|
27
|
+
url : str
|
|
28
|
+
The base url that raised the error.
|
|
29
|
+
retry_after : int | None
|
|
30
|
+
Number of seconds to wait before retrying if error was a 429.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, page_status: int, url: str, retry_after: int | None = None) -> None:
|
|
35
|
+
self.page_status = page_status
|
|
36
|
+
self.url = url
|
|
37
|
+
self.retry_after = retry_after
|
|
38
|
+
|
|
39
|
+
msg = f"Error {page_status}. {url} may be down."
|
|
40
|
+
if page_status == HTTPStatus.TOO_MANY_REQUESTS and self.retry_after is not None:
|
|
41
|
+
msg += f" Retry after {retry_after} seconds."
|
|
42
|
+
super().__init__(msg)
|
|
@@ -124,7 +124,7 @@ class ComicOverlay(BaseCompuGlobalModel):
|
|
|
124
124
|
----------
|
|
125
125
|
subtitles : list[Subtitle]
|
|
126
126
|
A list of subtitles
|
|
127
|
-
overlay_format : OverlayFormat | None
|
|
127
|
+
overlay_format : OverlayFormat | None
|
|
128
128
|
The format to use in the overlay
|
|
129
129
|
|
|
130
130
|
Returns
|
|
@@ -171,7 +171,7 @@ class ComicPanel(BaseCompuGlobalModel):
|
|
|
171
171
|
----------
|
|
172
172
|
screencap : Screencap
|
|
173
173
|
Screencap to use for the comic panel
|
|
174
|
-
overlay_format : OverlayFormat | None
|
|
174
|
+
overlay_format : OverlayFormat | None
|
|
175
175
|
The format to use in the comic panel overlay.
|
|
176
176
|
|
|
177
177
|
Returns
|
|
@@ -186,7 +186,7 @@ class ComicPanel(BaseCompuGlobalModel):
|
|
|
186
186
|
|
|
187
187
|
@property
|
|
188
188
|
def encoded(self) -> str:
|
|
189
|
-
"""
|
|
189
|
+
"""The base 64 encoded representation of this panel.
|
|
190
190
|
|
|
191
191
|
Returns
|
|
192
192
|
-------
|
|
@@ -208,7 +208,7 @@ class ComicStrip(BaseCompuGlobalModel, frozen=False):
|
|
|
208
208
|
----------
|
|
209
209
|
panels : list[ComicPanel]
|
|
210
210
|
The list of ComicPanels to use in the comic strip
|
|
211
|
-
layout : ComicLayout | None
|
|
211
|
+
layout : ComicLayout | None
|
|
212
212
|
The layout to use when displaying the panels
|
|
213
213
|
|
|
214
214
|
"""
|
|
@@ -233,7 +233,7 @@ class ComicStrip(BaseCompuGlobalModel, frozen=False):
|
|
|
233
233
|
----------
|
|
234
234
|
screencap : Screencap
|
|
235
235
|
The screencap to use for the comic strip
|
|
236
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
236
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
237
237
|
The format(s) to use in the overlays. See :meth:`OverlayFormat.normalise` for
|
|
238
238
|
full details on how formats are resolved.
|
|
239
239
|
|
|
@@ -266,7 +266,7 @@ class ComicStrip(BaseCompuGlobalModel, frozen=False):
|
|
|
266
266
|
----------
|
|
267
267
|
subtitles : list[Subtitle]
|
|
268
268
|
The subtitles to use for the overlays
|
|
269
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
269
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
270
270
|
The format(s) to use in the overlays. See :meth:`OverlayFormat.normalise` for
|
|
271
271
|
full details on how formats are resolved.
|
|
272
272
|
|
|
@@ -284,7 +284,7 @@ class ComicStrip(BaseCompuGlobalModel, frozen=False):
|
|
|
284
284
|
|
|
285
285
|
@property
|
|
286
286
|
def encoded(self) -> str:
|
|
287
|
-
"""
|
|
287
|
+
"""The base 64 encoded representation of this comic strip.
|
|
288
288
|
|
|
289
289
|
Returns
|
|
290
290
|
-------
|
|
@@ -30,6 +30,10 @@ class EpisodeMetadata(BaseCompuGlobalModel):
|
|
|
30
30
|
The original air date of the episode (yyyy-mm-dd)
|
|
31
31
|
wiki_link : str
|
|
32
32
|
The wikipedia link for the episode
|
|
33
|
+
video_width : int
|
|
34
|
+
The width of the video in pixels
|
|
35
|
+
video_height : int
|
|
36
|
+
The height of the video in pixels
|
|
33
37
|
|
|
34
38
|
"""
|
|
35
39
|
|
|
@@ -42,6 +46,8 @@ class EpisodeMetadata(BaseCompuGlobalModel):
|
|
|
42
46
|
writer: str = Field(alias="Writer")
|
|
43
47
|
original_air_date: str = Field(alias="OriginalAirDate")
|
|
44
48
|
wiki_link: str = Field(alias="WikiLink")
|
|
49
|
+
video_width: int = Field(alias="VideoWidth", ge=0, default=480)
|
|
50
|
+
video_height: int = Field(alias="VideoHeight", ge=0, default=360)
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
class EpisodeSummary(BaseCompuGlobalModel):
|
|
@@ -41,8 +41,8 @@ class FontFamily(StrEnum):
|
|
|
41
41
|
return [
|
|
42
42
|
FontFamily.IMPACT,
|
|
43
43
|
FontFamily.COMIC_NEUE,
|
|
44
|
-
FontFamily.JOST,
|
|
45
44
|
FontFamily.PACIFICO,
|
|
45
|
+
FontFamily.JOST,
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
|
|
@@ -70,7 +70,7 @@ class FontColor(BaseCompuGlobalModel):
|
|
|
70
70
|
The amount of green in the color (0-255)
|
|
71
71
|
blue : int
|
|
72
72
|
The amount of blue in the color (0-255)
|
|
73
|
-
alpha : int
|
|
73
|
+
alpha : int
|
|
74
74
|
The amount of alpha transparency in the color (0-255)
|
|
75
75
|
|
|
76
76
|
"""
|
|
@@ -17,12 +17,18 @@ class Frame(BaseCompuGlobalModel):
|
|
|
17
17
|
The episode key (S01E01)
|
|
18
18
|
timestamp : int
|
|
19
19
|
The timestamp of the frame
|
|
20
|
+
video_width : int
|
|
21
|
+
The width of the video in pixels
|
|
22
|
+
video_height : int
|
|
23
|
+
The height of the video in pixels
|
|
20
24
|
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
id: int = Field(alias="Id")
|
|
24
28
|
key: str = Field(alias="Episode")
|
|
25
29
|
timestamp: int = Field(alias="Timestamp", ge=0)
|
|
30
|
+
video_width: int = Field(alias="VideoWidth", ge=0, default=480)
|
|
31
|
+
video_height: int = Field(alias="VideoHeight", ge=0, default=360)
|
|
26
32
|
|
|
27
33
|
@property
|
|
28
34
|
def timecode(self) -> str:
|
|
@@ -36,7 +36,7 @@ class OverlayFormat(BaseCompuGlobalModel):
|
|
|
36
36
|
text_alignment: FontAlignment = Field(default=FontAlignment.ALIGN_CENTER)
|
|
37
37
|
all_caps: bool = Field(default=True)
|
|
38
38
|
|
|
39
|
-
def __str__(self) -> str: #
|
|
39
|
+
def __str__(self) -> str: # ruff: ignore[undocumented-magic-method]
|
|
40
40
|
return str(self.model_dump(exclude_defaults=True))
|
|
41
41
|
|
|
42
42
|
@property
|
|
@@ -63,9 +63,8 @@ class OverlayFormat(BaseCompuGlobalModel):
|
|
|
63
63
|
"""
|
|
64
64
|
return self.font_color.rgba
|
|
65
65
|
|
|
66
|
-
@
|
|
66
|
+
@staticmethod
|
|
67
67
|
def normalise(
|
|
68
|
-
cls,
|
|
69
68
|
overlay_format: "OverlayFormat | list[OverlayFormat] | None",
|
|
70
69
|
size: int,
|
|
71
70
|
) -> "list[OverlayFormat]":
|
|
@@ -73,7 +72,7 @@ class OverlayFormat(BaseCompuGlobalModel):
|
|
|
73
72
|
|
|
74
73
|
Parameters
|
|
75
74
|
----------
|
|
76
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
75
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
77
76
|
The format(s) to use in the overlays:
|
|
78
77
|
|
|
79
78
|
- If ``None``, default formatting is applied to all overlays
|
|
@@ -91,12 +90,12 @@ class OverlayFormat(BaseCompuGlobalModel):
|
|
|
91
90
|
|
|
92
91
|
"""
|
|
93
92
|
if overlay_format is None:
|
|
94
|
-
return size * [
|
|
93
|
+
return size * [OverlayFormat()]
|
|
95
94
|
|
|
96
|
-
if isinstance(overlay_format,
|
|
95
|
+
if isinstance(overlay_format, OverlayFormat):
|
|
97
96
|
return size * [overlay_format]
|
|
98
97
|
|
|
99
98
|
if len(overlay_format) < size:
|
|
100
|
-
return overlay_format + [
|
|
99
|
+
return overlay_format + [OverlayFormat()] * (size - len(overlay_format))
|
|
101
100
|
|
|
102
101
|
return overlay_format[:size]
|
|
@@ -22,6 +22,10 @@ class ScreencapMoment(BaseCompuGlobalModel):
|
|
|
22
22
|
The content of the subtitle
|
|
23
23
|
title : str
|
|
24
24
|
The title of the episode in the snapshot
|
|
25
|
+
video_width : int
|
|
26
|
+
The width of the video in pixels
|
|
27
|
+
video_height : int
|
|
28
|
+
The height of the video in pixels
|
|
25
29
|
|
|
26
30
|
"""
|
|
27
31
|
|
|
@@ -29,6 +33,8 @@ class ScreencapMoment(BaseCompuGlobalModel):
|
|
|
29
33
|
timestamp: int = Field(alias="Timestamp", ge=0)
|
|
30
34
|
content: str = Field(alias="Content")
|
|
31
35
|
title: str = Field(alias="Title")
|
|
36
|
+
video_width: int = Field(alias="VideoWidth", ge=0, default=480)
|
|
37
|
+
video_height: int = Field(alias="VideoHeight", ge=0, default=360)
|
|
32
38
|
|
|
33
39
|
@property
|
|
34
40
|
def key(self) -> str:
|
|
@@ -152,7 +152,7 @@ class Stream(BaseCompuGlobalModel):
|
|
|
152
152
|
----------
|
|
153
153
|
screencap : Screencap
|
|
154
154
|
The screencap to use for the Stream
|
|
155
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
155
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
156
156
|
The format(s) to use in the overlays. See :meth:`OverlayFormat.normalise` for
|
|
157
157
|
full details on how formats are resolved.
|
|
158
158
|
|
|
@@ -183,7 +183,7 @@ class Stream(BaseCompuGlobalModel):
|
|
|
183
183
|
----------
|
|
184
184
|
screencap : Screencap
|
|
185
185
|
The screencap to use for the overlays
|
|
186
|
-
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
186
|
+
overlay_format : OverlayFormat | list[OverlayFormat] | None
|
|
187
187
|
The format(s) to use in the overlays. See :meth:`OverlayFormat.normalise` for
|
|
188
188
|
full details on how formats are resolved.
|
|
189
189
|
|
|
@@ -207,7 +207,7 @@ class Stream(BaseCompuGlobalModel):
|
|
|
207
207
|
|
|
208
208
|
@property
|
|
209
209
|
def caption(self) -> str:
|
|
210
|
-
"""
|
|
210
|
+
"""The entire caption of the Stream (all overlays) as a string.
|
|
211
211
|
|
|
212
212
|
Returns
|
|
213
213
|
-------
|
|
@@ -219,7 +219,7 @@ class Stream(BaseCompuGlobalModel):
|
|
|
219
219
|
|
|
220
220
|
@property
|
|
221
221
|
def encoded(self) -> str:
|
|
222
|
-
"""
|
|
222
|
+
"""The base 64 encoded representation of this stream's overlays.
|
|
223
223
|
|
|
224
224
|
Returns
|
|
225
225
|
-------
|
|
@@ -12,7 +12,7 @@ license = "MIT"
|
|
|
12
12
|
requires-python = ">=3.13"
|
|
13
13
|
dynamic = ["version", "description"]
|
|
14
14
|
dependencies = [
|
|
15
|
-
"aiohttp>=3.14.
|
|
15
|
+
"aiohttp>=3.14.1", "pydantic>=2.13.0"
|
|
16
16
|
]
|
|
17
17
|
classifiers = [
|
|
18
18
|
"Programming Language :: Python :: 3.13",
|
|
@@ -62,12 +62,12 @@ line-length = 120
|
|
|
62
62
|
[tool.ruff.lint]
|
|
63
63
|
preview = true
|
|
64
64
|
select = ["ALL"]
|
|
65
|
-
ignore = ["
|
|
65
|
+
ignore = ["missing-copyright-notice", "undocumented-public-init", "incorrect-blank-line-before-class", "multi-line-summary-second-line"]
|
|
66
66
|
|
|
67
67
|
[tool.ruff.lint.per-file-ignores]
|
|
68
|
-
"examples/*" = ["
|
|
69
|
-
"docs/*" = ["
|
|
70
|
-
"tests/*" = ["
|
|
68
|
+
"examples/*" = ["implicit-namespace-package", "print", "missing-return-type-undocumented-public-function", "undocumented-public-function"]
|
|
69
|
+
"docs/*" = ["builtin-variable-shadowing", "implicit-namespace-package", "undocumented-public-module"]
|
|
70
|
+
"tests/*" = ["assert", "undocumented-public-function", "magic-value-comparison", "suspicious-non-cryptographic-random-usage"]
|
|
71
71
|
|
|
72
72
|
[tool.pyrefly]
|
|
73
73
|
project-includes = [
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"""All errors/exceptions returned by CompuGlobal APIs."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class NoSearchResultsFoundError(Exception):
|
|
5
|
-
"""Raised when no search results are found."""
|
|
6
|
-
|
|
7
|
-
def __init__(self, message: str = "No search results found.") -> None:
|
|
8
|
-
super().__init__(message)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class APIPageStatusError(Exception):
|
|
12
|
-
"""Raised when the status code for the API is not 200.
|
|
13
|
-
|
|
14
|
-
Parameters
|
|
15
|
-
----------
|
|
16
|
-
page_status : int
|
|
17
|
-
The page status code returned by the API.
|
|
18
|
-
url : str
|
|
19
|
-
The base url that raised the error.
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(self, page_status: int, url: str) -> None:
|
|
24
|
-
super().__init__(f"Error {page_status}. {url} may be down.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|