SunsetLog 0.0.1__tar.gz → 0.0.2__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,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: SunsetLog
3
+ Version: 0.0.2
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
+
11
+ Async Python client for [log-api.vmp.ir](https://log-api.vmp.ir): list channels and search logs. The library only returns data; you decide how to store or use it.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ **pip**
18
+
19
+ ```bash
20
+ pip install SunsetLog
21
+ ```
22
+
23
+ **uv**
24
+
25
+ ```bash
26
+ uv add SunsetLog
27
+ ```
28
+
29
+ - Python: **3.10+**
30
+ - Dependency: **httpx**
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ You need an API **token** (Bearer). Use the client as an async context manager and call `get_channels` or `search`.
37
+
38
+ ```python
39
+ import asyncio
40
+ from SunsetLog import SunsetLogClient
41
+
42
+ TOKEN = "your-bearer-token"
43
+
44
+ async def main():
45
+ async with SunsetLogClient(TOKEN) as client:
46
+ # List channels for a gang (returns dict of channel_name -> {id, ts})
47
+ channels = await client.get_channels(gang=1)
48
+ print(list(channels.keys()))
49
+
50
+ # Search logs (all channels if channels=None)
51
+ data = await client.search(gang=1, from_offset=0)
52
+ print(data["total"], len(data["hits"]))
53
+ for hit in data["hits"]:
54
+ print(hit.get("index"), hit.get("content"), hit.get("ts"))
55
+
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ ---
60
+
61
+ ## API overview
62
+
63
+ ### `SunsetLogClient(token, *, base_url=..., timeout=30.0, headers=...)`
64
+
65
+ - **token** (str): Bearer token for the API.
66
+ - **base_url**: Default `"https://log-api.vmp.ir"`.
67
+ - **timeout**, **headers**: Optional.
68
+
69
+ Use as `async with SunsetLogClient(TOKEN) as client:` or call `await client.close()` when done.
70
+
71
+ ### `get_channels(gang=1)`
72
+
73
+ - **Returns:** `dict[str, ChannelMeta]` — channel name → `{"id": str, "ts": int}`.
74
+
75
+ ### `search(*, channels=None, gang=1, q="", from_offset=0, mode="exact", operator="and")`
76
+
77
+ - **channels:** `None` (all channels for the gang), a single channel name (str), or a list of channel names.
78
+ - **gang:** Gang id (default `1`).
79
+ - **q:** Search query string.
80
+ - **from_offset:** Pagination offset (API returns up to 100 hits per request).
81
+ - **Returns:** `{"total": int, "hits": list[LogHit]}`. Each hit has `id`, `index` (channel name), `content`, `ts`, `reactions`.
82
+
83
+ ### `SunsetLogAPIError`
84
+
85
+ Raised on non-200 or invalid response. Has `.status_code` and `.body` when available.
86
+
87
+ ---
88
+
89
+ ## Pagination
90
+
91
+ The search API returns at most **100 hits** per request. To fetch all results, request pages by increasing `from_offset` until a page has fewer than 100 hits:
92
+
93
+ ```python
94
+ async def fetch_all(client, gang=1):
95
+ all_hits = []
96
+ from_offset = 0
97
+ while True:
98
+ data = await client.search(gang=gang, from_offset=from_offset)
99
+ hits = data.get("hits") or []
100
+ all_hits.extend(hits)
101
+ if len(hits) < 100:
102
+ break
103
+ from_offset += len(hits)
104
+ return all_hits
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Saving results
110
+
111
+ The library does **not** write files. You get dicts; you save them (e.g. JSON):
112
+
113
+ ```python
114
+ import json
115
+ from pathlib import Path
116
+
117
+ def save_to_file(data: dict, path: str) -> None:
118
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
119
+ with open(path, "w", encoding="utf-8") as f:
120
+ json.dump(data, f, ensure_ascii=False, indent=2)
121
+
122
+ # usage
123
+ data = await client.search(gang=1)
124
+ save_to_file(data, "my_logs.json")
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Exports
130
+
131
+ From `SunsetLog` you can import:
132
+
133
+ - `SunsetLogClient`
134
+ - `SunsetLogAPIError`
135
+ - `SearchResponse`, `ChannelsResponse`, `LogHit`, `ChannelMeta` (types)
136
+
137
+ ---
@@ -0,0 +1,129 @@
1
+ # SunsetLog
2
+
3
+ Async Python client for [log-api.vmp.ir](https://log-api.vmp.ir): list channels and search logs. The library only returns data; you decide how to store or use it.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ **pip**
10
+
11
+ ```bash
12
+ pip install SunsetLog
13
+ ```
14
+
15
+ **uv**
16
+
17
+ ```bash
18
+ uv add SunsetLog
19
+ ```
20
+
21
+ - Python: **3.10+**
22
+ - Dependency: **httpx**
23
+
24
+ ---
25
+
26
+ ## Quick start
27
+
28
+ You need an API **token** (Bearer). Use the client as an async context manager and call `get_channels` or `search`.
29
+
30
+ ```python
31
+ import asyncio
32
+ from SunsetLog import SunsetLogClient
33
+
34
+ TOKEN = "your-bearer-token"
35
+
36
+ async def main():
37
+ async with SunsetLogClient(TOKEN) as client:
38
+ # List channels for a gang (returns dict of channel_name -> {id, ts})
39
+ channels = await client.get_channels(gang=1)
40
+ print(list(channels.keys()))
41
+
42
+ # Search logs (all channels if channels=None)
43
+ data = await client.search(gang=1, from_offset=0)
44
+ print(data["total"], len(data["hits"]))
45
+ for hit in data["hits"]:
46
+ print(hit.get("index"), hit.get("content"), hit.get("ts"))
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ ---
52
+
53
+ ## API overview
54
+
55
+ ### `SunsetLogClient(token, *, base_url=..., timeout=30.0, headers=...)`
56
+
57
+ - **token** (str): Bearer token for the API.
58
+ - **base_url**: Default `"https://log-api.vmp.ir"`.
59
+ - **timeout**, **headers**: Optional.
60
+
61
+ Use as `async with SunsetLogClient(TOKEN) as client:` or call `await client.close()` when done.
62
+
63
+ ### `get_channels(gang=1)`
64
+
65
+ - **Returns:** `dict[str, ChannelMeta]` — channel name → `{"id": str, "ts": int}`.
66
+
67
+ ### `search(*, channels=None, gang=1, q="", from_offset=0, mode="exact", operator="and")`
68
+
69
+ - **channels:** `None` (all channels for the gang), a single channel name (str), or a list of channel names.
70
+ - **gang:** Gang id (default `1`).
71
+ - **q:** Search query string.
72
+ - **from_offset:** Pagination offset (API returns up to 100 hits per request).
73
+ - **Returns:** `{"total": int, "hits": list[LogHit]}`. Each hit has `id`, `index` (channel name), `content`, `ts`, `reactions`.
74
+
75
+ ### `SunsetLogAPIError`
76
+
77
+ Raised on non-200 or invalid response. Has `.status_code` and `.body` when available.
78
+
79
+ ---
80
+
81
+ ## Pagination
82
+
83
+ The search API returns at most **100 hits** per request. To fetch all results, request pages by increasing `from_offset` until a page has fewer than 100 hits:
84
+
85
+ ```python
86
+ async def fetch_all(client, gang=1):
87
+ all_hits = []
88
+ from_offset = 0
89
+ while True:
90
+ data = await client.search(gang=gang, from_offset=from_offset)
91
+ hits = data.get("hits") or []
92
+ all_hits.extend(hits)
93
+ if len(hits) < 100:
94
+ break
95
+ from_offset += len(hits)
96
+ return all_hits
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Saving results
102
+
103
+ The library does **not** write files. You get dicts; you save them (e.g. JSON):
104
+
105
+ ```python
106
+ import json
107
+ from pathlib import Path
108
+
109
+ def save_to_file(data: dict, path: str) -> None:
110
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
111
+ with open(path, "w", encoding="utf-8") as f:
112
+ json.dump(data, f, ensure_ascii=False, indent=2)
113
+
114
+ # usage
115
+ data = await client.search(gang=1)
116
+ save_to_file(data, "my_logs.json")
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Exports
122
+
123
+ From `SunsetLog` you can import:
124
+
125
+ - `SunsetLogClient`
126
+ - `SunsetLogAPIError`
127
+ - `SearchResponse`, `ChannelsResponse`, `LogHit`, `ChannelMeta` (types)
128
+
129
+ ---
@@ -1,5 +1,12 @@
1
1
  from SunsetLog.client import SunsetLogAPIError, SunsetLogClient
2
- from SunsetLog.models import ChannelMeta, ChannelsResponse, LogHit, SearchResponse
2
+ from SunsetLog.models import (
3
+ ChannelInfo,
4
+ ChannelListResponse,
5
+ ChannelMeta,
6
+ ChannelsResponse,
7
+ LogHit,
8
+ SearchResponse,
9
+ )
3
10
 
4
11
  __version__ = "0.1.0"
5
12
  __all__ = [
@@ -7,6 +14,8 @@ __all__ = [
7
14
  "SunsetLogAPIError",
8
15
  "SearchResponse",
9
16
  "ChannelsResponse",
17
+ "ChannelListResponse",
18
+ "ChannelInfo",
10
19
  "LogHit",
11
20
  "ChannelMeta",
12
21
  ]
@@ -2,13 +2,15 @@ from typing import Any
2
2
 
3
3
  import httpx
4
4
 
5
- from SunsetLog.models import ChannelsResponse, SearchResponse
5
+ from SunsetLog.models import ChannelListResponse, ChannelsResponse, SearchResponse
6
6
 
7
7
 
8
8
  class SunsetLogAPIError(Exception):
9
9
  """Raised when the API returns an error or unexpected response."""
10
10
 
11
- def __init__(self, message: str, status_code: int | None = None, body: str | None = None):
11
+ def __init__(
12
+ self, message: str, status_code: int | None = None, body: str | None = None
13
+ ):
12
14
  super().__init__(message)
13
15
  self.status_code = status_code
14
16
  self.body = body
@@ -79,6 +81,28 @@ class SunsetLogClient:
79
81
  raise SunsetLogAPIError("channels/latest returned non-object", body=r.text)
80
82
  return data
81
83
 
84
+ async def get_channel_list(
85
+ self, gang: int = 1, job: int = 1
86
+ ) -> ChannelListResponse:
87
+ """
88
+ Fetch the available channels for a gang/job.
89
+ GET /user/getChannels?gang={gang}&job={job}
90
+ """
91
+ client = self._get_client()
92
+ r = await client.get("/user/getChannels", params={"gang": gang, "job": job})
93
+ if r.status_code != 200:
94
+ raise SunsetLogAPIError(
95
+ f"user/getChannels failed: {r.status_code}",
96
+ status_code=r.status_code,
97
+ body=r.text,
98
+ )
99
+ data = r.json()
100
+ if not isinstance(data, dict) or "channels" not in data:
101
+ raise SunsetLogAPIError(
102
+ "user/getChannels returned invalid shape", body=r.text
103
+ )
104
+ return data
105
+
82
106
  async def search(
83
107
  self,
84
108
  *,
@@ -94,8 +118,9 @@ class SunsetLogClient:
94
118
  channels: single channel name or comma-separated list.
95
119
  """
96
120
  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"
121
+ channel_list = await self.get_channel_list(gang=gang)
122
+ indexes = [c["index"] for c in channel_list.get("channels", [])]
123
+ channels = ",".join(indexes) if indexes else "gang_glitch_locker1"
99
124
  elif isinstance(channels, list):
100
125
  channels = ",".join(channels)
101
126
 
@@ -26,3 +26,22 @@ class SearchResponse(TypedDict):
26
26
 
27
27
 
28
28
  ChannelsResponse = dict[str, ChannelMeta]
29
+
30
+
31
+ class ChannelInfo(TypedDict):
32
+ """Single channel entry from /user/getChannels."""
33
+
34
+ id: int
35
+ index: str
36
+ label: str
37
+ type: int
38
+
39
+
40
+ class ChannelListResponse(TypedDict):
41
+ """Response of /user/getChannels."""
42
+
43
+ channels: list[ChannelInfo]
44
+ categories: list[Any]
45
+ admin: bool
46
+ canViewAllGangs: bool
47
+ canViewAllJobs: bool
@@ -0,0 +1,57 @@
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.importStrategy": "useBundled",
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
+ "editor.codeActionsOnSave": {
28
+ "source.fixAll": "explicit",
29
+ "source.organizeImports": "never",
30
+ },
31
+ "remote.localPortHost": "allInterfaces",
32
+ "files.autoSave": "afterDelay",
33
+ },
34
+ "launch": {
35
+ "version": "0.2.0",
36
+ "configurations": [
37
+ {
38
+ "name": "Python Debugger: Current File",
39
+ "type": "debugpy",
40
+ "request": "launch",
41
+ "program": "main.py",
42
+ "console": "integratedTerminal",
43
+ },
44
+ ],
45
+ "compounds": [],
46
+ },
47
+ "extensions": {
48
+ "recommendations": [
49
+ "donjayamanne.python-extension-pack",
50
+ "charliermarsh.ruff",
51
+ "dbaeumer.vscode-eslint",
52
+ "esbenp.prettier-vscode",
53
+ "tamasfe.even-better-toml",
54
+ "Bar.python-import-helper",
55
+ ],
56
+ },
57
+ }
@@ -0,0 +1,24 @@
1
+ """Standalone example: fetch the channel list via /user/getChannels."""
2
+
3
+ import asyncio
4
+ import os
5
+
6
+ from SunsetLog import SunsetLogAPIError, SunsetLogClient
7
+
8
+
9
+ async def main() -> None:
10
+ token = os.environ["SUNSETLOG_TOKEN"]
11
+
12
+ async with SunsetLogClient(token) as client:
13
+ try:
14
+ result = await client.get_channel_list(gang=1, job=1)
15
+ except SunsetLogAPIError as e:
16
+ print(f"Error: {e} (status={e.status_code})")
17
+ return
18
+
19
+ for channel in result["channels"]:
20
+ print(f"{channel['id']:>6} {channel['index']:<30} {channel['label']}")
21
+
22
+
23
+ if __name__ == "__main__":
24
+ asyncio.run(main())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "SunsetLog"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.15"
@@ -30,7 +30,7 @@ name = "exceptiongroup"
30
30
  version = "1.3.1"
31
31
  source = { registry = "https://pypi.org/simple" }
32
32
  dependencies = [
33
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
33
+ { name = "typing-extensions" },
34
34
  ]
35
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
36
  wheels = [
@@ -85,7 +85,7 @@ wheels = [
85
85
 
86
86
  [[package]]
87
87
  name = "sunsetlog"
88
- version = "0.0.1"
88
+ version = "0.0.2"
89
89
  source = { editable = "." }
90
90
  dependencies = [
91
91
  { name = "httpx" },
sunsetlog-0.0.1/PKG-INFO DELETED
@@ -1,10 +0,0 @@
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
-
sunsetlog-0.0.1/README.md DELETED
@@ -1,2 +0,0 @@
1
- # SunsetLog
2
-
@@ -1,61 +0,0 @@
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
- }
File without changes
File without changes
File without changes