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.
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -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,2 @@
1
+ *.pyc
2
+ example.py
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: SunsetLog
3
+ Version: 0.0.1
4
+ Summary: Add your description here
5
+ Requires-Python: <3.15,>=3.10
6
+ Requires-Dist: httpx>=0.28.1
7
+ Description-Content-Type: text/markdown
8
+
9
+ # SunsetLog
10
+
@@ -0,0 +1,2 @@
1
+ # SunsetLog
2
+
@@ -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"]
@@ -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
+ ]