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.
Files changed (26) hide show
  1. {compuglobal-0.4.2 → compuglobal-0.5.0}/PKG-INFO +2 -2
  2. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/__init__.py +3 -2
  3. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/aio.py +29 -22
  4. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/client.py +57 -10
  5. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/endpoint.py +12 -12
  6. compuglobal-0.5.0/compuglobal/errors.py +42 -0
  7. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/comic.py +7 -7
  8. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/episode.py +6 -0
  9. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/font.py +2 -2
  10. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/frame.py +6 -0
  11. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/overlay.py +6 -7
  12. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/screencap.py +6 -0
  13. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/stream.py +4 -4
  14. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/subtitle.py +1 -1
  15. {compuglobal-0.4.2 → compuglobal-0.5.0}/pyproject.toml +5 -5
  16. compuglobal-0.4.2/compuglobal/errors.py +0 -24
  17. {compuglobal-0.4.2 → compuglobal-0.5.0}/LICENSE +0 -0
  18. {compuglobal-0.4.2 → compuglobal-0.5.0}/README.rst +0 -0
  19. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/__init__.py +0 -0
  20. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/config.py +0 -0
  21. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/discovery.py +0 -0
  22. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/media.py +0 -0
  23. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/api/metadata.py +0 -0
  24. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/__init__.py +0 -0
  25. {compuglobal-0.4.2 → compuglobal-0.5.0}/compuglobal/models/base.py +0 -0
  26. {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.4.2
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.0
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.4.2"
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, optional
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__(self, session: aiohttp.ClientSession, default_format: OverlayFormat | None = None) -> None:
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() + extra_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, optional
99
+ episode : str | None
93
100
  An episode key
94
- timestamp : int | None, optional
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, optional
128
+ season_minimum : int | None
122
129
  The minimum season allowed in the search results
123
- season_maximum : int | None, optional
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, optional
175
+ season_minimum : int | None
169
176
  The minimum season allowed in the search
170
- season_maximum : int | None, optional
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, optional
199
+ season_minimum : int | None
193
200
  Minimum season number allowed in random result
194
- season_maximum : int | None, optional
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, optional
339
+ subtitles : list[Subtitle] | None
333
340
  A list of subtitles to overlay in the comic panel
334
- overlay_format : OverlayFormat | None, optional
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, optional
369
+ subtitles : list[Subtitle] | None
363
370
  The subtitles to overlay in the comic strip
364
- overlay_format : OverlayFormat | list[OverlayFormat] | None, optional
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, optional
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, optional
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, optional
433
+ subtitles : list[Subtitle] | None
427
434
  The subtitles to overlay in the gif
428
- overlay_format : OverlayFormat | list[OverlayFormat] | None, optional
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, optional
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, optional
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, optional
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
- log.error("Non-2xx response %s | GET %s", response.status, url)
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
- log.debug("%s %s | params=%s | body=%s", request.method.value, request.url, request.params, request.body)
104
- if request.method == RequestMethod.POST:
105
- return await self.post_data(request.url, json=request.body)
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
- return await self.get(request.url, params=request.params)
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, optional
37
+ method: RequestMethod
38
38
  The HTTP RequestMethod for the request (GET/POST/PUT)
39
- params: dict[str, Any] | None, optional
39
+ params: dict[str, Any] | None
40
40
  The query parameters to use in the request
41
- body: dict[str, Any] | list[Any] | None, optional
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, optional
64
+ method : RequestMethod
65
65
  The HTTP RequestMethod for the request (GET/POST/PUT)
66
- required_query_params : frozenset[str], optional
66
+ required_query_params : frozenset[str]
67
67
  The required query parameters to use in the request
68
- optional_query_params : frozenset[str], optional
68
+ optional_query_params : frozenset[str]
69
69
  Optional query params that can be used in the request
70
- body_model : type[BaseModel] | None, optional
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, optional
123
+ query : dict[str, Any] | None
124
124
  The query params to use in the url
125
- path_params : dict[str, Any] | None, optional
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, optional
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, optional
159
+ path_params : dict[str, Any] | None
160
160
  The path params to use in the path of the request
161
- body : BaseModel | None, optional
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, optional
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, optional
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
- """Get the base 64 encoded representation of this panel.
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, optional
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, optional
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, optional
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
- """Get the base 64 encoded representation of this comic strip.
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, optional
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: # noqa: D105
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
- @classmethod
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, optional
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 * [cls()]
93
+ return size * [OverlayFormat()]
95
94
 
96
- if isinstance(overlay_format, cls):
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 + [cls()] * (size - len(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, optional
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, optional
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
- """Get the entire caption of the Stream (all overlays) as a string.
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
- """Get the base 64 encoded representation of this stream's overlays.
222
+ """The base 64 encoded representation of this stream's overlays.
223
223
 
224
224
  Returns
225
225
  -------
@@ -38,7 +38,7 @@ class Subtitle(BaseCompuGlobalModel):
38
38
 
39
39
  @property
40
40
  def duration(self) -> int:
41
- """Get the duration of the subtitle in milliseconds.
41
+ """The duration of the subtitle in milliseconds.
42
42
 
43
43
  Returns
44
44
  -------
@@ -12,7 +12,7 @@ license = "MIT"
12
12
  requires-python = ">=3.13"
13
13
  dynamic = ["version", "description"]
14
14
  dependencies = [
15
- "aiohttp>=3.14.0", "pydantic>=2.13.0"
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 = ["CPY001", "D107", "D203", "D213"]
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/*" = ["INP001", "T201", "ANN201", "D103"]
69
- "docs/*" = ["A001", "INP001", "D100"]
70
- "tests/*" = ["S101", "D103", "PLR2004", "S311"]
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