compuglobal 0.3.6__tar.gz → 0.3.8__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 (24) hide show
  1. {compuglobal-0.3.6 → compuglobal-0.3.8}/PKG-INFO +9 -4
  2. {compuglobal-0.3.6 → compuglobal-0.3.8}/README.rst +7 -2
  3. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/__init__.py +3 -1
  4. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/aio.py +33 -7
  5. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/discovery.py +4 -2
  6. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/endpoint.py +9 -6
  7. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/media.py +3 -3
  8. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/metadata.py +1 -1
  9. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/frame.py +2 -4
  10. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/screencap.py +26 -3
  11. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/subtitle.py +3 -2
  12. compuglobal-0.3.8/compuglobal/models/timestamp.py +76 -0
  13. {compuglobal-0.3.6 → compuglobal-0.3.8}/pyproject.toml +1 -1
  14. {compuglobal-0.3.6 → compuglobal-0.3.8}/LICENSE +0 -0
  15. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/__init__.py +0 -0
  16. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/client.py +0 -0
  17. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/api/config.py +0 -0
  18. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/errors.py +0 -0
  19. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/__init__.py +0 -0
  20. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/base.py +0 -0
  21. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/comic.py +0 -0
  22. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/episode.py +0 -0
  23. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/font.py +0 -0
  24. {compuglobal-0.3.6 → compuglobal-0.3.8}/compuglobal/models/stream.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compuglobal
3
- Version: 0.3.6
3
+ Version: 0.3.8
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.13.0
15
+ Requires-Dist: aiohttp>=3.14.0
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
@@ -25,10 +25,15 @@ CompuGlobal
25
25
 
26
26
  .. image:: https://img.shields.io/pypi/v/compuglobal.svg
27
27
  :target: https://pypi.python.org/pypi/compuglobal
28
+
28
29
  .. image:: https://img.shields.io/pypi/pyversions/compuglobal.svg
29
30
  :target: https://pypi.python.org/pypi/compuglobal
30
- .. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
31
- :target: http://makeapullrequest.com
31
+
32
+ .. image:: https://img.shields.io/github/actions/workflow/status/MitchellAW/CompuGlobal/test.yml?label=tests
33
+ :target: https://github.com/MitchellAW/CompuGlobal/actions/workflows/test.yml
34
+
35
+ .. image:: https://img.shields.io/github/license/mitchellaw/compuglobal
36
+ :target: https://github.com/MitchellAW/CompuGlobal/blob/main/LICENSE
32
37
 
33
38
  Python wrapper for all endpoints of the following undocumented APIs:
34
39
 
@@ -6,10 +6,15 @@ CompuGlobal
6
6
 
7
7
  .. image:: https://img.shields.io/pypi/v/compuglobal.svg
8
8
  :target: https://pypi.python.org/pypi/compuglobal
9
+
9
10
  .. image:: https://img.shields.io/pypi/pyversions/compuglobal.svg
10
11
  :target: https://pypi.python.org/pypi/compuglobal
11
- .. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
12
- :target: http://makeapullrequest.com
12
+
13
+ .. image:: https://img.shields.io/github/actions/workflow/status/MitchellAW/CompuGlobal/test.yml?label=tests
14
+ :target: https://github.com/MitchellAW/CompuGlobal/actions/workflows/test.yml
15
+
16
+ .. image:: https://img.shields.io/github/license/mitchellaw/compuglobal
17
+ :target: https://github.com/MitchellAW/CompuGlobal/blob/main/LICENSE
13
18
 
14
19
  Python wrapper for all endpoints of the following undocumented APIs:
15
20
 
@@ -16,11 +16,12 @@ from compuglobal.models.frame import Frame, FrameResult
16
16
  from compuglobal.models.screencap import Screencap, ScreencapMoment
17
17
  from compuglobal.models.stream import Stream, StreamOverlay
18
18
  from compuglobal.models.subtitle import Subtitle
19
+ from compuglobal.models.timestamp import Timestamp
19
20
 
20
21
  __title__ = "compuglobal"
21
22
  __author__ = "MitchellAW"
22
23
  __license__ = "MIT"
23
- __version__ = "0.3.6"
24
+ __version__ = "0.3.8"
24
25
 
25
26
  __all__ = [
26
27
  "APIPageStatusError",
@@ -47,4 +48,5 @@ __all__ = [
47
48
  "Stream",
48
49
  "StreamOverlay",
49
50
  "Subtitle",
51
+ "Timestamp",
50
52
  ]
@@ -91,13 +91,22 @@ class AsyncCompuGlobalAPI:
91
91
  caption = await self.client.handle_request(request)
92
92
  return Screencap.model_validate(caption)
93
93
 
94
- async def search(self, search_text: str) -> list[FrameResult]:
94
+ async def search(
95
+ self,
96
+ search_text: str,
97
+ season_minimum: int | None = None,
98
+ season_maximum: int | None = None,
99
+ ) -> list[FrameResult]:
95
100
  """Perform a search of the given search text and returns a list of all the Frames.
96
101
 
97
102
  Parameters
98
103
  ----------
99
104
  search_text : str
100
105
  The search text to query
106
+ season_minimum: int | None, optional
107
+ The minimum season allowed in the search results
108
+ season_maximum: int | None, optional
109
+ The maximum season allowed in the search results
101
110
 
102
111
  Returns
103
112
  -------
@@ -110,9 +119,11 @@ class AsyncCompuGlobalAPI:
110
119
  Raises an error if no search results are found
111
120
 
112
121
  """
113
- params = {"q": search_text}
122
+ optional_params = {"smin": season_minimum, "smax": season_maximum}
123
+ query = {"q": search_text}
124
+ query |= {k: v for k, v in optional_params.items() if v is not None}
114
125
 
115
- request = self.discovery.SEARCH.build_request(self.client.base_url, query=params)
126
+ request = self.discovery.SEARCH.build_request(self.client.base_url, query=query)
116
127
  search_results = await self.client.handle_request(request)
117
128
 
118
129
  if len(search_results) > 0:
@@ -120,13 +131,22 @@ class AsyncCompuGlobalAPI:
120
131
 
121
132
  raise NoSearchResultsFoundError
122
133
 
123
- async def search_for_screencap(self, search_text: str) -> Screencap:
134
+ async def search_for_screencap(
135
+ self,
136
+ search_text: str,
137
+ season_minimum: int | None = None,
138
+ season_maximum: int | None = None,
139
+ ) -> Screencap:
124
140
  """Perform a search of the given search text and returns the top result.
125
141
 
126
142
  Parameters
127
143
  ----------
128
144
  search_text : str
129
145
  The search text to query
146
+ season_minimum: int | None, optional
147
+ The minimum season allowed in the search
148
+ season_maximum: int | None, optional
149
+ The maximum season allowed in the search
130
150
 
131
151
  Returns
132
152
  -------
@@ -134,11 +154,15 @@ class AsyncCompuGlobalAPI:
134
154
  The screencap of the top search result
135
155
 
136
156
  """
137
- search_results = await self.search(search_text)
157
+ search_results = await self.search(search_text, season_minimum=season_minimum, season_maximum=season_maximum)
138
158
  result = search_results[0]
139
159
  return await self.get_screencap(result.key, result.timestamp)
140
160
 
141
- async def get_random_screencap(self) -> Screencap:
161
+ async def get_random_screencap(
162
+ self,
163
+ season_minimum: int | None = None,
164
+ season_maximum: int | None = None,
165
+ ) -> Screencap:
142
166
  """Get a random TV Show screencap.
143
167
 
144
168
  Returns
@@ -147,7 +171,9 @@ class AsyncCompuGlobalAPI:
147
171
  A random screencap object.
148
172
 
149
173
  """
150
- request = self.discovery.RANDOM.build_request(self.client.base_url)
174
+ optional_params = {"smin": season_minimum, "smax": season_maximum}
175
+ query = {k: v for k, v in optional_params.items() if v is not None}
176
+ request = self.discovery.RANDOM.build_request(self.client.base_url, query=query)
151
177
  random = await self.client.handle_request(request)
152
178
  return Screencap.model_validate(random)
153
179
 
@@ -26,7 +26,7 @@ class DiscoveryAPI:
26
26
  CAPTION = Endpoint(
27
27
  path="/api/caption",
28
28
  method=RequestMethod.GET,
29
- query_params=frozenset({"e", "t", "nearby"}),
29
+ required_query_params=frozenset({"e", "t", "nearby"}),
30
30
  )
31
31
 
32
32
  DISCOVER = Endpoint(
@@ -37,6 +37,7 @@ class DiscoveryAPI:
37
37
  RANDOM = Endpoint(
38
38
  path="/api/random",
39
39
  method=RequestMethod.GET,
40
+ optional_query_params=frozenset({"smin", "smax"}),
40
41
  )
41
42
 
42
43
  NAVIGATOR = Endpoint(
@@ -47,7 +48,8 @@ class DiscoveryAPI:
47
48
  SEARCH = Endpoint(
48
49
  path="/api/search",
49
50
  method=RequestMethod.GET,
50
- query_params=frozenset({"q"}),
51
+ required_query_params=frozenset({"q"}),
52
+ optional_query_params=frozenset({"smin", "smax"}),
51
53
  )
52
54
 
53
55
  FRAMES = Endpoint(
@@ -52,11 +52,13 @@ class Endpoint:
52
52
  Attributes
53
53
  ----------
54
54
  path: str
55
- The url path
55
+ The url path containing any path parameter names
56
56
  method: RequestMethod, optional
57
57
  The HTTP RequestMethod for the request (GET/POST/PUT)
58
- query_params: dict[str, Any] | None, optional
59
- The query parameters to use in the request
58
+ required_query_params: dict[str, Any] | None, optional
59
+ The required query parameters to use in the request
60
+ optional_query_params: dict[str, Any] | None, optional
61
+ Optional query params that can be used in the request
60
62
  body_model: type[BaseModel], optional
61
63
  The pydantic model for the json body
62
64
 
@@ -64,7 +66,8 @@ class Endpoint:
64
66
 
65
67
  path: str
66
68
  method: RequestMethod = RequestMethod.GET
67
- query_params: frozenset[str] = frozenset()
69
+ required_query_params: frozenset[str] = frozenset()
70
+ optional_query_params: frozenset[str] = frozenset()
68
71
  body_model: type[BaseModel] | None = None
69
72
 
70
73
  def build_url(
@@ -178,8 +181,8 @@ class Endpoint:
178
181
  Raises error if contains missing or unexpected params from the definition of the endpoint
179
182
 
180
183
  """
181
- missing = set(self.query_params - query.keys())
182
- unexpected = query.keys() - self.query_params
184
+ missing = set(self.required_query_params - query.keys())
185
+ unexpected = query.keys() - (self.required_query_params | self.optional_query_params)
183
186
 
184
187
  if missing:
185
188
  raise ValueError(
@@ -33,13 +33,13 @@ class MediaAPI:
33
33
  COMIC_PANEL = Endpoint(
34
34
  path="/comic/img",
35
35
  method=RequestMethod.GET,
36
- query_params=frozenset({"b64"}),
36
+ required_query_params=frozenset({"b64"}),
37
37
  )
38
38
 
39
39
  COMIC_STRIP = Endpoint(
40
40
  path="/comic/img",
41
41
  method=RequestMethod.GET,
42
- query_params=frozenset({"b64", "layout"}),
42
+ required_query_params=frozenset({"b64", "layout"}),
43
43
  )
44
44
 
45
45
  RENDER_GIF = Endpoint(
@@ -56,7 +56,7 @@ class MediaAPI:
56
56
  DETECT_LOOP = Endpoint(
57
57
  path="/api/detect-loop",
58
58
  method=RequestMethod.GET,
59
- query_params=frozenset(
59
+ required_query_params=frozenset(
60
60
  {"episode", "start", "end"},
61
61
  ),
62
62
  )
@@ -23,5 +23,5 @@ class MetadataAPI:
23
23
  TRANSCRIPT = Endpoint(
24
24
  path="/api/transcript",
25
25
  method=RequestMethod.GET,
26
- query_params=frozenset({"e", "t"}),
26
+ required_query_params=frozenset({"e", "t"}),
27
27
  )
@@ -3,6 +3,7 @@
3
3
  from pydantic import Field
4
4
 
5
5
  from compuglobal.models.base import BaseCompuGlobalModel
6
+ from compuglobal.models.timestamp import Timestamp
6
7
 
7
8
 
8
9
  class Frame(BaseCompuGlobalModel):
@@ -32,10 +33,7 @@ class Frame(BaseCompuGlobalModel):
32
33
  A readable timestamp for the frame in format `mm:ss`.
33
34
 
34
35
  """
35
- seconds = int(self.timestamp / 1000)
36
- minutes = int(seconds / 60)
37
- seconds -= minutes * 60
38
- return f"{minutes}:{seconds:02d}"
36
+ return Timestamp.get_real_timestamp(timestamp=self.timestamp)
39
37
 
40
38
  def __str__(self) -> str:
41
39
  """Get the string representation of the Frame.
@@ -6,6 +6,7 @@ from compuglobal.models.base import BaseCompuGlobalModel
6
6
  from compuglobal.models.episode import EpisodeMetadata
7
7
  from compuglobal.models.frame import Frame
8
8
  from compuglobal.models.subtitle import Subtitle
9
+ from compuglobal.models.timestamp import Timestamp
9
10
 
10
11
 
11
12
  class ScreencapMoment(BaseCompuGlobalModel):
@@ -29,6 +30,17 @@ class ScreencapMoment(BaseCompuGlobalModel):
29
30
  content: str = Field(alias="Content")
30
31
  title: str = Field(alias="Title")
31
32
 
33
+ def get_real_timestamp(self) -> str:
34
+ """Get a readable timestamp for the moments timestamp in format `mm:ss`.
35
+
36
+ Returns
37
+ -------
38
+ str
39
+ A readable timestamp in format `mm:ss`.
40
+
41
+ """
42
+ return Timestamp.get_real_timestamp(self.timestamp)
43
+
32
44
 
33
45
  class Screencap(BaseCompuGlobalModel):
34
46
  """A Screencap of an episode at a point in time of a TV Show.
@@ -58,7 +70,7 @@ class Screencap(BaseCompuGlobalModel):
58
70
  max_timestamp: int = Field(alias="MaxTimestamp", ge=0)
59
71
 
60
72
  def get_real_timestamp(self) -> str:
61
- """Get a readable timestamp for the frame in format "mm:ss".
73
+ """Get a readable timestamp for the frame in format `mm:ss`.
62
74
 
63
75
  Returns
64
76
  -------
@@ -66,10 +78,21 @@ class Screencap(BaseCompuGlobalModel):
66
78
  A readable timestamp for the frame in format `mm:ss`.
67
79
 
68
80
  """
69
- return self.frame.get_real_timestamp()
81
+ return Timestamp.get_real_timestamp(timestamp=self.frame.timestamp)
82
+
83
+ def get_duration(self) -> int:
84
+ """Get duration of screencap subtitles in milliseconds.
85
+
86
+ Returns
87
+ -------
88
+ int
89
+ Duration in milliseconds
90
+
91
+ """
92
+ return Timestamp.get_subtitles_duration(self.subtitles)
70
93
 
71
94
  def captions(self) -> list[str]:
72
- """Get a list of captions for the screencap from all subttiles.
95
+ """Get a list of captions for the screencap from all subtitles.
73
96
 
74
97
  Returns
75
98
  -------
@@ -3,6 +3,7 @@
3
3
  from pydantic import Field
4
4
 
5
5
  from compuglobal.models.base import BaseCompuGlobalModel
6
+ from compuglobal.models.timestamp import Timestamp
6
7
 
7
8
 
8
9
  class Subtitle(BaseCompuGlobalModel):
@@ -41,7 +42,7 @@ class Subtitle(BaseCompuGlobalModel):
41
42
  Returns
42
43
  -------
43
44
  int
44
- The duration in milliseonds.
45
+ The duration in milliseconds.
45
46
 
46
47
  """
47
- return self.end_timestamp - self.start_timestamp
48
+ return Timestamp.get_duration(start_timestamp=self.start_timestamp, end_timestamp=self.end_timestamp)
@@ -0,0 +1,76 @@
1
+ """Timestamp module with helper class for working with timestamps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from compuglobal.models.subtitle import Subtitle
9
+
10
+
11
+ class Timestamp:
12
+ """Helper class for working with timestamps."""
13
+
14
+ @staticmethod
15
+ def get_minutes_seconds(milliseconds: int) -> tuple[int, int]:
16
+ """Get minutes and seconds from milliseconds.
17
+
18
+ Parameters
19
+ ----------
20
+ milliseconds : int
21
+ The length of time in milliseconds
22
+
23
+ Returns
24
+ -------
25
+ tuple[int, int]
26
+ The minutes, and seconds as a tuple
27
+
28
+ """
29
+ seconds = int(milliseconds / 1000)
30
+ minutes = int(seconds / 60)
31
+ seconds -= minutes * 60
32
+ return minutes, seconds
33
+
34
+ @staticmethod
35
+ def get_real_timestamp(timestamp: int) -> str:
36
+ """Get a readable timestamp for the frame in format `mm:ss`.
37
+
38
+ Returns
39
+ -------
40
+ str
41
+ A readable timestamp for the frame in format `mm:ss`.
42
+
43
+ """
44
+ minutes, seconds = Timestamp.get_minutes_seconds(timestamp)
45
+ return f"{minutes}:{seconds:02d}"
46
+
47
+ @staticmethod
48
+ def get_duration(start_timestamp: int, end_timestamp: int) -> int:
49
+ """Get the duration of the subtitle in milliseconds.
50
+
51
+ Returns
52
+ -------
53
+ int
54
+ The duration in milliseconds.
55
+
56
+ """
57
+ return end_timestamp - start_timestamp
58
+
59
+ @staticmethod
60
+ def get_subtitles_duration(subtitles: list[Subtitle]) -> int:
61
+ """Get the duration between the start of the earliest subtitle, and the end of the latest subtitle.
62
+
63
+ Parameters
64
+ ----------
65
+ subtitles : list[Subtitle]
66
+ The subtitles
67
+
68
+ Returns
69
+ -------
70
+ int
71
+ The duration in milliseconds
72
+
73
+ """
74
+ start_timestamp = min(subtitle.start_timestamp for subtitle in subtitles)
75
+ end_timestamp = max(subtitle.end_timestamp for subtitle in subtitles)
76
+ return Timestamp.get_duration(start_timestamp, end_timestamp)
@@ -12,7 +12,7 @@ license = "MIT"
12
12
  requires-python = ">=3.13"
13
13
  dynamic = ["version", "description"]
14
14
  dependencies = [
15
- "aiohttp>=3.13.0", "pydantic>=2.13.0"
15
+ "aiohttp>=3.14.0", "pydantic>=2.13.0"
16
16
  ]
17
17
  classifiers = [
18
18
  "Programming Language :: Python :: 3.13",
File without changes