stophy 0.1.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.
@@ -0,0 +1,18 @@
1
+ # JavaScript / TypeScript
2
+ node_modules
3
+ dist
4
+ *.tsbuildinfo
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ .venv/
10
+ build/
11
+ *.egg-info/
12
+ .pytest_cache/
13
+
14
+ # Lockfiles for the publishable packages are committed; root has none.
15
+
16
+ # Misc
17
+ *.log
18
+ .DS_Store
stophy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: stophy
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Stophy - YouTube context API for AI agents.
5
+ Project-URL: Homepage, https://stophy.dev
6
+ Project-URL: Repository, https://github.com/stophy/stophy-sdk
7
+ Author: Stophy
8
+ License: MIT
9
+ Keywords: ai-agents,sdk,stophy,transcript,youtube
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: httpx>=0.27
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # stophy
17
+
18
+ Official Python SDK for [Stophy](https://stophy.dev) **YouTube context API for AI agents**. Search videos, fetch transcripts, read comments and live chat, inspect channels and playlists, and get autocomplete suggestions, all returned as structured JSON.
19
+
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ uv install stophy
25
+ ```
26
+
27
+ Get an API key from your [Stophy dashboard](https://stophy.dev). The SDK sends it as `Authorization: Bearer <key>` on every request.
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ import os
33
+ from stophy import Stophy
34
+
35
+ stophy = Stophy(os.environ["STOPHY_API_KEY"])
36
+
37
+ result = stophy.video(type="transcript", video_url="https://www.youtube.com/watch?v=D7liwdjvhWc")
38
+ print(result["data"]["text"])
39
+ ```
40
+
41
+ ## Methods
42
+
43
+ | Method | Description |
44
+ | --- | --- |
45
+ | `stophy.video(...)` | Details, transcript, comments, replies, or live chat (set `type`) |
46
+ | `stophy.search(...)` | Search with filters for type, sort, date, duration, features |
47
+ | `stophy.channel(...)` | Channel metadata + content by `tab` |
48
+ | `stophy.playlist(...)` | Playlist items, paginated |
49
+ | `stophy.suggest(...)` | Search autocomplete suggestions |
50
+ | `stophy.credits()` | Current credit balance |
51
+ | `stophy.logs(...)` | Recent request logs |
52
+ | `stophy.usage(...)` | Daily credit/request counts |
53
+
54
+ Arguments are keyword-only and snake_case; the SDK maps them to the API's field names for you. `video()` is overloaded on `type`, so the returned `data` is typed for the variant you asked for (transcript, comments, details, …).
55
+
56
+ ```python
57
+ # Search
58
+ results = stophy.search(q="typescript tutorial", sort_by="popularity", duration="long")
59
+
60
+ # Comments, then replies to a comment
61
+ comments = stophy.video(type="comments", video_url=url, sort_by="top")
62
+ token = comments["data"]["items"][0].get("repliesToken")
63
+ if token:
64
+ replies = stophy.video(type="replies", continuation_token=token)
65
+
66
+ # Autocomplete and account
67
+ print(stophy.suggest(q="react")["data"]["suggestions"])
68
+ print(stophy.credits()["data"]["credits"])
69
+ ```
70
+
71
+ ### Pagination
72
+
73
+ ```python
74
+ token = None
75
+ while True:
76
+ page = stophy.search(q="lofi", continuation_token=token)
77
+ ... # handle page["data"]["items"]
78
+ token = page["data"].get("continuationToken")
79
+ if not token:
80
+ break
81
+ ```
82
+
83
+ ## Errors
84
+
85
+ Non-2xx responses raise `StophyError`:
86
+
87
+ ```python
88
+ from stophy import Stophy, StophyError
89
+
90
+ try:
91
+ stophy.credits()
92
+ except StophyError as err:
93
+ print(err.status, err.code, err, err.request_id)
94
+ # err.code: "UNAUTHORIZED" | "INSUFFICIENT_CREDITS" | "BAD_REQUEST" |
95
+ # "INVALID_INPUT" | "NOT_FOUND" | "CONCURRENCY_LIMITED" | "INTERNAL_ERROR"
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
stophy-0.1.0/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # stophy
2
+
3
+ Official Python SDK for [Stophy](https://stophy.dev) **YouTube context API for AI agents**. Search videos, fetch transcripts, read comments and live chat, inspect channels and playlists, and get autocomplete suggestions, all returned as structured JSON.
4
+
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ uv install stophy
10
+ ```
11
+
12
+ Get an API key from your [Stophy dashboard](https://stophy.dev). The SDK sends it as `Authorization: Bearer <key>` on every request.
13
+
14
+ ## Quick start
15
+
16
+ ```python
17
+ import os
18
+ from stophy import Stophy
19
+
20
+ stophy = Stophy(os.environ["STOPHY_API_KEY"])
21
+
22
+ result = stophy.video(type="transcript", video_url="https://www.youtube.com/watch?v=D7liwdjvhWc")
23
+ print(result["data"]["text"])
24
+ ```
25
+
26
+ ## Methods
27
+
28
+ | Method | Description |
29
+ | --- | --- |
30
+ | `stophy.video(...)` | Details, transcript, comments, replies, or live chat (set `type`) |
31
+ | `stophy.search(...)` | Search with filters for type, sort, date, duration, features |
32
+ | `stophy.channel(...)` | Channel metadata + content by `tab` |
33
+ | `stophy.playlist(...)` | Playlist items, paginated |
34
+ | `stophy.suggest(...)` | Search autocomplete suggestions |
35
+ | `stophy.credits()` | Current credit balance |
36
+ | `stophy.logs(...)` | Recent request logs |
37
+ | `stophy.usage(...)` | Daily credit/request counts |
38
+
39
+ Arguments are keyword-only and snake_case; the SDK maps them to the API's field names for you. `video()` is overloaded on `type`, so the returned `data` is typed for the variant you asked for (transcript, comments, details, …).
40
+
41
+ ```python
42
+ # Search
43
+ results = stophy.search(q="typescript tutorial", sort_by="popularity", duration="long")
44
+
45
+ # Comments, then replies to a comment
46
+ comments = stophy.video(type="comments", video_url=url, sort_by="top")
47
+ token = comments["data"]["items"][0].get("repliesToken")
48
+ if token:
49
+ replies = stophy.video(type="replies", continuation_token=token)
50
+
51
+ # Autocomplete and account
52
+ print(stophy.suggest(q="react")["data"]["suggestions"])
53
+ print(stophy.credits()["data"]["credits"])
54
+ ```
55
+
56
+ ### Pagination
57
+
58
+ ```python
59
+ token = None
60
+ while True:
61
+ page = stophy.search(q="lofi", continuation_token=token)
62
+ ... # handle page["data"]["items"]
63
+ token = page["data"].get("continuationToken")
64
+ if not token:
65
+ break
66
+ ```
67
+
68
+ ## Errors
69
+
70
+ Non-2xx responses raise `StophyError`:
71
+
72
+ ```python
73
+ from stophy import Stophy, StophyError
74
+
75
+ try:
76
+ stophy.credits()
77
+ except StophyError as err:
78
+ print(err.status, err.code, err, err.request_id)
79
+ # err.code: "UNAUTHORIZED" | "INSUFFICIENT_CREDITS" | "BAD_REQUEST" |
80
+ # "INVALID_INPUT" | "NOT_FOUND" | "CONCURRENCY_LIMITED" | "INTERNAL_ERROR"
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "stophy"
3
+ version = "0.1.0"
4
+ description = "Official Python SDK for Stophy - YouTube context API for AI agents."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Stophy" }]
9
+ keywords = ["stophy", "youtube", "transcript", "sdk", "ai-agents"]
10
+ dependencies = ["httpx>=0.27"]
11
+
12
+ [project.urls]
13
+ Homepage = "https://stophy.dev"
14
+ Repository = "https://github.com/stophy/stophy-sdk"
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=8"]
18
+
19
+ [dependency-groups]
20
+ dev = ["pytest>=8"]
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/stophy"]
@@ -0,0 +1,6 @@
1
+ from . import types
2
+ from .client import Stophy
3
+ from .errors import StophyError
4
+
5
+ __all__ = ["Stophy", "StophyError", "types"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import time
5
+ from email.utils import parsedate_to_datetime
6
+ from typing import Any, Dict, List, Literal, Mapping, Optional, overload
7
+
8
+ import httpx
9
+
10
+ from .errors import StophyError
11
+ from .types import (
12
+ ChannelResponse,
13
+ CommentsResponse,
14
+ CreditsResponse,
15
+ LiveChatResponse,
16
+ LogsResponse,
17
+ PlaylistResponse,
18
+ SearchResponse,
19
+ SuggestResponse,
20
+ TranscriptResponse,
21
+ UsageResponse,
22
+ VideoDetailsResponse,
23
+ VideoResponse,
24
+ )
25
+
26
+ DEFAULT_BASE_URL = "https://api.stophy.dev"
27
+ RETRYABLE_STATUS = {429, 500, 502, 503, 504}
28
+
29
+
30
+ def _compact(params: Mapping[str, Any]) -> Dict[str, Any]:
31
+ """Drop keys whose value is None so we only send what the caller set."""
32
+ return {k: v for k, v in params.items() if v is not None}
33
+
34
+
35
+ class Stophy:
36
+ """Client for Stophy — YouTube context API for AI agents.
37
+
38
+ >>> stophy = Stophy(os.environ["STOPHY_API_KEY"])
39
+ >>> result = stophy.video(type="transcript", video_url=url)
40
+ >>> print(result["data"]["text"])
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: str,
46
+ *,
47
+ base_url: str = DEFAULT_BASE_URL,
48
+ headers: Optional[Mapping[str, str]] = None,
49
+ timeout: float = 30.0,
50
+ max_retries: int = 2,
51
+ retry_initial_delay: float = 0.5,
52
+ transport: Optional[httpx.BaseTransport] = None,
53
+ ) -> None:
54
+ if not api_key:
55
+ raise ValueError("Stophy: api_key is required.")
56
+ self._max_retries = max_retries
57
+ self._retry_base = retry_initial_delay
58
+ self.client = httpx.Client(
59
+ base_url=base_url,
60
+ headers={"Authorization": f"Bearer {api_key}", **(headers or {})},
61
+ timeout=timeout,
62
+ transport=transport,
63
+ )
64
+
65
+ # --- endpoints ---------------------------------------------------------
66
+
67
+ @overload
68
+ def video(
69
+ self,
70
+ *,
71
+ type: Literal["details"],
72
+ video_url: Optional[str] = None,
73
+ sort_by: Optional[str] = None,
74
+ chat_type: Optional[str] = None,
75
+ continuation_token: Optional[str] = None,
76
+ ) -> VideoDetailsResponse: ...
77
+
78
+ @overload
79
+ def video(
80
+ self,
81
+ *,
82
+ type: Literal["transcript"],
83
+ video_url: Optional[str] = None,
84
+ sort_by: Optional[str] = None,
85
+ chat_type: Optional[str] = None,
86
+ continuation_token: Optional[str] = None,
87
+ ) -> TranscriptResponse: ...
88
+
89
+ @overload
90
+ def video(
91
+ self,
92
+ *,
93
+ type: Literal["comments", "replies"],
94
+ video_url: Optional[str] = None,
95
+ sort_by: Optional[str] = None,
96
+ chat_type: Optional[str] = None,
97
+ continuation_token: Optional[str] = None,
98
+ ) -> CommentsResponse: ...
99
+
100
+ @overload
101
+ def video(
102
+ self,
103
+ *,
104
+ type: Literal["livechat"],
105
+ video_url: Optional[str] = None,
106
+ sort_by: Optional[str] = None,
107
+ chat_type: Optional[str] = None,
108
+ continuation_token: Optional[str] = None,
109
+ ) -> LiveChatResponse: ...
110
+
111
+ def video(
112
+ self,
113
+ *,
114
+ type: str,
115
+ video_url: Optional[str] = None,
116
+ sort_by: Optional[str] = None,
117
+ chat_type: Optional[str] = None,
118
+ continuation_token: Optional[str] = None,
119
+ ) -> VideoResponse:
120
+ """Video details, transcript, comments, replies, or live chat — pick with ``type``.
121
+
122
+ The shape of ``data`` depends on ``type`` (details / transcript /
123
+ comments / replies / livechat).
124
+ """
125
+ return self._post(
126
+ "/v1/video",
127
+ {
128
+ "type": type,
129
+ "videoUrl": video_url,
130
+ "sortBy": sort_by,
131
+ "chatType": chat_type,
132
+ "continuationToken": continuation_token,
133
+ },
134
+ )
135
+
136
+ def search(
137
+ self,
138
+ *,
139
+ q: str,
140
+ type: Optional[str] = None,
141
+ sort_by: Optional[str] = None,
142
+ upload_date: Optional[str] = None,
143
+ duration: Optional[str] = None,
144
+ features: Optional[List[str]] = None,
145
+ continuation_token: Optional[str] = None,
146
+ ) -> SearchResponse:
147
+ """Search YouTube, optionally filtered by type, sort, date, duration, and features."""
148
+ return self._post(
149
+ "/v1/search",
150
+ {
151
+ "q": q,
152
+ "type": type,
153
+ "sortBy": sort_by,
154
+ "uploadDate": upload_date,
155
+ "duration": duration,
156
+ "features": features,
157
+ "continuationToken": continuation_token,
158
+ },
159
+ )
160
+
161
+ def channel(
162
+ self,
163
+ *,
164
+ channel_url: str,
165
+ tab: Optional[str] = None,
166
+ sort_by: Optional[str] = None,
167
+ continuation_token: Optional[str] = None,
168
+ ) -> ChannelResponse:
169
+ """Channel metadata and content. Switch sections with ``tab``."""
170
+ return self._post(
171
+ "/v1/channel",
172
+ {
173
+ "channelUrl": channel_url,
174
+ "tab": tab,
175
+ "sortBy": sort_by,
176
+ "continuationToken": continuation_token,
177
+ },
178
+ )
179
+
180
+ def playlist(
181
+ self,
182
+ *,
183
+ playlist_url: str,
184
+ continuation_token: Optional[str] = None,
185
+ ) -> PlaylistResponse:
186
+ """Playlist items. Page through long playlists with ``continuation_token``."""
187
+ return self._post(
188
+ "/v1/playlist",
189
+ {"playlistUrl": playlist_url, "continuationToken": continuation_token},
190
+ )
191
+
192
+ def suggest(
193
+ self,
194
+ *,
195
+ q: str,
196
+ hl: Optional[str] = None,
197
+ gl: Optional[str] = None,
198
+ ) -> SuggestResponse:
199
+ """Search autocomplete suggestions."""
200
+ return self._get("/v1/suggest", {"q": q, "hl": hl, "gl": gl})
201
+
202
+ def credits(self) -> CreditsResponse:
203
+ """Your current credit balance."""
204
+ return self._get("/v1/credits")
205
+
206
+ def logs(
207
+ self,
208
+ *,
209
+ days: Optional[str] = None,
210
+ endpoint: Optional[str] = None,
211
+ page: Optional[int] = None,
212
+ ) -> LogsResponse:
213
+ """Recent API request logs."""
214
+ return self._get("/v1/logs", {"days": days, "endpoint": endpoint, "page": page})
215
+
216
+ def usage(
217
+ self,
218
+ *,
219
+ days: Optional[str] = None,
220
+ tz: Optional[str] = None,
221
+ ) -> UsageResponse:
222
+ """Daily credit and request counts."""
223
+ return self._get("/v1/usage", {"days": days, "tz": tz})
224
+
225
+ # --- plumbing ----------------------------------------------------------
226
+
227
+ def _get(self, path: str, params: Optional[Mapping[str, Any]] = None) -> Any:
228
+ return self._handle(self._send("GET", path, params=_compact(params or {})))
229
+
230
+ def _post(self, path: str, body: Mapping[str, Any]) -> Any:
231
+ return self._handle(self._send("POST", path, json=_compact(body)))
232
+
233
+ def _send(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
234
+ """Send a request, retrying transient failures with backoff + jitter.
235
+
236
+ Every Stophy endpoint is a read, so retrying is always safe.
237
+ """
238
+ attempt = 0
239
+ while True:
240
+ try:
241
+ resp = self.client.request(method, path, **kwargs)
242
+ except httpx.TransportError:
243
+ if attempt >= self._max_retries:
244
+ raise
245
+ time.sleep(self._backoff(attempt))
246
+ attempt += 1
247
+ continue
248
+ if attempt < self._max_retries and resp.status_code in RETRYABLE_STATUS:
249
+ time.sleep(self._retry_after(resp, attempt))
250
+ attempt += 1
251
+ continue
252
+ return resp
253
+
254
+ def _backoff(self, attempt: int) -> float:
255
+ window = self._retry_base * (2**attempt)
256
+ return window / 2 + random.random() * (window / 2)
257
+
258
+ def _retry_after(self, resp: httpx.Response, attempt: int) -> float:
259
+ header = resp.headers.get("retry-after")
260
+ if header:
261
+ try:
262
+ return float(header)
263
+ except ValueError:
264
+ try:
265
+ delay = parsedate_to_datetime(header).timestamp() - time.time()
266
+ return max(0.0, delay)
267
+ except (TypeError, ValueError):
268
+ pass
269
+ return self._backoff(attempt)
270
+
271
+ def _handle(self, resp: httpx.Response) -> Dict[str, Any]:
272
+ try:
273
+ payload = resp.json()
274
+ except ValueError:
275
+ payload = None
276
+
277
+ if resp.is_success and isinstance(payload, dict):
278
+ return payload
279
+
280
+ err = payload if isinstance(payload, dict) else {}
281
+ raise StophyError(
282
+ err.get("error") or f"Stophy request failed with status {resp.status_code}",
283
+ status=resp.status_code,
284
+ code=err.get("code"),
285
+ request_id=resp.headers.get("x-request-id"),
286
+ details=err.get("details"),
287
+ )
288
+
289
+ # --- lifecycle ---------------------------------------------------------
290
+
291
+ def close(self) -> None:
292
+ self.client.close()
293
+
294
+ def __enter__(self) -> "Stophy":
295
+ return self
296
+
297
+ def __exit__(self, *exc: Any) -> None:
298
+ self.close()
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class StophyError(Exception):
7
+ """Raised when the Stophy API responds with a non-2xx status."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ *,
13
+ status: int,
14
+ code: Optional[str] = None,
15
+ request_id: Optional[str] = None,
16
+ details: Any = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.status = status
20
+ self.code = code
21
+ self.request_id = request_id
22
+ self.details = details
@@ -0,0 +1,423 @@
1
+ """Response types mirroring the Stophy OpenAPI schemas.
2
+
3
+ These are ``TypedDict``s with ``total=False`` — they describe the shape of the
4
+ JSON the API returns so editors and type checkers can help, without forcing
5
+ every optional/nullable field to be present.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import List, Literal, Optional, TypedDict, Union
11
+
12
+ CacheState = Literal["hit", "miss"]
13
+ ErrorCode = Literal[
14
+ "UNAUTHORIZED",
15
+ "INSUFFICIENT_CREDITS",
16
+ "BAD_REQUEST",
17
+ "INVALID_INPUT",
18
+ "NOT_FOUND",
19
+ "CONCURRENCY_LIMITED",
20
+ "INTERNAL_ERROR",
21
+ ]
22
+
23
+
24
+ class Thumbnail(TypedDict, total=False):
25
+ url: str
26
+ width: int
27
+ height: int
28
+
29
+
30
+ class EmptyState(TypedDict, total=False):
31
+ code: str
32
+ message: str
33
+
34
+
35
+ # --- transcript -----------------------------------------------------------
36
+ class TranscriptLanguage(TypedDict, total=False):
37
+ code: Optional[str]
38
+ name: Optional[str]
39
+ isAutoGenerated: bool
40
+
41
+
42
+ class TranscriptSegment(TypedDict, total=False):
43
+ start: float
44
+ duration: float
45
+ text: str
46
+
47
+
48
+ class TranscriptResult(TypedDict, total=False):
49
+ language: TranscriptLanguage
50
+ segments: List[TranscriptSegment]
51
+ text: str
52
+ empty: EmptyState
53
+
54
+
55
+ # --- video details --------------------------------------------------------
56
+ class VideoDetails(TypedDict, total=False):
57
+ id: str
58
+ type: Literal["video"]
59
+ videoUrl: str
60
+ title: Optional[str]
61
+ author: Optional[str]
62
+ authorId: Optional[str]
63
+ category: Optional[str]
64
+ description: Optional[str]
65
+ durationSec: Optional[float]
66
+ durationText: Optional[str]
67
+ isLive: bool
68
+ likeCount: Optional[float]
69
+ likeCountText: Optional[str]
70
+ publishedAt: Optional[str]
71
+ tags: List[str]
72
+ viewCount: Optional[float]
73
+ viewCountText: Optional[str]
74
+ thumbnails: List[Thumbnail]
75
+
76
+
77
+ class RelatedVideo(TypedDict, total=False):
78
+ id: str
79
+ type: Literal["video"]
80
+ videoUrl: str
81
+ title: Optional[str]
82
+ author: Optional[str]
83
+ authorId: Optional[str]
84
+ durationSec: Optional[float]
85
+ durationText: Optional[str]
86
+ publishedAt: Optional[str]
87
+ publishedAtText: Optional[str]
88
+ viewCount: Optional[float]
89
+ viewCountText: Optional[str]
90
+ thumbnails: List[Thumbnail]
91
+
92
+
93
+ class VideoDetailsData(TypedDict, total=False):
94
+ video: VideoDetails
95
+ related: List[RelatedVideo]
96
+
97
+
98
+ # --- comments / replies ---------------------------------------------------
99
+ class Comment(TypedDict, total=False):
100
+ id: Optional[str]
101
+ text: Optional[str]
102
+ author: Optional[str]
103
+ authorId: Optional[str]
104
+ authorThumbnail: Optional[str]
105
+ hasChannelOwnerReplied: bool
106
+ isChannelOwner: bool
107
+ isHearted: bool
108
+ isPinned: bool
109
+ isVerified: bool
110
+ publishedAt: Optional[str]
111
+ publishedAtText: Optional[str]
112
+ likeCount: Optional[float]
113
+ likeCountText: Optional[str]
114
+ replyCount: Optional[float]
115
+ replyCountText: Optional[str]
116
+ repliesToken: Optional[str]
117
+
118
+
119
+ class CommentsData(TypedDict, total=False):
120
+ items: List[Comment]
121
+ continuationToken: Optional[str]
122
+ empty: EmptyState
123
+
124
+
125
+ # --- live chat ------------------------------------------------------------
126
+ class LiveChatMessage(TypedDict, total=False):
127
+ id: str
128
+ text: str
129
+ author: Optional[str]
130
+ authorId: Optional[str]
131
+ timestampUsec: Optional[str]
132
+ isOwner: bool
133
+ isModerator: bool
134
+ isVerified: bool
135
+ superChatAmount: Optional[str]
136
+ superChatCurrency: Optional[str]
137
+
138
+
139
+ class LiveChatData(TypedDict, total=False):
140
+ status: str
141
+ isLive: bool
142
+ concurrentViewers: Optional[float]
143
+ pollIntervalMs: Optional[float]
144
+ messages: List[LiveChatMessage]
145
+ continuationToken: Optional[str]
146
+
147
+
148
+ VideoData = Union[VideoDetailsData, TranscriptResult, CommentsData, LiveChatData]
149
+
150
+
151
+ # --- search ---------------------------------------------------------------
152
+ class SearchVideo(TypedDict, total=False):
153
+ type: Literal["video"]
154
+ id: str
155
+ videoUrl: str
156
+ title: str
157
+ author: Optional[str]
158
+ authorId: Optional[str]
159
+ description: Optional[str]
160
+ duration: Optional[str]
161
+ durationSec: Optional[float]
162
+ durationText: Optional[str]
163
+ isLive: bool
164
+ isUpcoming: bool
165
+ isVerified: bool
166
+ viewCount: Optional[float]
167
+ viewCountText: Optional[str]
168
+ publishedAt: Optional[str]
169
+ publishedAtText: Optional[str]
170
+ thumbnails: List[Thumbnail]
171
+
172
+
173
+ class SearchShort(TypedDict, total=False):
174
+ type: Literal["short"]
175
+ id: str
176
+ shortUrl: str
177
+ title: str
178
+ author: Optional[str]
179
+ authorId: Optional[str]
180
+ description: Optional[str]
181
+ duration: Optional[str]
182
+ durationSec: Optional[float]
183
+ durationText: Optional[str]
184
+ viewCount: Optional[float]
185
+ viewCountText: Optional[str]
186
+ publishedAt: Optional[str]
187
+ publishedAtText: Optional[str]
188
+ thumbnails: List[Thumbnail]
189
+
190
+
191
+ class SearchPlaylist(TypedDict, total=False):
192
+ type: Literal["playlist"]
193
+ id: str
194
+ playlistUrl: str
195
+ title: str
196
+ author: Optional[str]
197
+ authorId: Optional[str]
198
+ videoCount: Optional[float]
199
+ videoCountText: Optional[str]
200
+ thumbnails: List[Thumbnail]
201
+
202
+
203
+ class SearchChannel(TypedDict, total=False):
204
+ type: Literal["channel"]
205
+ id: str
206
+ channelUrl: str
207
+ name: str
208
+ handle: Optional[str]
209
+ description: Optional[str]
210
+ subscriberCount: Optional[float]
211
+ subscriberCountText: Optional[str]
212
+ isVerified: bool
213
+ thumbnails: List[Thumbnail]
214
+
215
+
216
+ SearchItem = Union[SearchVideo, SearchShort, SearchPlaylist, SearchChannel]
217
+
218
+
219
+ class SearchData(TypedDict, total=False):
220
+ items: List[SearchItem]
221
+ continuationToken: Optional[str]
222
+ estimatedResults: int
223
+ empty: EmptyState
224
+
225
+
226
+ # --- channel --------------------------------------------------------------
227
+ class ChannelLink(TypedDict, total=False):
228
+ title: str
229
+ url: str
230
+
231
+
232
+ class ChannelProfile(TypedDict, total=False):
233
+ id: Optional[str]
234
+ name: Optional[str]
235
+ handle: Optional[str]
236
+ channelUrl: Optional[str]
237
+ description: Optional[str]
238
+ subscriberCount: Optional[float]
239
+ subscriberCountText: Optional[str]
240
+ videoCount: Optional[float]
241
+ videoCountText: Optional[str]
242
+ viewCount: Optional[float]
243
+ viewCountText: Optional[str]
244
+ isVerified: bool
245
+ country: Optional[str]
246
+ joinedDate: Optional[str]
247
+ thumbnails: List[Thumbnail]
248
+ banners: List[Thumbnail]
249
+ links: Optional[List[ChannelLink]]
250
+
251
+
252
+ class ContentItem(TypedDict, total=False):
253
+ id: str
254
+ type: str
255
+ title: Optional[str]
256
+ videoUrl: str
257
+ shortUrl: str
258
+ playlistUrl: str
259
+ author: Optional[str]
260
+ authorId: Optional[str]
261
+ durationSec: Optional[float]
262
+ durationText: Optional[str]
263
+ viewCount: Optional[float]
264
+ viewCountText: Optional[str]
265
+ publishedAt: Optional[str]
266
+ publishedAtText: Optional[str]
267
+ thumbnails: List[Thumbnail]
268
+
269
+
270
+ class ChannelData(TypedDict, total=False):
271
+ channel: Optional[ChannelProfile]
272
+ tab: Optional[str]
273
+ items: List[ContentItem]
274
+ continuationToken: Optional[str]
275
+ empty: EmptyState
276
+
277
+
278
+ # --- playlist -------------------------------------------------------------
279
+ class PlaylistMeta(TypedDict, total=False):
280
+ id: str
281
+ type: Literal["playlist"]
282
+ playlistUrl: str
283
+ title: Optional[str]
284
+ author: Optional[str]
285
+ authorId: Optional[str]
286
+ description: Optional[str]
287
+ videoCount: Optional[str]
288
+ thumbnails: List[Thumbnail]
289
+
290
+
291
+ class PlaylistItem(TypedDict, total=False):
292
+ id: str
293
+ type: Literal["video"]
294
+ videoUrl: str
295
+ title: Optional[str]
296
+ author: Optional[str]
297
+ authorId: Optional[str]
298
+ durationSec: Optional[float]
299
+ durationText: Optional[str]
300
+ index: Optional[float]
301
+ isLive: bool
302
+ isPlayable: bool
303
+ isUpcoming: bool
304
+ upcomingAt: Optional[str]
305
+ viewCount: Optional[float]
306
+ viewCountText: Optional[str]
307
+ publishedAt: Optional[str]
308
+ publishedAtText: Optional[str]
309
+ thumbnails: List[Thumbnail]
310
+
311
+
312
+ class PlaylistData(TypedDict, total=False):
313
+ playlist: Optional[PlaylistMeta]
314
+ items: List[PlaylistItem]
315
+ continuationToken: Optional[str]
316
+ empty: EmptyState
317
+
318
+
319
+ # --- suggest / account ----------------------------------------------------
320
+ class SuggestData(TypedDict, total=False):
321
+ q: str
322
+ hl: str
323
+ gl: str
324
+ suggestions: List[str]
325
+
326
+
327
+ class CreditsData(TypedDict, total=False):
328
+ credits: int
329
+
330
+
331
+ class LogEntry(TypedDict, total=False):
332
+ id: str
333
+ userId: str
334
+ apiKeyId: Optional[str]
335
+ apiKeyName: Optional[str]
336
+ endpoint: str
337
+ method: str
338
+ status: int
339
+ credits: int
340
+ durationMs: Optional[float]
341
+ response: Optional[str]
342
+ createdAt: str
343
+
344
+
345
+ class LogsData(TypedDict, total=False):
346
+ logs: List[LogEntry]
347
+ total: int
348
+ page: int
349
+ pageSize: int
350
+ totalPages: int
351
+ endpoints: List[str]
352
+
353
+
354
+ class UsageItem(TypedDict, total=False):
355
+ date: str
356
+ credits: int
357
+ requests: int
358
+
359
+
360
+ class UsageData(TypedDict, total=False):
361
+ items: List[UsageItem]
362
+
363
+
364
+ # --- response envelopes ---------------------------------------------------
365
+ class _Envelope(TypedDict, total=False):
366
+ success: bool
367
+ requestId: str
368
+ cacheState: CacheState
369
+ creditsUsed: int
370
+ creditsRemaining: int
371
+
372
+
373
+ class _AccountEnvelope(TypedDict, total=False):
374
+ success: bool
375
+ requestId: str
376
+
377
+
378
+ class VideoResponse(_Envelope, total=False):
379
+ data: VideoData
380
+
381
+
382
+ class VideoDetailsResponse(_Envelope, total=False):
383
+ data: VideoDetailsData
384
+
385
+
386
+ class TranscriptResponse(_Envelope, total=False):
387
+ data: TranscriptResult
388
+
389
+
390
+ class CommentsResponse(_Envelope, total=False):
391
+ data: CommentsData
392
+
393
+
394
+ class LiveChatResponse(_Envelope, total=False):
395
+ data: LiveChatData
396
+
397
+
398
+ class SearchResponse(_Envelope, total=False):
399
+ data: SearchData
400
+
401
+
402
+ class ChannelResponse(_Envelope, total=False):
403
+ data: ChannelData
404
+
405
+
406
+ class PlaylistResponse(_Envelope, total=False):
407
+ data: PlaylistData
408
+
409
+
410
+ class SuggestResponse(_Envelope, total=False):
411
+ data: SuggestData
412
+
413
+
414
+ class CreditsResponse(_AccountEnvelope, total=False):
415
+ data: CreditsData
416
+
417
+
418
+ class LogsResponse(_AccountEnvelope, total=False):
419
+ data: LogsData
420
+
421
+
422
+ class UsageResponse(_AccountEnvelope, total=False):
423
+ data: UsageData
stophy-0.1.0/uv.lock ADDED
@@ -0,0 +1,304 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.9"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.10'",
6
+ "python_full_version < '3.10'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "anyio"
11
+ version = "4.12.1"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ resolution-markers = [
14
+ "python_full_version < '3.10'",
15
+ ]
16
+ dependencies = [
17
+ { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
18
+ { name = "idna", marker = "python_full_version < '3.10'" },
19
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "anyio"
28
+ version = "4.14.1"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ resolution-markers = [
31
+ "python_full_version >= '3.10'",
32
+ ]
33
+ dependencies = [
34
+ { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
35
+ { name = "idna", marker = "python_full_version >= '3.10'" },
36
+ { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" },
37
+ ]
38
+ sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
39
+ wheels = [
40
+ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "certifi"
45
+ version = "2026.6.17"
46
+ source = { registry = "https://pypi.org/simple" }
47
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
48
+ wheels = [
49
+ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
50
+ ]
51
+
52
+ [[package]]
53
+ name = "colorama"
54
+ version = "0.4.6"
55
+ source = { registry = "https://pypi.org/simple" }
56
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
57
+ wheels = [
58
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
59
+ ]
60
+
61
+ [[package]]
62
+ name = "exceptiongroup"
63
+ version = "1.3.1"
64
+ source = { registry = "https://pypi.org/simple" }
65
+ dependencies = [
66
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
67
+ ]
68
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
69
+ wheels = [
70
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
71
+ ]
72
+
73
+ [[package]]
74
+ name = "h11"
75
+ version = "0.16.0"
76
+ source = { registry = "https://pypi.org/simple" }
77
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
78
+ wheels = [
79
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
80
+ ]
81
+
82
+ [[package]]
83
+ name = "httpcore"
84
+ version = "1.0.9"
85
+ source = { registry = "https://pypi.org/simple" }
86
+ dependencies = [
87
+ { name = "certifi" },
88
+ { name = "h11" },
89
+ ]
90
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "httpx"
97
+ version = "0.28.1"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ dependencies = [
100
+ { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
101
+ { name = "anyio", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
102
+ { name = "certifi" },
103
+ { name = "httpcore" },
104
+ { name = "idna" },
105
+ ]
106
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
107
+ wheels = [
108
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
109
+ ]
110
+
111
+ [[package]]
112
+ name = "idna"
113
+ version = "3.18"
114
+ source = { registry = "https://pypi.org/simple" }
115
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
116
+ wheels = [
117
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
118
+ ]
119
+
120
+ [[package]]
121
+ name = "iniconfig"
122
+ version = "2.1.0"
123
+ source = { registry = "https://pypi.org/simple" }
124
+ resolution-markers = [
125
+ "python_full_version < '3.10'",
126
+ ]
127
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
128
+ wheels = [
129
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
130
+ ]
131
+
132
+ [[package]]
133
+ name = "iniconfig"
134
+ version = "2.3.0"
135
+ source = { registry = "https://pypi.org/simple" }
136
+ resolution-markers = [
137
+ "python_full_version >= '3.10'",
138
+ ]
139
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
140
+ wheels = [
141
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
142
+ ]
143
+
144
+ [[package]]
145
+ name = "packaging"
146
+ version = "26.2"
147
+ source = { registry = "https://pypi.org/simple" }
148
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
149
+ wheels = [
150
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
151
+ ]
152
+
153
+ [[package]]
154
+ name = "pluggy"
155
+ version = "1.6.0"
156
+ source = { registry = "https://pypi.org/simple" }
157
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
158
+ wheels = [
159
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
160
+ ]
161
+
162
+ [[package]]
163
+ name = "pygments"
164
+ version = "2.20.0"
165
+ source = { registry = "https://pypi.org/simple" }
166
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
167
+ wheels = [
168
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
169
+ ]
170
+
171
+ [[package]]
172
+ name = "pytest"
173
+ version = "8.4.2"
174
+ source = { registry = "https://pypi.org/simple" }
175
+ resolution-markers = [
176
+ "python_full_version < '3.10'",
177
+ ]
178
+ dependencies = [
179
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
180
+ { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
181
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
182
+ { name = "packaging", marker = "python_full_version < '3.10'" },
183
+ { name = "pluggy", marker = "python_full_version < '3.10'" },
184
+ { name = "pygments", marker = "python_full_version < '3.10'" },
185
+ { name = "tomli", marker = "python_full_version < '3.10'" },
186
+ ]
187
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
188
+ wheels = [
189
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
190
+ ]
191
+
192
+ [[package]]
193
+ name = "pytest"
194
+ version = "9.1.1"
195
+ source = { registry = "https://pypi.org/simple" }
196
+ resolution-markers = [
197
+ "python_full_version >= '3.10'",
198
+ ]
199
+ dependencies = [
200
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
201
+ { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
202
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
203
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
204
+ { name = "pluggy", marker = "python_full_version >= '3.10'" },
205
+ { name = "pygments", marker = "python_full_version >= '3.10'" },
206
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
207
+ ]
208
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
209
+ wheels = [
210
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
211
+ ]
212
+
213
+ [[package]]
214
+ name = "stophy"
215
+ version = "0.1.0"
216
+ source = { editable = "." }
217
+ dependencies = [
218
+ { name = "httpx" },
219
+ ]
220
+
221
+ [package.optional-dependencies]
222
+ dev = [
223
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
224
+ { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
225
+ ]
226
+
227
+ [package.dev-dependencies]
228
+ dev = [
229
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
230
+ { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
231
+ ]
232
+
233
+ [package.metadata]
234
+ requires-dist = [
235
+ { name = "httpx", specifier = ">=0.27" },
236
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
237
+ ]
238
+ provides-extras = ["dev"]
239
+
240
+ [package.metadata.requires-dev]
241
+ dev = [{ name = "pytest", specifier = ">=8" }]
242
+
243
+ [[package]]
244
+ name = "tomli"
245
+ version = "2.4.1"
246
+ source = { registry = "https://pypi.org/simple" }
247
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
248
+ wheels = [
249
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
250
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
251
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
252
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
253
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
254
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
255
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
256
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
257
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
258
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
259
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
260
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
261
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
262
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
263
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
264
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
265
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
266
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
267
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
268
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
269
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
270
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
271
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
272
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
273
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
274
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
275
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
276
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
277
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
278
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
279
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
280
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
281
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
282
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
283
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
284
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
285
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
286
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
287
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
288
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
289
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
290
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
291
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
292
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
293
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
294
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
295
+ ]
296
+
297
+ [[package]]
298
+ name = "typing-extensions"
299
+ version = "4.15.0"
300
+ source = { registry = "https://pypi.org/simple" }
301
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
302
+ wheels = [
303
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
304
+ ]