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.
- pixivutil_server_client-0.1.0/PKG-INFO +65 -0
- pixivutil_server_client-0.1.0/README.md +54 -0
- pixivutil_server_client-0.1.0/pixivutil_client/__init__.py +9 -0
- pixivutil_server_client-0.1.0/pixivutil_client/client.py +246 -0
- pixivutil_server_client-0.1.0/pixivutil_client/exceptions.py +19 -0
- pixivutil_server_client-0.1.0/pixivutil_client/models.py +43 -0
- pixivutil_server_client-0.1.0/pyproject.toml +32 -0
|
@@ -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,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
|
+
]
|