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.
- aria2_rust-0.3.0/.gitignore +23 -0
- aria2_rust-0.3.0/PKG-INFO +13 -0
- aria2_rust-0.3.0/aria2_rust_client/__init__.py +33 -0
- aria2_rust-0.3.0/aria2_rust_client/client.py +214 -0
- aria2_rust-0.3.0/aria2_rust_client/errors.py +32 -0
- aria2_rust-0.3.0/aria2_rust_client/events.py +153 -0
- aria2_rust-0.3.0/aria2_rust_client/transport.py +256 -0
- aria2_rust-0.3.0/aria2_rust_client/types.py +185 -0
- aria2_rust-0.3.0/package-lock.json +6 -0
- aria2_rust-0.3.0/pyproject.toml +29 -0
- aria2_rust-0.3.0/tests/__init__.py +0 -0
- aria2_rust-0.3.0/tests/e2e/__init__.py +0 -0
- aria2_rust-0.3.0/tests/e2e/conftest.py +197 -0
- aria2_rust-0.3.0/tests/e2e/test_concurrent.py +19 -0
- aria2_rust-0.3.0/tests/e2e/test_error_handling.py +38 -0
- aria2_rust-0.3.0/tests/e2e/test_events.py +51 -0
- aria2_rust-0.3.0/tests/e2e/test_http_download.py +45 -0
- aria2_rust-0.3.0/tests/e2e/test_pause_resume.py +36 -0
- aria2_rust-0.3.0/tests/integration/__init__.py +0 -0
- aria2_rust-0.3.0/tests/integration/conftest.py +285 -0
- aria2_rust-0.3.0/tests/integration/test_auth.py +24 -0
- aria2_rust-0.3.0/tests/integration/test_multi_task.py +53 -0
- aria2_rust-0.3.0/tests/integration/test_options.py +35 -0
- aria2_rust-0.3.0/tests/integration/test_rpc_methods.py +101 -0
- aria2_rust-0.3.0/tests/unit/__init__.py +0 -0
- aria2_rust-0.3.0/tests/unit/test_client.py +338 -0
- aria2_rust-0.3.0/tests/unit/test_errors.py +104 -0
- aria2_rust-0.3.0/tests/unit/test_transport.py +232 -0
- aria2_rust-0.3.0/tests/unit/test_types.py +284 -0
|
@@ -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
|