SunsetLog 0.0.1__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.
- sunsetlog-0.0.1/.gitattributes +2 -0
- sunsetlog-0.0.1/.github/workflows/publish-pypi.yml +30 -0
- sunsetlog-0.0.1/.gitignore +2 -0
- sunsetlog-0.0.1/.python-version +1 -0
- sunsetlog-0.0.1/PKG-INFO +10 -0
- sunsetlog-0.0.1/README.md +2 -0
- sunsetlog-0.0.1/SunsetLog/__init__.py +12 -0
- sunsetlog-0.0.1/SunsetLog/client.py +121 -0
- sunsetlog-0.0.1/SunsetLog/models.py +28 -0
- sunsetlog-0.0.1/SunsetLog.code-workspace +61 -0
- sunsetlog-0.0.1/pyproject.toml +16 -0
- sunsetlog-0.0.1/uv.lock +104 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish Python Package to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [created]
|
|
5
|
+
jobs:
|
|
6
|
+
deploy:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
|
|
11
|
+
- name: Install system dependencies
|
|
12
|
+
run: |
|
|
13
|
+
sudo apt-get update
|
|
14
|
+
sudo apt-get install -y curl
|
|
15
|
+
- name: Install uv
|
|
16
|
+
run: |
|
|
17
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
18
|
+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
19
|
+
- name: Install package dependencies
|
|
20
|
+
run: |
|
|
21
|
+
uv sync
|
|
22
|
+
uv pip install twine
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: uv build
|
|
25
|
+
- name: Publish package to PyPI
|
|
26
|
+
env:
|
|
27
|
+
TWINE_USERNAME: __token__
|
|
28
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
29
|
+
run: |
|
|
30
|
+
uv run twine upload dist/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
sunsetlog-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from SunsetLog.client import SunsetLogAPIError, SunsetLogClient
|
|
2
|
+
from SunsetLog.models import ChannelMeta, ChannelsResponse, LogHit, SearchResponse
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
__all__ = [
|
|
6
|
+
"SunsetLogClient",
|
|
7
|
+
"SunsetLogAPIError",
|
|
8
|
+
"SearchResponse",
|
|
9
|
+
"ChannelsResponse",
|
|
10
|
+
"LogHit",
|
|
11
|
+
"ChannelMeta",
|
|
12
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from SunsetLog.models import ChannelsResponse, SearchResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SunsetLogAPIError(Exception):
|
|
9
|
+
"""Raised when the API returns an error or unexpected response."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, status_code: int | None = None, body: str | None = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.body = body
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SunsetLogClient:
|
|
18
|
+
"""
|
|
19
|
+
Async client for log-api.vmp.ir.
|
|
20
|
+
Use as async context manager or call close() when done.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
token: str,
|
|
26
|
+
*,
|
|
27
|
+
base_url: str = "https://log-api.vmp.ir",
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
headers: dict[str, str] | None = None,
|
|
30
|
+
):
|
|
31
|
+
self._base_url = base_url.rstrip("/")
|
|
32
|
+
self._token = token
|
|
33
|
+
self._timeout = timeout
|
|
34
|
+
self._headers = {
|
|
35
|
+
"Accept": "*/*",
|
|
36
|
+
"Authorization": f"Bearer {token}",
|
|
37
|
+
"User-Agent": "SunsetLog/1.0 (httpx)",
|
|
38
|
+
**(headers or {}),
|
|
39
|
+
}
|
|
40
|
+
self._client: httpx.AsyncClient | None = None
|
|
41
|
+
|
|
42
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
43
|
+
if self._client is None or self._client.is_closed:
|
|
44
|
+
self._client = httpx.AsyncClient(
|
|
45
|
+
base_url=self._base_url,
|
|
46
|
+
headers=self._headers,
|
|
47
|
+
timeout=self._timeout,
|
|
48
|
+
)
|
|
49
|
+
return self._client
|
|
50
|
+
|
|
51
|
+
async def close(self) -> None:
|
|
52
|
+
"""Close the underlying HTTP client."""
|
|
53
|
+
if self._client and not self._client.is_closed:
|
|
54
|
+
await self._client.aclose()
|
|
55
|
+
self._client = None
|
|
56
|
+
|
|
57
|
+
async def __aenter__(self) -> "SunsetLogClient":
|
|
58
|
+
self._get_client()
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
62
|
+
await self.close()
|
|
63
|
+
|
|
64
|
+
async def get_channels(self, gang: int = 1) -> ChannelsResponse:
|
|
65
|
+
"""
|
|
66
|
+
Fetch latest message id/ts per channel for a gang.
|
|
67
|
+
GET /channels/latest?gang={gang}
|
|
68
|
+
"""
|
|
69
|
+
client = self._get_client()
|
|
70
|
+
r = await client.get("/channels/latest", params={"gang": gang})
|
|
71
|
+
if r.status_code != 200:
|
|
72
|
+
raise SunsetLogAPIError(
|
|
73
|
+
f"channels/latest failed: {r.status_code}",
|
|
74
|
+
status_code=r.status_code,
|
|
75
|
+
body=r.text,
|
|
76
|
+
)
|
|
77
|
+
data = r.json()
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
raise SunsetLogAPIError("channels/latest returned non-object", body=r.text)
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
async def search(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
channels: str | list[str] | None = None,
|
|
86
|
+
gang: int = 1,
|
|
87
|
+
q: str = "",
|
|
88
|
+
from_offset: int = 0,
|
|
89
|
+
mode: str = "exact",
|
|
90
|
+
operator: str = "and",
|
|
91
|
+
) -> SearchResponse:
|
|
92
|
+
"""
|
|
93
|
+
Search logs. GET /search.
|
|
94
|
+
channels: single channel name or comma-separated list.
|
|
95
|
+
"""
|
|
96
|
+
if channels is None:
|
|
97
|
+
channels_list = await self.get_channels(gang=gang)
|
|
98
|
+
channels = ",".join(channels_list.keys()) if channels_list else "gang_glitch_locker1"
|
|
99
|
+
elif isinstance(channels, list):
|
|
100
|
+
channels = ",".join(channels)
|
|
101
|
+
|
|
102
|
+
client = self._get_client()
|
|
103
|
+
params: dict[str, Any] = {
|
|
104
|
+
"q": q,
|
|
105
|
+
"from": from_offset,
|
|
106
|
+
"mode": mode,
|
|
107
|
+
"operator": operator,
|
|
108
|
+
"channels": channels,
|
|
109
|
+
"gang": gang,
|
|
110
|
+
}
|
|
111
|
+
r = await client.get("/search", params=params)
|
|
112
|
+
if r.status_code != 200:
|
|
113
|
+
raise SunsetLogAPIError(
|
|
114
|
+
f"search failed: {r.status_code}",
|
|
115
|
+
status_code=r.status_code,
|
|
116
|
+
body=r.text,
|
|
117
|
+
)
|
|
118
|
+
data = r.json()
|
|
119
|
+
if not isinstance(data, dict) or "hits" not in data:
|
|
120
|
+
raise SunsetLogAPIError("search returned invalid shape", body=r.text)
|
|
121
|
+
return data
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChannelMeta(TypedDict):
|
|
5
|
+
"""Latest message meta per channel."""
|
|
6
|
+
|
|
7
|
+
id: str
|
|
8
|
+
ts: int
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogHit(TypedDict, total=False):
|
|
12
|
+
"""Single log entry from search."""
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
index: str
|
|
16
|
+
content: str
|
|
17
|
+
ts: int
|
|
18
|
+
reactions: list[dict[str, Any]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SearchResponse(TypedDict):
|
|
22
|
+
"""Search API response."""
|
|
23
|
+
|
|
24
|
+
hits: list[LogHit]
|
|
25
|
+
total: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ChannelsResponse = dict[str, ChannelMeta]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"folders": [
|
|
3
|
+
{
|
|
4
|
+
"path": "."
|
|
5
|
+
}
|
|
6
|
+
],
|
|
7
|
+
"settings": {
|
|
8
|
+
"python.analysis.inlayHints.variableTypes": true,
|
|
9
|
+
"python.analysis.autoImportCompletions": true,
|
|
10
|
+
"python.analysis.completeFunctionParens": true,
|
|
11
|
+
"python.analysis.inlayHints.pytestParameters": true,
|
|
12
|
+
"python.analysis.inlayHints.callArgumentNames": "all",
|
|
13
|
+
"python.analysis.inlayHints.functionReturnTypes": true,
|
|
14
|
+
"ruff.enable": true,
|
|
15
|
+
"ruff.format.preview": true,
|
|
16
|
+
"ruff.fixAll": true,
|
|
17
|
+
"ruff.organizeImports": true,
|
|
18
|
+
"ruff.showSyntaxErrors": true,
|
|
19
|
+
"[python]": {
|
|
20
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
21
|
+
"editor.formatOnSave": true,
|
|
22
|
+
"editor.codeActionsOnSave": {
|
|
23
|
+
"source.fixAll.ruff": "explicit",
|
|
24
|
+
"source.organizeImports.ruff": "explicit",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
"ruff.configuration": "pyproject.toml",
|
|
28
|
+
|
|
29
|
+
"editor.codeActionsOnSave": {
|
|
30
|
+
"source.fixAll": "explicit",
|
|
31
|
+
"source.organizeImports": "never"
|
|
32
|
+
},
|
|
33
|
+
"remote.localPortHost": "allInterfaces",
|
|
34
|
+
"files.autoSave": "afterDelay",
|
|
35
|
+
"ruff.lineLength": 120,
|
|
36
|
+
},
|
|
37
|
+
"launch": {
|
|
38
|
+
"version": "0.2.0",
|
|
39
|
+
"configurations": [
|
|
40
|
+
{
|
|
41
|
+
"name": "Python Debugger: Current File",
|
|
42
|
+
"type": "debugpy",
|
|
43
|
+
"request": "launch",
|
|
44
|
+
"program": "main.py",
|
|
45
|
+
"console": "integratedTerminal"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"compounds": []
|
|
49
|
+
},
|
|
50
|
+
"extensions": {
|
|
51
|
+
"recommendations": [
|
|
52
|
+
"donjayamanne.python-extension-pack",
|
|
53
|
+
"charliermarsh.ruff",
|
|
54
|
+
"dbaeumer.vscode-eslint",
|
|
55
|
+
"esbenp.prettier-vscode",
|
|
56
|
+
"tamasfe.even-better-toml",
|
|
57
|
+
"Bar.python-import-helper"
|
|
58
|
+
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "SunsetLog"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10,<3.15"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.28.1",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["SunsetLog"]
|
sunsetlog-0.0.1/uv.lock
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10, <3.15"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "anyio"
|
|
7
|
+
version = "4.12.1"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
11
|
+
{ name = "idna" },
|
|
12
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
13
|
+
]
|
|
14
|
+
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" }
|
|
15
|
+
wheels = [
|
|
16
|
+
{ 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" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[package]]
|
|
20
|
+
name = "certifi"
|
|
21
|
+
version = "2026.1.4"
|
|
22
|
+
source = { registry = "https://pypi.org/simple" }
|
|
23
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
|
24
|
+
wheels = [
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "exceptiongroup"
|
|
30
|
+
version = "1.3.1"
|
|
31
|
+
source = { registry = "https://pypi.org/simple" }
|
|
32
|
+
dependencies = [
|
|
33
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
34
|
+
]
|
|
35
|
+
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" }
|
|
36
|
+
wheels = [
|
|
37
|
+
{ 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" },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[[package]]
|
|
41
|
+
name = "h11"
|
|
42
|
+
version = "0.16.0"
|
|
43
|
+
source = { registry = "https://pypi.org/simple" }
|
|
44
|
+
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" }
|
|
45
|
+
wheels = [
|
|
46
|
+
{ 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" },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[[package]]
|
|
50
|
+
name = "httpcore"
|
|
51
|
+
version = "1.0.9"
|
|
52
|
+
source = { registry = "https://pypi.org/simple" }
|
|
53
|
+
dependencies = [
|
|
54
|
+
{ name = "certifi" },
|
|
55
|
+
{ name = "h11" },
|
|
56
|
+
]
|
|
57
|
+
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" }
|
|
58
|
+
wheels = [
|
|
59
|
+
{ 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" },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[[package]]
|
|
63
|
+
name = "httpx"
|
|
64
|
+
version = "0.28.1"
|
|
65
|
+
source = { registry = "https://pypi.org/simple" }
|
|
66
|
+
dependencies = [
|
|
67
|
+
{ name = "anyio" },
|
|
68
|
+
{ name = "certifi" },
|
|
69
|
+
{ name = "httpcore" },
|
|
70
|
+
{ name = "idna" },
|
|
71
|
+
]
|
|
72
|
+
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" }
|
|
73
|
+
wheels = [
|
|
74
|
+
{ 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" },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[[package]]
|
|
78
|
+
name = "idna"
|
|
79
|
+
version = "3.11"
|
|
80
|
+
source = { registry = "https://pypi.org/simple" }
|
|
81
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
82
|
+
wheels = [
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[[package]]
|
|
87
|
+
name = "sunsetlog"
|
|
88
|
+
version = "0.0.1"
|
|
89
|
+
source = { editable = "." }
|
|
90
|
+
dependencies = [
|
|
91
|
+
{ name = "httpx" },
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[package.metadata]
|
|
95
|
+
requires-dist = [{ name = "httpx", specifier = ">=0.28.1" }]
|
|
96
|
+
|
|
97
|
+
[[package]]
|
|
98
|
+
name = "typing-extensions"
|
|
99
|
+
version = "4.15.0"
|
|
100
|
+
source = { registry = "https://pypi.org/simple" }
|
|
101
|
+
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" }
|
|
102
|
+
wheels = [
|
|
103
|
+
{ 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" },
|
|
104
|
+
]
|