novel-downloader 1.2.2__py3-none-any.whl → 1.3.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.
- novel_downloader/__init__.py +1 -2
- novel_downloader/cli/__init__.py +0 -1
- novel_downloader/cli/clean.py +2 -10
- novel_downloader/cli/download.py +16 -22
- novel_downloader/cli/interactive.py +0 -1
- novel_downloader/cli/main.py +1 -3
- novel_downloader/cli/settings.py +8 -8
- novel_downloader/config/__init__.py +0 -1
- novel_downloader/config/adapter.py +32 -27
- novel_downloader/config/loader.py +116 -108
- novel_downloader/config/models.py +35 -29
- novel_downloader/config/site_rules.py +2 -4
- novel_downloader/core/__init__.py +0 -1
- novel_downloader/core/downloaders/__init__.py +4 -4
- novel_downloader/core/downloaders/base/__init__.py +14 -0
- novel_downloader/core/downloaders/{base_async_downloader.py → base/base_async.py} +49 -53
- novel_downloader/core/downloaders/{base_downloader.py → base/base_sync.py} +64 -43
- novel_downloader/core/downloaders/biquge/__init__.py +12 -0
- novel_downloader/core/downloaders/biquge/biquge_sync.py +25 -0
- novel_downloader/core/downloaders/common/__init__.py +14 -0
- novel_downloader/core/downloaders/{common_asynb_downloader.py → common/common_async.py} +42 -33
- novel_downloader/core/downloaders/{common_downloader.py → common/common_sync.py} +33 -21
- novel_downloader/core/downloaders/qidian/__init__.py +10 -0
- novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +79 -62
- novel_downloader/core/factory/__init__.py +4 -5
- novel_downloader/core/factory/{downloader_factory.py → downloader.py} +25 -26
- novel_downloader/core/factory/{parser_factory.py → parser.py} +12 -14
- novel_downloader/core/factory/{requester_factory.py → requester.py} +29 -16
- novel_downloader/core/factory/{saver_factory.py → saver.py} +4 -9
- novel_downloader/core/interfaces/__init__.py +8 -9
- novel_downloader/core/interfaces/{async_downloader_protocol.py → async_downloader.py} +4 -5
- novel_downloader/core/interfaces/{async_requester_protocol.py → async_requester.py} +23 -12
- novel_downloader/core/interfaces/{parser_protocol.py → parser.py} +11 -6
- novel_downloader/core/interfaces/{saver_protocol.py → saver.py} +2 -3
- novel_downloader/core/interfaces/{downloader_protocol.py → sync_downloader.py} +6 -7
- novel_downloader/core/interfaces/{requester_protocol.py → sync_requester.py} +31 -17
- novel_downloader/core/parsers/__init__.py +5 -4
- novel_downloader/core/parsers/{base_parser.py → base.py} +18 -9
- novel_downloader/core/parsers/biquge/__init__.py +10 -0
- novel_downloader/core/parsers/biquge/main_parser.py +126 -0
- novel_downloader/core/parsers/{common_parser → common}/__init__.py +2 -3
- novel_downloader/core/parsers/{common_parser → common}/helper.py +13 -13
- novel_downloader/core/parsers/{common_parser → common}/main_parser.py +15 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_encrypted.py +40 -48
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_normal.py +17 -21
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_router.py +10 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/main_parser.py +14 -10
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_encrypted.py +36 -44
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_normal.py +19 -23
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_router.py +10 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/main_parser.py +14 -10
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/node_decryptor.py +7 -10
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/book_info_parser.py +5 -6
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +7 -8
- novel_downloader/core/requesters/__init__.py +9 -5
- novel_downloader/core/requesters/base/__init__.py +16 -0
- novel_downloader/core/requesters/{base_async_session.py → base/async_session.py} +177 -73
- novel_downloader/core/requesters/base/browser.py +340 -0
- novel_downloader/core/requesters/base/session.py +364 -0
- novel_downloader/core/requesters/biquge/__init__.py +12 -0
- novel_downloader/core/requesters/biquge/session.py +90 -0
- novel_downloader/core/requesters/{common_requester → common}/__init__.py +4 -5
- novel_downloader/core/requesters/common/async_session.py +96 -0
- novel_downloader/core/requesters/common/session.py +113 -0
- novel_downloader/core/requesters/qidian/__init__.py +21 -0
- novel_downloader/core/requesters/qidian/broswer.py +307 -0
- novel_downloader/core/requesters/qidian/session.py +287 -0
- novel_downloader/core/savers/__init__.py +5 -3
- novel_downloader/core/savers/{base_saver.py → base.py} +12 -13
- novel_downloader/core/savers/biquge.py +25 -0
- novel_downloader/core/savers/{common_saver → common}/__init__.py +2 -3
- novel_downloader/core/savers/{common_saver/common_epub.py → common/epub.py} +23 -51
- novel_downloader/core/savers/{common_saver → common}/main_saver.py +43 -9
- novel_downloader/core/savers/{common_saver/common_txt.py → common/txt.py} +16 -46
- novel_downloader/core/savers/epub_utils/__init__.py +0 -1
- novel_downloader/core/savers/epub_utils/css_builder.py +13 -7
- novel_downloader/core/savers/epub_utils/initializer.py +4 -5
- novel_downloader/core/savers/epub_utils/text_to_html.py +2 -3
- novel_downloader/core/savers/epub_utils/volume_intro.py +1 -3
- novel_downloader/core/savers/{qidian_saver.py → qidian.py} +12 -6
- novel_downloader/locales/en.json +8 -4
- novel_downloader/locales/zh.json +5 -1
- novel_downloader/resources/config/settings.toml +88 -0
- novel_downloader/utils/cache.py +2 -2
- novel_downloader/utils/chapter_storage.py +340 -0
- novel_downloader/utils/constants.py +6 -4
- novel_downloader/utils/crypto_utils.py +3 -3
- novel_downloader/utils/file_utils/__init__.py +0 -1
- novel_downloader/utils/file_utils/io.py +12 -17
- novel_downloader/utils/file_utils/normalize.py +1 -3
- novel_downloader/utils/file_utils/sanitize.py +2 -9
- novel_downloader/utils/fontocr/__init__.py +0 -1
- novel_downloader/utils/fontocr/ocr_v1.py +19 -22
- novel_downloader/utils/fontocr/ocr_v2.py +147 -60
- novel_downloader/utils/hash_store.py +19 -20
- novel_downloader/utils/hash_utils.py +0 -1
- novel_downloader/utils/i18n.py +3 -4
- novel_downloader/utils/logger.py +5 -6
- novel_downloader/utils/model_loader.py +5 -8
- novel_downloader/utils/network.py +9 -10
- novel_downloader/utils/state.py +6 -7
- novel_downloader/utils/text_utils/__init__.py +0 -1
- novel_downloader/utils/text_utils/chapter_formatting.py +2 -7
- novel_downloader/utils/text_utils/diff_display.py +0 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -4
- novel_downloader/utils/text_utils/text_cleaning.py +0 -1
- novel_downloader/utils/time_utils/__init__.py +0 -1
- novel_downloader/utils/time_utils/datetime_utils.py +8 -10
- novel_downloader/utils/time_utils/sleep_utils.py +1 -3
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/METADATA +14 -17
- novel_downloader-1.3.1.dist-info/RECORD +127 -0
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/WHEEL +1 -1
- novel_downloader/core/requesters/base_browser.py +0 -214
- novel_downloader/core/requesters/base_session.py +0 -246
- novel_downloader/core/requesters/common_requester/common_async_session.py +0 -98
- novel_downloader/core/requesters/common_requester/common_session.py +0 -126
- novel_downloader/core/requesters/qidian_requester/__init__.py +0 -22
- novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +0 -396
- novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
- novel_downloader/resources/config/settings.yaml +0 -76
- novel_downloader-1.2.2.dist-info/RECORD +0 -115
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,6 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
|
-
novel_downloader.core.requesters.
|
3
|
+
novel_downloader.core.requesters.base.async_session
|
5
4
|
---------------------------------------------------
|
6
5
|
|
7
6
|
This module defines the BaseAsyncSession class, which provides asynchronous
|
@@ -12,8 +11,11 @@ cookie handling, and defines abstract methods for subclasses.
|
|
12
11
|
|
13
12
|
import abc
|
14
13
|
import asyncio
|
14
|
+
import logging
|
15
|
+
import random
|
15
16
|
import time
|
16
|
-
|
17
|
+
import types
|
18
|
+
from typing import Any, Literal, Self
|
17
19
|
|
18
20
|
import aiohttp
|
19
21
|
from aiohttp import ClientResponse, ClientSession, ClientTimeout, TCPConnector
|
@@ -25,7 +27,8 @@ from novel_downloader.utils.constants import DEFAULT_USER_HEADERS
|
|
25
27
|
|
26
28
|
class RateLimiter:
|
27
29
|
"""
|
28
|
-
Simple async token-bucket rate limiter:
|
30
|
+
Simple async token-bucket rate limiter:
|
31
|
+
ensures no more than rate_per_sec
|
29
32
|
requests are started per second, across all coroutines.
|
30
33
|
"""
|
31
34
|
|
@@ -40,7 +43,8 @@ class RateLimiter:
|
|
40
43
|
elapsed = now - self._last
|
41
44
|
delay = self._interval - elapsed
|
42
45
|
if delay > 0:
|
43
|
-
|
46
|
+
jitter = random.uniform(0, 0.3)
|
47
|
+
await asyncio.sleep(delay + jitter)
|
44
48
|
self._last = time.monotonic()
|
45
49
|
|
46
50
|
|
@@ -61,10 +65,10 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
61
65
|
def is_async(self) -> Literal[True]:
|
62
66
|
return True
|
63
67
|
|
64
|
-
def
|
68
|
+
def __init__(
|
65
69
|
self,
|
66
70
|
config: RequesterConfig,
|
67
|
-
cookies:
|
71
|
+
cookies: dict[str, str] | None = None,
|
68
72
|
) -> None:
|
69
73
|
"""
|
70
74
|
Initialize the async session with configuration.
|
@@ -74,26 +78,30 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
74
78
|
:param cookies: Optional initial cookies to set on the session.
|
75
79
|
"""
|
76
80
|
self._config = config
|
77
|
-
self._timeout = config.timeout
|
78
81
|
self._retry_times = config.retry_times
|
79
|
-
self._retry_interval = config.
|
82
|
+
self._retry_interval = config.backoff_factor
|
83
|
+
self._timeout = config.timeout
|
84
|
+
self._max_rps = config.max_rps
|
85
|
+
self._max_connections = config.max_connections
|
86
|
+
|
80
87
|
self._cookies = cookies or {}
|
81
88
|
self._headers = DEFAULT_USER_HEADERS.copy()
|
82
|
-
self._session:
|
83
|
-
self._rate_limiter:
|
89
|
+
self._session: ClientSession | None = None
|
90
|
+
self._rate_limiter: RateLimiter | None = None
|
84
91
|
|
85
|
-
|
92
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
93
|
+
|
94
|
+
self._init_session()
|
95
|
+
|
96
|
+
def _init_session(self) -> None:
|
86
97
|
"""
|
87
98
|
Set up the aiohttp.ClientSession with timeout, connector, headers, and cookies.
|
88
99
|
"""
|
89
|
-
|
90
|
-
|
91
|
-
self._rate_limiter = RateLimiter(max_rps)
|
100
|
+
if self._max_rps is not None:
|
101
|
+
self._rate_limiter = RateLimiter(self._max_rps)
|
92
102
|
|
93
103
|
timeout = ClientTimeout(total=self._timeout)
|
94
|
-
connector = TCPConnector(
|
95
|
-
limit_per_host=getattr(self._config, "max_connections", 10)
|
96
|
-
)
|
104
|
+
connector = TCPConnector(limit_per_host=self._max_connections)
|
97
105
|
self._session = ClientSession(
|
98
106
|
timeout=timeout,
|
99
107
|
connector=connector,
|
@@ -101,7 +109,13 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
101
109
|
cookies=self._cookies,
|
102
110
|
)
|
103
111
|
|
104
|
-
async def login(
|
112
|
+
async def login(
|
113
|
+
self,
|
114
|
+
username: str = "",
|
115
|
+
password: str = "",
|
116
|
+
manual_login: bool = False,
|
117
|
+
**kwargs: Any,
|
118
|
+
) -> bool:
|
105
119
|
"""
|
106
120
|
Attempt to log in asynchronously.
|
107
121
|
Override in subclasses that require authentication.
|
@@ -115,7 +129,9 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
115
129
|
|
116
130
|
@abc.abstractmethod
|
117
131
|
async def get_book_info(
|
118
|
-
self,
|
132
|
+
self,
|
133
|
+
book_id: str,
|
134
|
+
**kwargs: Any,
|
119
135
|
) -> str:
|
120
136
|
"""
|
121
137
|
Fetch the raw HTML (or JSON) of the book info page asynchronously.
|
@@ -128,7 +144,10 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
128
144
|
|
129
145
|
@abc.abstractmethod
|
130
146
|
async def get_book_chapter(
|
131
|
-
self,
|
147
|
+
self,
|
148
|
+
book_id: str,
|
149
|
+
chapter_id: str,
|
150
|
+
**kwargs: Any,
|
132
151
|
) -> str:
|
133
152
|
"""
|
134
153
|
Fetch the raw HTML (or JSON) of a single chapter asynchronously.
|
@@ -140,7 +159,11 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
140
159
|
"""
|
141
160
|
...
|
142
161
|
|
143
|
-
async def get_bookcase(
|
162
|
+
async def get_bookcase(
|
163
|
+
self,
|
164
|
+
page: int = 1,
|
165
|
+
**kwargs: Any,
|
166
|
+
) -> str:
|
144
167
|
"""
|
145
168
|
Optional: Retrieve the HTML content of the authenticated user's bookcase page.
|
146
169
|
Subclasses that support user login/bookcase should override this.
|
@@ -162,17 +185,12 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
162
185
|
:return: The response body as text.
|
163
186
|
:raises: aiohttp.ClientError on final failure.
|
164
187
|
"""
|
165
|
-
if self._session is None:
|
166
|
-
await self._setup()
|
167
|
-
if self._session is None:
|
168
|
-
raise RuntimeError("Session not initialized after setup")
|
169
|
-
|
170
188
|
if self._rate_limiter:
|
171
189
|
await self._rate_limiter.wait()
|
172
190
|
|
173
191
|
for attempt in range(self._retry_times + 1):
|
174
192
|
try:
|
175
|
-
async with self.
|
193
|
+
async with self.session.get(url, **kwargs) as resp:
|
176
194
|
resp.raise_for_status()
|
177
195
|
text: str = await resp.text()
|
178
196
|
return text
|
@@ -185,7 +203,10 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
185
203
|
raise RuntimeError("Unreachable code reached in fetch()")
|
186
204
|
|
187
205
|
async def get(
|
188
|
-
self,
|
206
|
+
self,
|
207
|
+
url: str,
|
208
|
+
params: dict[str, Any] | None = None,
|
209
|
+
**kwargs: Any,
|
189
210
|
) -> ClientResponse:
|
190
211
|
"""
|
191
212
|
Send an HTTP GET request asynchronously.
|
@@ -196,20 +217,13 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
196
217
|
:return: aiohttp.ClientResponse object.
|
197
218
|
:raises RuntimeError: If the session is not initialized.
|
198
219
|
"""
|
199
|
-
|
200
|
-
await self._setup()
|
201
|
-
if self._session is None:
|
202
|
-
raise RuntimeError("Session not initialized after setup")
|
203
|
-
|
204
|
-
if self._rate_limiter:
|
205
|
-
await self._rate_limiter.wait()
|
206
|
-
return await self._session.get(url, params=params, **kwargs)
|
220
|
+
return await self._request("GET", url, params=params, **kwargs)
|
207
221
|
|
208
222
|
async def post(
|
209
223
|
self,
|
210
224
|
url: str,
|
211
|
-
data:
|
212
|
-
json:
|
225
|
+
data: dict[str, Any] | bytes | None = None,
|
226
|
+
json: dict[str, Any] | None = None,
|
213
227
|
**kwargs: Any,
|
214
228
|
) -> ClientResponse:
|
215
229
|
"""
|
@@ -222,14 +236,7 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
222
236
|
:return: aiohttp.ClientResponse object.
|
223
237
|
:raises RuntimeError: If the session is not initialized.
|
224
238
|
"""
|
225
|
-
|
226
|
-
await self._setup()
|
227
|
-
if self._session is None:
|
228
|
-
raise RuntimeError("Session not initialized after setup")
|
229
|
-
|
230
|
-
if self._rate_limiter:
|
231
|
-
await self._rate_limiter.wait()
|
232
|
-
return await self._session.post(url, data=data, json=json, **kwargs)
|
239
|
+
return await self._request("POST", url, data=data, json=json, **kwargs)
|
233
240
|
|
234
241
|
@property
|
235
242
|
def session(self) -> ClientSession:
|
@@ -243,41 +250,106 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
243
250
|
return self._session
|
244
251
|
|
245
252
|
@property
|
246
|
-
def
|
247
|
-
"""
|
248
|
-
|
253
|
+
def cookies(self) -> dict[str, str]:
|
254
|
+
"""
|
255
|
+
Get the current session cookies.
|
249
256
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
257
|
+
:return: A dict mapping cookie names to their values.
|
258
|
+
"""
|
259
|
+
if self._session:
|
260
|
+
return {c.key: c.value for c in self._session.cookie_jar}
|
261
|
+
else:
|
262
|
+
return self._cookies
|
254
263
|
|
255
264
|
@property
|
256
|
-
def
|
257
|
-
"""
|
258
|
-
|
265
|
+
def headers(self) -> dict[str, str]:
|
266
|
+
"""
|
267
|
+
Get the current session headers.
|
259
268
|
|
260
|
-
|
261
|
-
|
262
|
-
|
269
|
+
:return: A dict mapping header names to their values.
|
270
|
+
"""
|
271
|
+
if self._session:
|
272
|
+
return dict(self._session.headers)
|
273
|
+
else:
|
274
|
+
return self._headers
|
275
|
+
|
276
|
+
def get_header(self, key: str, default: Any = None) -> Any:
|
263
277
|
"""
|
264
|
-
|
278
|
+
Retrieve a specific header value by name.
|
265
279
|
|
266
|
-
:param
|
267
|
-
:param
|
280
|
+
:param key: The header name to look up.
|
281
|
+
:param default: The value to return if the header is not present.
|
282
|
+
:return: The header value if present, else default.
|
268
283
|
"""
|
269
|
-
|
270
|
-
|
271
|
-
self._cookies.update({str(k): str(v) for k, v in cookies.items()})
|
284
|
+
if self._session:
|
285
|
+
return self._session.headers.get(key, default)
|
272
286
|
else:
|
273
|
-
|
274
|
-
|
287
|
+
return self._headers.get(key, default)
|
288
|
+
|
289
|
+
def update_header(self, key: str, value: str) -> None:
|
290
|
+
"""
|
291
|
+
Update or add a single header in the session.
|
292
|
+
|
293
|
+
:param key: The name of the header.
|
294
|
+
:param value: The value of the header.
|
295
|
+
"""
|
296
|
+
self._headers[key] = value
|
297
|
+
if self._session:
|
298
|
+
self._session.headers[key] = value
|
299
|
+
|
300
|
+
def update_headers(self, headers: dict[str, str]) -> None:
|
301
|
+
"""
|
302
|
+
Update or add multiple headers in the session.
|
303
|
+
|
304
|
+
:param headers: A dictionary of header key-value pairs.
|
305
|
+
"""
|
306
|
+
self._headers.update(headers)
|
307
|
+
if self._session:
|
308
|
+
self._session.headers.update(headers)
|
309
|
+
|
310
|
+
def update_cookie(self, key: str, value: str) -> None:
|
311
|
+
"""
|
312
|
+
Update or add a single cookie in the session.
|
313
|
+
|
314
|
+
:param key: The name of the cookie.
|
315
|
+
:param value: The value of the cookie.
|
316
|
+
"""
|
317
|
+
self._cookies[key] = value
|
318
|
+
if self._session:
|
319
|
+
self._session.cookie_jar.update_cookies({key: value})
|
320
|
+
|
321
|
+
def update_cookies(
|
322
|
+
self,
|
323
|
+
cookies: dict[str, str],
|
324
|
+
) -> None:
|
325
|
+
"""
|
326
|
+
Update or add multiple cookies in the session.
|
327
|
+
|
328
|
+
:param cookies: A dictionary of cookie key-value pairs.
|
329
|
+
"""
|
330
|
+
self._cookies.update(cookies)
|
331
|
+
if self._session:
|
332
|
+
self._session.cookie_jar.update_cookies(cookies)
|
275
333
|
|
276
|
-
|
334
|
+
def clear_cookies(self) -> None:
|
335
|
+
"""
|
336
|
+
Clear cookies from the session.
|
337
|
+
"""
|
338
|
+
self._cookies = {}
|
277
339
|
if self._session:
|
278
|
-
self._session.cookie_jar.
|
340
|
+
self._session.cookie_jar.clear()
|
341
|
+
|
342
|
+
async def _request(
|
343
|
+
self,
|
344
|
+
method: str,
|
345
|
+
url: str,
|
346
|
+
**kwargs: Any,
|
347
|
+
) -> ClientResponse:
|
348
|
+
if self._rate_limiter:
|
349
|
+
await self._rate_limiter.wait()
|
350
|
+
return await self.session.request(method, url, **kwargs)
|
279
351
|
|
280
|
-
async def
|
352
|
+
async def close(self) -> None:
|
281
353
|
"""
|
282
354
|
Shutdown and clean up the session. Closes connection pool.
|
283
355
|
"""
|
@@ -285,16 +357,48 @@ class BaseAsyncSession(AsyncRequesterProtocol, abc.ABC):
|
|
285
357
|
await self._session.close()
|
286
358
|
self._session = None
|
287
359
|
|
288
|
-
def
|
360
|
+
def sync_close(self) -> None:
|
361
|
+
"""
|
362
|
+
Sync wrapper for closing the aiohttp session
|
363
|
+
when called from sync contexts.
|
364
|
+
"""
|
365
|
+
if self._session:
|
366
|
+
try:
|
367
|
+
loop = asyncio.get_running_loop()
|
368
|
+
loop.create_task(self.close())
|
369
|
+
except RuntimeError:
|
370
|
+
loop = asyncio.new_event_loop()
|
371
|
+
asyncio.set_event_loop(loop)
|
372
|
+
loop.run_until_complete(self.close())
|
373
|
+
loop.close()
|
374
|
+
|
375
|
+
async def __aenter__(self) -> Self:
|
376
|
+
if self._session is None:
|
377
|
+
self._init_session()
|
378
|
+
return self
|
379
|
+
|
380
|
+
async def __aexit__(
|
381
|
+
self,
|
382
|
+
exc_type: type[BaseException] | None,
|
383
|
+
exc_val: BaseException | None,
|
384
|
+
tb: types.TracebackType | None,
|
385
|
+
) -> None:
|
386
|
+
await self.close()
|
387
|
+
|
388
|
+
def __del__(self) -> None:
|
389
|
+
self.sync_close()
|
390
|
+
|
391
|
+
def __getstate__(self) -> dict[str, Any]:
|
289
392
|
"""
|
290
393
|
Prepare object state for serialization: remove unpickleable session.
|
291
394
|
"""
|
395
|
+
self.sync_close()
|
292
396
|
state = self.__dict__.copy()
|
293
397
|
state.pop("_session", None)
|
294
398
|
state.pop("_rate_limiter", None)
|
295
399
|
return state
|
296
400
|
|
297
|
-
def __setstate__(self, state:
|
401
|
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
298
402
|
"""
|
299
403
|
Restore object state. Session will be lazily reinitialized on next request.
|
300
404
|
"""
|