Aria2-rust 0.3.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,23 @@
1
+ target/
2
+ aria2_original/
3
+ aria2-next/
4
+ .trae/
5
+ bindings/nodejs/node_modules/
6
+ bindings/python/aria2_rust_client/__pycache__/
7
+ bindings/python/tests/e2e/__pycache__/
8
+ bindings/python/tests/unit/__pycache__/
9
+ bindings/python/tests/integration/__pycache__/
10
+ bindings/python/tests/__pycache__/
11
+ *.txt
12
+ !aria2/tests/fixtures/compatibility_option_inventory.txt
13
+ !aria2-core/tests/fixtures/compatibility_option_policies.txt
14
+ *.md
15
+ *.7z
16
+ *.bak
17
+ *.bin
18
+ !docs/
19
+ AGENTS.md
20
+ docs/compatibility-status.md
21
+ docs/MIGRATION.md
22
+ .codex-python-deps/
23
+ !docs/compatibility-status.md
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: Aria2_rust
3
+ Version: 0.3.0
4
+ Summary: Python SDK for aria2-rust JSON-RPC interface
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: httpx>=0.25
7
+ Requires-Dist: typing-extensions>=4.0
8
+ Requires-Dist: websockets>=12.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
11
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
12
+ Requires-Dist: pytest>=7.0; extra == 'dev'
13
+ Requires-Dist: respx>=0.20; extra == 'dev'
@@ -0,0 +1,33 @@
1
+ from .client import Aria2Client
2
+ from .errors import Aria2Error, AuthError, ConnectionError, RpcError, TimeoutError
3
+ from .events import EventSubscriber
4
+ from .types import (
5
+ DownloadEvent,
6
+ DownloadStatus,
7
+ EventType,
8
+ FileInfo,
9
+ GlobalStat,
10
+ SessionInfo,
11
+ StatusInfo,
12
+ UriEntry,
13
+ VersionInfo,
14
+ )
15
+
16
+ __all__ = [
17
+ "Aria2Client",
18
+ "Aria2Error",
19
+ "AuthError",
20
+ "ConnectionError",
21
+ "RpcError",
22
+ "TimeoutError",
23
+ "EventSubscriber",
24
+ "DownloadEvent",
25
+ "DownloadStatus",
26
+ "EventType",
27
+ "FileInfo",
28
+ "GlobalStat",
29
+ "SessionInfo",
30
+ "StatusInfo",
31
+ "UriEntry",
32
+ "VersionInfo",
33
+ ]
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from typing_extensions import Self
7
+
8
+ from .errors import Aria2Error
9
+ from .events import EventSubscriber
10
+ from .transport import HttpTransport, Transport, WebSocketTransport
11
+ from .types import (
12
+ EventType,
13
+ GlobalStat,
14
+ SessionInfo,
15
+ StatusInfo,
16
+ VersionInfo,
17
+ )
18
+
19
+
20
+ def _http_to_ws(url: str) -> str:
21
+ if url.startswith("https://"):
22
+ return "wss://" + url[8:]
23
+ if url.startswith("http://"):
24
+ return "ws://" + url[7:]
25
+ return url
26
+
27
+
28
+ class Aria2Client:
29
+ def __init__(
30
+ self,
31
+ url: str = "http://localhost:6800/jsonrpc",
32
+ token: Optional[str] = None,
33
+ timeout: float = 30.0,
34
+ ) -> None:
35
+ self._url = url
36
+ self._token = token
37
+ self._timeout = timeout
38
+ self._transport: Transport
39
+
40
+ if url.startswith("ws://") or url.startswith("wss://"):
41
+ self._transport = WebSocketTransport(url, token, timeout)
42
+ else:
43
+ self._transport = HttpTransport(url, token, timeout)
44
+
45
+ async def __aenter__(self) -> Self:
46
+ return self
47
+
48
+ async def __aexit__(self, *args: Any) -> None:
49
+ await self.close()
50
+
51
+ async def _call(self, method: str, params: Optional[list] = None) -> Any:
52
+ return await self._transport.send_request(method, params or [])
53
+
54
+ async def add_uri(
55
+ self, uris: List[str], options: Optional[Dict] = None
56
+ ) -> str:
57
+ params: list = [uris]
58
+ if options is not None:
59
+ params.append(options)
60
+ result = await self._call("aria2.addUri", params)
61
+ return str(result)
62
+
63
+ async def add_torrent(
64
+ self, torrent: bytes, options: Optional[Dict] = None
65
+ ) -> str:
66
+ encoded = base64.b64encode(torrent).decode("ascii")
67
+ params: list = [encoded]
68
+ if options is not None:
69
+ params.append(options)
70
+ result = await self._call("aria2.addTorrent", params)
71
+ return str(result)
72
+
73
+ async def add_metalink(
74
+ self, metalink: bytes, options: Optional[Dict] = None
75
+ ) -> str:
76
+ encoded = base64.b64encode(metalink).decode("ascii")
77
+ params: list = [encoded]
78
+ if options is not None:
79
+ params.append(options)
80
+ result = await self._call("aria2.addMetalink", params)
81
+ return str(result)
82
+
83
+ async def remove(self, gid: str) -> str:
84
+ result = await self._call("aria2.remove", [gid])
85
+ return str(result)
86
+
87
+ async def pause(self, gid: str) -> str:
88
+ result = await self._call("aria2.pause", [gid])
89
+ return str(result)
90
+
91
+ async def unpause(self, gid: str) -> str:
92
+ result = await self._call("aria2.unpause", [gid])
93
+ return str(result)
94
+
95
+ async def force_pause(self, gid: str) -> str:
96
+ result = await self._call("aria2.forcePause", [gid])
97
+ return str(result)
98
+
99
+ async def force_remove(self, gid: str) -> str:
100
+ result = await self._call("aria2.forceRemove", [gid])
101
+ return str(result)
102
+
103
+ async def force_unpause(self, gid: str) -> str:
104
+ result = await self._call("aria2.forceUnpause", [gid])
105
+ return str(result)
106
+
107
+ async def tell_status(
108
+ self, gid: str, keys: Optional[List[str]] = None
109
+ ) -> StatusInfo:
110
+ params: list = [gid]
111
+ if keys is not None:
112
+ params.append(keys)
113
+ result = await self._call("aria2.tellStatus", params)
114
+ if isinstance(result, dict):
115
+ return StatusInfo.from_dict(result)
116
+ raise Aria2Error(f"Unexpected result type for tellStatus: {type(result)}")
117
+
118
+ async def tell_active(
119
+ self, keys: Optional[List[str]] = None
120
+ ) -> List[StatusInfo]:
121
+ params: list = []
122
+ if keys is not None:
123
+ params.append(keys)
124
+ result = await self._call("aria2.tellActive", params)
125
+ if isinstance(result, list):
126
+ return [StatusInfo.from_dict(item) for item in result if isinstance(item, dict)]
127
+ raise Aria2Error(f"Unexpected result type for tellActive: {type(result)}")
128
+
129
+ async def tell_waiting(
130
+ self, offset: int, num: int, keys: Optional[List[str]] = None
131
+ ) -> List[StatusInfo]:
132
+ params: list = [offset, num]
133
+ if keys is not None:
134
+ params.append(keys)
135
+ result = await self._call("aria2.tellWaiting", params)
136
+ if isinstance(result, list):
137
+ return [StatusInfo.from_dict(item) for item in result if isinstance(item, dict)]
138
+ raise Aria2Error(f"Unexpected result type for tellWaiting: {type(result)}")
139
+
140
+ async def tell_stopped(
141
+ self, offset: int, num: int, keys: Optional[List[str]] = None
142
+ ) -> List[StatusInfo]:
143
+ params: list = [offset, num]
144
+ if keys is not None:
145
+ params.append(keys)
146
+ result = await self._call("aria2.tellStopped", params)
147
+ if isinstance(result, list):
148
+ return [StatusInfo.from_dict(item) for item in result if isinstance(item, dict)]
149
+ raise Aria2Error(f"Unexpected result type for tellStopped: {type(result)}")
150
+
151
+ async def get_global_stat(self) -> GlobalStat:
152
+ result = await self._call("aria2.getGlobalStat")
153
+ if isinstance(result, dict):
154
+ return GlobalStat.from_dict(result)
155
+ raise Aria2Error(f"Unexpected result type for getGlobalStat: {type(result)}")
156
+
157
+ async def purge_download_result(self) -> str:
158
+ result = await self._call("aria2.purgeDownloadResult")
159
+ return str(result) if result is not None else "OK"
160
+
161
+ async def remove_download_result(self, gid: str) -> str:
162
+ result = await self._call("aria2.removeDownloadResult", [gid])
163
+ return str(result) if result is not None else "OK"
164
+
165
+ async def get_global_option(self) -> Dict:
166
+ result = await self._call("aria2.getGlobalOption")
167
+ return result if isinstance(result, dict) else {}
168
+
169
+ async def change_global_option(self, options: Dict) -> str:
170
+ result = await self._call("aria2.changeGlobalOption", [options])
171
+ return str(result) if result is not None else "OK"
172
+
173
+ async def get_option(self, gid: str) -> Dict:
174
+ result = await self._call("aria2.getOption", [gid])
175
+ return result if isinstance(result, dict) else {}
176
+
177
+ async def change_option(self, gid: str, options: Dict) -> str:
178
+ result = await self._call("aria2.changeOption", [gid, options])
179
+ return str(result) if result is not None else "OK"
180
+
181
+ async def get_version(self) -> VersionInfo:
182
+ result = await self._call("aria2.getVersion")
183
+ if isinstance(result, dict):
184
+ return VersionInfo.from_dict(result)
185
+ raise Aria2Error(f"Unexpected result type for getVersion: {type(result)}")
186
+
187
+ async def get_session_info(self) -> SessionInfo:
188
+ result = await self._call("aria2.getSessionInfo")
189
+ if isinstance(result, dict):
190
+ return SessionInfo.from_dict(result)
191
+ raise Aria2Error(f"Unexpected result type for getSessionInfo: {type(result)}")
192
+
193
+ async def shutdown(self) -> str:
194
+ result = await self._call("aria2.shutdown")
195
+ return str(result) if result is not None else "OK"
196
+
197
+ async def force_shutdown(self) -> str:
198
+ result = await self._call("aria2.forceShutdown")
199
+ return str(result) if result is not None else "OK"
200
+
201
+ async def save_session(self) -> str:
202
+ result = await self._call("aria2.saveSession")
203
+ return str(result) if result is not None else "OK"
204
+
205
+ async def subscribe_events(
206
+ self, filter: Optional[List[EventType]] = None
207
+ ) -> EventSubscriber:
208
+ ws_url = _http_to_ws(self._url)
209
+ subscriber = EventSubscriber(ws_url, self._token, filter)
210
+ await subscriber.start()
211
+ return subscriber
212
+
213
+ async def close(self) -> None:
214
+ await self._transport.close()
@@ -0,0 +1,32 @@
1
+ class Aria2Error(Exception):
2
+ code: int = -1
3
+
4
+ def __init__(self, message: str, code: int = -1) -> None:
5
+ self.code = code
6
+ super().__init__(message)
7
+
8
+
9
+ class ConnectionError(Aria2Error):
10
+ code: int = -2
11
+
12
+ def __init__(self, message: str = "Connection error", code: int = -2) -> None:
13
+ super().__init__(message, code)
14
+
15
+
16
+ class AuthError(Aria2Error):
17
+ code: int = -3
18
+
19
+ def __init__(self, message: str = "Authentication error", code: int = -3) -> None:
20
+ super().__init__(message, code)
21
+
22
+
23
+ class RpcError(Aria2Error):
24
+ def __init__(self, message: str, code: int = -1) -> None:
25
+ super().__init__(message, code)
26
+
27
+
28
+ class TimeoutError(Aria2Error):
29
+ code: int = -4
30
+
31
+ def __init__(self, message: str = "Request timeout", code: int = -4) -> None:
32
+ super().__init__(message, code)
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import List, Optional
6
+
7
+ try:
8
+ from typing import Self
9
+ except ImportError:
10
+ from typing_extensions import Self
11
+
12
+ from .errors import ConnectionError, TimeoutError
13
+ from .types import DownloadEvent, EventType
14
+
15
+ _EVENT_METHOD_MAP: dict[str, EventType] = {
16
+ "aria2.onDownloadStart": EventType.DOWNLOAD_START,
17
+ "aria2.onDownloadPause": EventType.DOWNLOAD_PAUSE,
18
+ "aria2.onDownloadStop": EventType.DOWNLOAD_STOP,
19
+ "aria2.onDownloadComplete": EventType.DOWNLOAD_COMPLETE,
20
+ "aria2.onDownloadError": EventType.DOWNLOAD_ERROR,
21
+ "aria2.onBtDownloadComplete": EventType.BT_DOWNLOAD_COMPLETE,
22
+ }
23
+
24
+
25
+ class EventSubscriber:
26
+ def __init__(
27
+ self,
28
+ ws_url: str,
29
+ token: Optional[str] = None,
30
+ filter: Optional[List[EventType]] = None,
31
+ ) -> None:
32
+ self._ws_url = ws_url
33
+ self._token = token
34
+ self._filter = set(filter) if filter is not None else None
35
+ self._ws: Any = None
36
+ self._listener_task: Optional[asyncio.Task] = None
37
+ self._queue: asyncio.Queue[Optional[DownloadEvent]] = asyncio.Queue()
38
+ self._closed = False
39
+ self._reconnect_attempts = 0
40
+ self._max_reconnect_attempts = 5
41
+
42
+ def _should_include(self, event: DownloadEvent) -> bool:
43
+ if self._filter is None:
44
+ return True
45
+ return event.event_type in self._filter
46
+
47
+ async def _connect(self) -> None:
48
+ try:
49
+ import websockets
50
+
51
+ self._ws = await websockets.connect(self._ws_url)
52
+ self._reconnect_attempts = 0
53
+ except Exception as exc:
54
+ raise ConnectionError(f"Failed to connect WebSocket: {exc}") from exc
55
+
56
+ async def _listen(self) -> None:
57
+ while not self._closed:
58
+ if self._ws is None:
59
+ try:
60
+ await self._connect()
61
+ except ConnectionError:
62
+ if not await self._try_reconnect():
63
+ break
64
+ continue
65
+
66
+ try:
67
+ async for raw_message in self._ws:
68
+ try:
69
+ message = json.loads(raw_message)
70
+ except (json.JSONDecodeError, TypeError):
71
+ continue
72
+
73
+ method = message.get("method", "")
74
+ if not method.startswith("aria2.on"):
75
+ continue
76
+
77
+ params = message.get("params", [{}])
78
+ event_params = params[0] if isinstance(params, list) and params else {}
79
+ if not isinstance(event_params, dict):
80
+ event_params = {}
81
+
82
+ event = DownloadEvent.from_rpc_notification(method, event_params)
83
+ if self._should_include(event):
84
+ await self._queue.put(event)
85
+ except asyncio.CancelledError:
86
+ break
87
+ except Exception:
88
+ if self._closed:
89
+ break
90
+ if not await self._try_reconnect():
91
+ break
92
+
93
+ await self._queue.put(None)
94
+
95
+ async def _try_reconnect(self) -> bool:
96
+ if self._closed:
97
+ return False
98
+ if self._reconnect_attempts >= self._max_reconnect_attempts:
99
+ return False
100
+
101
+ backoff = min(2**self._reconnect_attempts, 16)
102
+ self._reconnect_attempts += 1
103
+
104
+ try:
105
+ await asyncio.sleep(backoff)
106
+ except asyncio.CancelledError:
107
+ return False
108
+
109
+ try:
110
+ await self._connect()
111
+ return True
112
+ except ConnectionError:
113
+ return await self._try_reconnect()
114
+
115
+ async def start(self) -> None:
116
+ await self._connect()
117
+ self._listener_task = asyncio.create_task(self._listen())
118
+
119
+ def __aiter__(self) -> Self:
120
+ return self
121
+
122
+ async def __anext__(self) -> DownloadEvent:
123
+ if self._closed:
124
+ raise StopAsyncIteration
125
+
126
+ event = await self._queue.get()
127
+ if event is None:
128
+ raise StopAsyncIteration
129
+ return event
130
+
131
+ async def close(self) -> None:
132
+ self._closed = True
133
+
134
+ if self._listener_task is not None:
135
+ self._listener_task.cancel()
136
+ try:
137
+ await self._listener_task
138
+ except asyncio.CancelledError:
139
+ pass
140
+ self._listener_task = None
141
+
142
+ if self._ws is not None:
143
+ try:
144
+ await self._ws.close()
145
+ except Exception:
146
+ pass
147
+ self._ws = None
148
+
149
+ while not self._queue.empty():
150
+ try:
151
+ self._queue.get_nowait()
152
+ except asyncio.QueueEmpty:
153
+ break