SunsetLog 0.0.1__py3-none-any.whl

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/__init__.py 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
+ ]
SunsetLog/client.py ADDED
@@ -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
SunsetLog/models.py ADDED
@@ -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,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,6 @@
1
+ SunsetLog/__init__.py,sha256=ld2wjZCI6TYb4erdvS9s07Wkpahzu3aQZrksnZhSl08,311
2
+ SunsetLog/client.py,sha256=_cqjGuJbAmr7CTBPQr9ci9nYtRgmEbW2DYcoQ5oAXP4,3845
3
+ SunsetLog/models.py,sha256=K0VYPiKMoKGNMMqWr7Vhz2SAa6gKLyPGRRYypteMv4o,456
4
+ sunsetlog-0.0.1.dist-info/METADATA,sha256=yuFWn7m1noVbMUHKXyEZaEX-pt6Z27GmzO82dYJDuMg,201
5
+ sunsetlog-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ sunsetlog-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any