pixivutil-server-client 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,65 @@
1
+ Metadata-Version: 2.3
2
+ Name: pixivutil-server-client
3
+ Version: 0.1.0
4
+ Summary: Async aiohttp client SDK for PixivUtil Server
5
+ Author: psilabs-dev
6
+ Requires-Dist: aiohttp>=3.13.3
7
+ Requires-Dist: pydantic>=2.12.4
8
+ Requires-Dist: pixivutil-server-common>=0.1.0
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
12
+ # pixivutil-server-client
13
+
14
+ Async `aiohttp` client SDK for PixivUtil Server.
15
+
16
+ ## Example
17
+
18
+ ```python
19
+ import asyncio
20
+
21
+ from pixivutil_client import PixivAsyncClient
22
+
23
+
24
+ async def main() -> None:
25
+ async with PixivAsyncClient(
26
+ base_url="http://localhost:8000",
27
+ api_key="your-api-key",
28
+ ) as client:
29
+ health = await client.health()
30
+ print(health)
31
+
32
+ queued = await client.queue_download_artwork(123456)
33
+ print(queued.task_id)
34
+
35
+
36
+ asyncio.run(main())
37
+ ```
38
+
39
+ ## Install
40
+
41
+ From PyPI:
42
+
43
+ ```sh
44
+ uv pip install pixivutil-server-client
45
+ ```
46
+
47
+ From the `pixivutil-server` project root:
48
+
49
+ ```sh
50
+ uv pip install -e ./PixivUtilClient
51
+ ```
52
+
53
+ From Git (published as a subdirectory):
54
+
55
+ ```sh
56
+ uv pip install "git+https://github.com/psilabs-dev/pixivutil-server.git@dev-2.4.0/main#subdirectory=PixivUtilClient"
57
+ ```
58
+
59
+ ## Test
60
+
61
+ From the `pixivutil-server` project root:
62
+
63
+ ```sh
64
+ uv run --package pixivutil-server-client pytest PixivUtilClient/tests
65
+ ```
@@ -0,0 +1,54 @@
1
+ # pixivutil-server-client
2
+
3
+ Async `aiohttp` client SDK for PixivUtil Server.
4
+
5
+ ## Example
6
+
7
+ ```python
8
+ import asyncio
9
+
10
+ from pixivutil_client import PixivAsyncClient
11
+
12
+
13
+ async def main() -> None:
14
+ async with PixivAsyncClient(
15
+ base_url="http://localhost:8000",
16
+ api_key="your-api-key",
17
+ ) as client:
18
+ health = await client.health()
19
+ print(health)
20
+
21
+ queued = await client.queue_download_artwork(123456)
22
+ print(queued.task_id)
23
+
24
+
25
+ asyncio.run(main())
26
+ ```
27
+
28
+ ## Install
29
+
30
+ From PyPI:
31
+
32
+ ```sh
33
+ uv pip install pixivutil-server-client
34
+ ```
35
+
36
+ From the `pixivutil-server` project root:
37
+
38
+ ```sh
39
+ uv pip install -e ./PixivUtilClient
40
+ ```
41
+
42
+ From Git (published as a subdirectory):
43
+
44
+ ```sh
45
+ uv pip install "git+https://github.com/psilabs-dev/pixivutil-server.git@dev-2.4.0/main#subdirectory=PixivUtilClient"
46
+ ```
47
+
48
+ ## Test
49
+
50
+ From the `pixivutil-server` project root:
51
+
52
+ ```sh
53
+ uv run --package pixivutil-server-client pytest PixivUtilClient/tests
54
+ ```
@@ -0,0 +1,9 @@
1
+ from pixivutil_client.client import PixivAsyncClient
2
+ from pixivutil_client.exceptions import PixivAPIError, PixivClientError, PixivTransportError
3
+
4
+ __all__ = [
5
+ "PixivAsyncClient",
6
+ "PixivAPIError",
7
+ "PixivClientError",
8
+ "PixivTransportError",
9
+ ]
@@ -0,0 +1,246 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+ from urllib.parse import quote
6
+
7
+ import aiohttp
8
+
9
+ from pixivutil_client.exceptions import PixivAPIError, PixivTransportError
10
+ from pixivutil_client.models import (
11
+ PixivImageComplete,
12
+ PixivMemberPortfolio,
13
+ PixivSeriesInfo,
14
+ PixivTagInfo,
15
+ QueueTaskResponse,
16
+ TagMetadataFilterMode,
17
+ TagSortOrder,
18
+ TagTypeMode,
19
+ )
20
+
21
+
22
+ class PixivAsyncClient:
23
+ """Async HTTP client for PixivUtil Server APIs."""
24
+
25
+ def __init__(
26
+ self,
27
+ base_url: str,
28
+ api_key: str | None = None,
29
+ timeout_seconds: float = 30,
30
+ ssl: bool | None = True,
31
+ session: aiohttp.ClientSession | None = None,
32
+ ) -> None:
33
+ self.base_url = base_url.rstrip("/")
34
+ self.api_key = api_key
35
+ self.timeout_seconds = timeout_seconds
36
+ self.ssl = ssl
37
+ self._session = session
38
+ self._owns_session = session is None
39
+
40
+ async def __aenter__(self) -> "PixivAsyncClient":
41
+ await self._ensure_session()
42
+ return self
43
+
44
+ async def __aexit__(self, *_: object) -> None:
45
+ await self.close()
46
+
47
+ async def close(self) -> None:
48
+ if self._session is not None and self._owns_session:
49
+ await self._session.close()
50
+ self._session = None
51
+
52
+ async def _ensure_session(self) -> aiohttp.ClientSession:
53
+ if self._session is None:
54
+ timeout = aiohttp.ClientTimeout(total=self.timeout_seconds)
55
+ self._session = aiohttp.ClientSession(timeout=timeout)
56
+ return self._session
57
+
58
+ def _auth_headers(self) -> dict[str, str]:
59
+ if not self.api_key:
60
+ return {}
61
+ return {"Authorization": f"Bearer {self.api_key}"}
62
+
63
+ async def _request(
64
+ self,
65
+ method: str,
66
+ path: str,
67
+ *,
68
+ params: dict[str, Any] | None = None,
69
+ json_body: dict[str, Any] | None = None,
70
+ ) -> Any:
71
+ session = await self._ensure_session()
72
+ url = f"{self.base_url}{path}"
73
+
74
+ try:
75
+ async with session.request(
76
+ method,
77
+ url,
78
+ params=params,
79
+ json=json_body,
80
+ headers=self._auth_headers(),
81
+ ssl=self.ssl,
82
+ ) as response:
83
+ payload = await self._decode_payload(response)
84
+ if response.status >= 400:
85
+ raise PixivAPIError(
86
+ response.status,
87
+ self._extract_error_message(payload),
88
+ body=payload,
89
+ )
90
+ return payload
91
+ except PixivAPIError:
92
+ raise
93
+ except aiohttp.ClientError as error:
94
+ raise PixivTransportError(str(error)) from error
95
+
96
+ async def _decode_payload(self, response: aiohttp.ClientResponse) -> Any:
97
+ raw_text = await response.text()
98
+ if not raw_text:
99
+ return None
100
+
101
+ try:
102
+ return json.loads(raw_text)
103
+ except json.JSONDecodeError:
104
+ return raw_text
105
+
106
+ def _extract_error_message(self, payload: Any) -> str:
107
+ if isinstance(payload, dict):
108
+ if "detail" in payload:
109
+ return str(payload["detail"])
110
+ if "error" in payload:
111
+ return str(payload["error"])
112
+ if "message" in payload:
113
+ return str(payload["message"])
114
+ if isinstance(payload, str):
115
+ return payload
116
+ return "Request failed"
117
+
118
+ async def health(self) -> str:
119
+ payload = await self._request("GET", "/api/health/")
120
+ return str(payload)
121
+
122
+ async def health_pixiv(self) -> str:
123
+ payload = await self._request("GET", "/api/health/pixiv")
124
+ return str(payload)
125
+
126
+ async def queue_download_artwork(self, artwork_id: int) -> QueueTaskResponse:
127
+ payload = await self._request("POST", f"/api/queue/download/artwork/{artwork_id}")
128
+ return QueueTaskResponse.model_validate(payload)
129
+
130
+ async def queue_download_member(self, member_id: int) -> QueueTaskResponse:
131
+ payload = await self._request("POST", f"/api/queue/download/member/{member_id}")
132
+ return QueueTaskResponse.model_validate(payload)
133
+
134
+ async def queue_download_tag(
135
+ self,
136
+ tag: str,
137
+ *,
138
+ bookmark_count: int | None = None,
139
+ sort_order: TagSortOrder = "date_d",
140
+ type_mode: TagTypeMode = "a",
141
+ wildcard: bool = False,
142
+ start_date: str | None = None,
143
+ end_date: str | None = None,
144
+ lookback_days: int | None = None,
145
+ ) -> QueueTaskResponse:
146
+ params: dict[str, Any] = {
147
+ "sort_order": sort_order,
148
+ "type_mode": type_mode,
149
+ "wildcard": wildcard,
150
+ }
151
+ if bookmark_count is not None:
152
+ params["bookmark_count"] = bookmark_count
153
+ if start_date is not None:
154
+ params["start_date"] = start_date
155
+ if end_date is not None:
156
+ params["end_date"] = end_date
157
+ if lookback_days is not None:
158
+ params["lookback_days"] = lookback_days
159
+
160
+ encoded_tag = quote(tag, safe="")
161
+ payload = await self._request("POST", f"/api/queue/download/tag/{encoded_tag}", params=params)
162
+ return QueueTaskResponse.model_validate(payload)
163
+
164
+ async def queue_delete_artwork(self, artwork_id: int, delete_metadata: bool = True) -> QueueTaskResponse:
165
+ payload = await self._request(
166
+ "DELETE",
167
+ f"/api/queue/download/artwork/{artwork_id}",
168
+ params={"delete_metadata": str(delete_metadata).lower()},
169
+ )
170
+ return QueueTaskResponse.model_validate(payload)
171
+
172
+ async def queue_metadata_artwork(self, artwork_id: int) -> QueueTaskResponse:
173
+ payload = await self._request("POST", f"/api/queue/metadata/artwork/{artwork_id}")
174
+ return QueueTaskResponse.model_validate(payload)
175
+
176
+ async def queue_metadata_member(self, member_id: int) -> QueueTaskResponse:
177
+ payload = await self._request("POST", f"/api/queue/metadata/member/{member_id}")
178
+ return QueueTaskResponse.model_validate(payload)
179
+
180
+ async def queue_metadata_series(self, series_id: int) -> QueueTaskResponse:
181
+ payload = await self._request("POST", f"/api/queue/metadata/series/{series_id}")
182
+ return QueueTaskResponse.model_validate(payload)
183
+
184
+ async def queue_metadata_tag(
185
+ self,
186
+ tag: str,
187
+ filter_mode: TagMetadataFilterMode = "none",
188
+ ) -> QueueTaskResponse:
189
+ encoded_tag = quote(tag, safe="")
190
+ payload = await self._request(
191
+ "POST",
192
+ f"/api/queue/metadata/tag/{encoded_tag}",
193
+ params={"filter_mode": filter_mode},
194
+ )
195
+ return QueueTaskResponse.model_validate(payload)
196
+
197
+ async def get_member_ids(self) -> list[int]:
198
+ payload = await self._request("GET", "/api/database/members")
199
+ return list(payload)
200
+
201
+ async def get_image_ids(self) -> list[int]:
202
+ payload = await self._request("GET", "/api/database/images")
203
+ return list(payload)
204
+
205
+ async def get_tags(self) -> list[str]:
206
+ payload = await self._request("GET", "/api/database/tags")
207
+ return list(payload)
208
+
209
+ async def get_series(self) -> list[str]:
210
+ payload = await self._request("GET", "/api/database/series")
211
+ return list(payload)
212
+
213
+ async def get_member(self, member_id: int) -> PixivMemberPortfolio:
214
+ payload = await self._request("GET", f"/api/database/member/{member_id}")
215
+ return PixivMemberPortfolio.model_validate(payload)
216
+
217
+ async def get_image(self, image_id: int) -> PixivImageComplete:
218
+ payload = await self._request("GET", f"/api/database/image/{image_id}")
219
+ return PixivImageComplete.model_validate(payload)
220
+
221
+ async def get_tag(self, tag_id: str) -> PixivTagInfo:
222
+ encoded_tag_id = quote(tag_id, safe="")
223
+ payload = await self._request("GET", f"/api/database/tag/{encoded_tag_id}")
224
+ return PixivTagInfo.model_validate(payload)
225
+
226
+ async def get_series_info(self, series_id: str) -> PixivSeriesInfo:
227
+ encoded_series_id = quote(series_id, safe="")
228
+ payload = await self._request("GET", f"/api/database/series/{encoded_series_id}")
229
+ return PixivSeriesInfo.model_validate(payload)
230
+
231
+ async def get_cookie(self) -> str:
232
+ payload = await self._request("GET", "/api/server/cookie")
233
+ return str(payload)
234
+
235
+ async def update_cookie(self, cookie: str) -> str:
236
+ encoded_cookie = quote(cookie, safe="")
237
+ payload = await self._request("PUT", f"/api/server/cookie/{encoded_cookie}")
238
+ return str(payload)
239
+
240
+ async def reset_database(self) -> str:
241
+ payload = await self._request("DELETE", "/api/server/database")
242
+ return str(payload)
243
+
244
+ async def reset_downloads(self) -> str:
245
+ payload = await self._request("DELETE", "/api/server/downloads")
246
+ return str(payload)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class PixivClientError(Exception):
5
+ """Base client exception."""
6
+
7
+
8
+ class PixivTransportError(PixivClientError):
9
+ """Network/transport-level error."""
10
+
11
+
12
+ class PixivAPIError(PixivClientError):
13
+ """API-level error response."""
14
+
15
+ def __init__(self, status: int, message: str, body: object | None = None) -> None:
16
+ super().__init__(f"HTTP {status}: {message}")
17
+ self.status = status
18
+ self.message = message
19
+ self.body = body
@@ -0,0 +1,43 @@
1
+ """
2
+ Compatibility module re-exporting shared API contracts.
3
+ """
4
+
5
+ from pixivutil_server_common.models import (
6
+ PixivDateInfo,
7
+ PixivImageComplete,
8
+ PixivImageToSeries,
9
+ PixivImageToTag,
10
+ PixivMangaImage,
11
+ PixivMasterImage,
12
+ PixivMasterMember,
13
+ PixivMasterSeries,
14
+ PixivMasterTag,
15
+ PixivMemberPortfolio,
16
+ PixivSeriesInfo,
17
+ PixivTagInfo,
18
+ PixivTagTranslation,
19
+ QueueTaskResponse,
20
+ TagMetadataFilterMode,
21
+ TagSortOrder,
22
+ TagTypeMode,
23
+ )
24
+
25
+ __all__ = [
26
+ "PixivDateInfo",
27
+ "PixivImageComplete",
28
+ "PixivImageToSeries",
29
+ "PixivImageToTag",
30
+ "PixivMangaImage",
31
+ "PixivMasterImage",
32
+ "PixivMasterMember",
33
+ "PixivMasterSeries",
34
+ "PixivMasterTag",
35
+ "PixivMemberPortfolio",
36
+ "PixivSeriesInfo",
37
+ "PixivTagInfo",
38
+ "PixivTagTranslation",
39
+ "QueueTaskResponse",
40
+ "TagMetadataFilterMode",
41
+ "TagSortOrder",
42
+ "TagTypeMode",
43
+ ]
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.9.25,<0.10.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "pixivutil-server-client"
7
+ version = "0.1.0"
8
+ description = "Async aiohttp client SDK for PixivUtil Server"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ authors = [
12
+ { name = "psilabs-dev" }
13
+ ]
14
+ dependencies = [
15
+ "aiohttp>=3.13.3",
16
+ "pydantic>=2.12.4",
17
+ "pixivutil-server-common>=0.1.0",
18
+ ]
19
+
20
+ [tool.uv.build-backend]
21
+ module-name = "pixivutil_client"
22
+ module-root = ""
23
+
24
+ [tool.uv.sources]
25
+ pixivutil-server-common = { workspace = true }
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=9.0.2",
30
+ "pytest-asyncio>=1.3.0",
31
+ "ruff>=0.15.0",
32
+ ]