python-httpfile 0.0.2__py3-none-any.whl → 0.0.3__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.
- httpfile/__init__.py +795 -53
- {python_httpfile-0.0.2.dist-info → python_httpfile-0.0.3.dist-info}/METADATA +5 -3
- python_httpfile-0.0.3.dist-info/RECORD +7 -0
- python_httpfile-0.0.2.dist-info/RECORD +0 -7
- {python_httpfile-0.0.2.dist-info → python_httpfile-0.0.3.dist-info}/LICENSE +0 -0
- {python_httpfile-0.0.2.dist-info → python_httpfile-0.0.3.dist-info}/WHEEL +0 -0
httpfile/__init__.py
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# encoding: utf-8
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
__author__ = "ChenyangGao <https://chenyanggao.github.io>"
|
|
5
|
-
__version__ = (0, 0,
|
|
7
|
+
__version__ = (0, 0, 3)
|
|
6
8
|
__all__ = ["HTTPFileReader", "AsyncHTTPFileReader"]
|
|
7
9
|
|
|
8
10
|
import errno
|
|
9
11
|
|
|
10
|
-
from collections.abc import
|
|
12
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
11
13
|
from functools import cached_property, partial
|
|
12
|
-
from
|
|
14
|
+
from inspect import isawaitable, signature
|
|
13
15
|
from io import (
|
|
14
16
|
BufferedReader, RawIOBase, TextIOWrapper, UnsupportedOperation, DEFAULT_BUFFER_SIZE,
|
|
15
17
|
)
|
|
16
18
|
from os import fstat, stat, PathLike
|
|
17
19
|
from shutil import COPY_BUFSIZE # type: ignore
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
+
from sys import exc_info
|
|
21
|
+
from typing import cast, overload, Any, BinaryIO, Literal, Self
|
|
22
|
+
from types import MappingProxyType, MethodType
|
|
20
23
|
from warnings import warn
|
|
21
24
|
|
|
25
|
+
from asynctools import ensure_async, run_async
|
|
26
|
+
from filewrap import AsyncBufferedReader, AsyncTextIOWrapper
|
|
22
27
|
from http_response import get_filename, get_length, get_range, get_total_length, is_chunked, is_range_request
|
|
23
28
|
from property import funcproperty
|
|
24
29
|
from urlopen import urlopen
|
|
25
30
|
|
|
26
31
|
|
|
27
|
-
def get_filesize(
|
|
32
|
+
def get_filesize(
|
|
33
|
+
file,
|
|
34
|
+
/,
|
|
35
|
+
dont_read: bool = True,
|
|
36
|
+
) -> int:
|
|
28
37
|
if isinstance(file, (bytes, str, PathLike)):
|
|
29
38
|
return stat(file).st_size
|
|
30
39
|
curpos = 0
|
|
@@ -88,12 +97,12 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
88
97
|
self,
|
|
89
98
|
/,
|
|
90
99
|
url: str | Callable[[], str],
|
|
91
|
-
headers:
|
|
100
|
+
headers: None | Mapping = None,
|
|
92
101
|
start: int = 0,
|
|
93
102
|
# NOTE: If the offset of the forward seek is not higher than this value,
|
|
94
103
|
# it will be directly read and discarded, default to 1 MB
|
|
95
104
|
seek_threshold: int = 1 << 20,
|
|
96
|
-
urlopen: Callable
|
|
105
|
+
urlopen: Callable = urlopen,
|
|
97
106
|
):
|
|
98
107
|
if headers:
|
|
99
108
|
headers = {**headers, "Accept-Encoding": "identity"}
|
|
@@ -146,7 +155,7 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
146
155
|
def __exit__(self, /, *exc_info):
|
|
147
156
|
self.close()
|
|
148
157
|
|
|
149
|
-
def __iter__(self, /):
|
|
158
|
+
def __iter__(self, /) -> Self:
|
|
150
159
|
return self
|
|
151
160
|
|
|
152
161
|
def __len__(self, /) -> int:
|
|
@@ -165,29 +174,41 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
165
174
|
name = cls.__qualname__
|
|
166
175
|
if module != "__main__":
|
|
167
176
|
name = module + "." + name
|
|
168
|
-
return f"{name}({self.url!r},
|
|
177
|
+
return f"{name}(url={self.url!r}, headers={self.headers!r}, start={self.tell()}, seek_threshold={self.seek_threshold}, urlopen={self.urlopen!r})"
|
|
169
178
|
|
|
170
179
|
def __setattr__(self, attr, val, /):
|
|
171
180
|
raise TypeError("can't set attribute")
|
|
172
181
|
|
|
173
182
|
def _add_start(self, delta: int, /):
|
|
174
|
-
self.
|
|
183
|
+
if not self.file_can_tell:
|
|
184
|
+
self.__dict__["start"] += delta
|
|
185
|
+
|
|
186
|
+
@cached_property
|
|
187
|
+
def file_can_tell(self, /) -> bool:
|
|
188
|
+
try:
|
|
189
|
+
self.file.tell()
|
|
190
|
+
return True
|
|
191
|
+
except (AttributeError, TypeError, OSError):
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
@funcproperty
|
|
195
|
+
def file_closed(self, /) -> bool:
|
|
196
|
+
return self.file.closed
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def closed(self, /) -> bool:
|
|
200
|
+
return self.__dict__["closed"]
|
|
175
201
|
|
|
176
202
|
def close(self, /):
|
|
177
203
|
if not self.closed:
|
|
178
204
|
self.response.close()
|
|
179
205
|
self.__dict__["closed"] = True
|
|
180
206
|
|
|
181
|
-
@funcproperty
|
|
182
|
-
def closed(self, /):
|
|
183
|
-
return self.__dict__["closed"]
|
|
184
|
-
|
|
185
207
|
@funcproperty
|
|
186
208
|
def file(self, /) -> BinaryIO:
|
|
187
209
|
return self.response
|
|
188
210
|
|
|
189
|
-
|
|
190
|
-
def fileno(self, /):
|
|
211
|
+
def fileno(self, /) -> int:
|
|
191
212
|
return self.file.fileno()
|
|
192
213
|
|
|
193
214
|
def flush(self, /):
|
|
@@ -209,7 +230,7 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
209
230
|
raise ValueError("I/O operation on closed file.")
|
|
210
231
|
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
211
232
|
return b""
|
|
212
|
-
if self.
|
|
233
|
+
if self.file_closed:
|
|
213
234
|
self.reconnect()
|
|
214
235
|
if size is None or size < 0:
|
|
215
236
|
data = self.file.read()
|
|
@@ -227,19 +248,19 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
227
248
|
raise ValueError("I/O operation on closed file.")
|
|
228
249
|
if not buffer or not self.chunked and self.tell() >= self.length:
|
|
229
250
|
return 0
|
|
230
|
-
if self.
|
|
251
|
+
if self.file_closed:
|
|
231
252
|
self.reconnect()
|
|
232
253
|
size = self.file.readinto(buffer)
|
|
233
254
|
if size:
|
|
234
255
|
self._add_start(size)
|
|
235
256
|
return size
|
|
236
257
|
|
|
237
|
-
def readline(self, size:
|
|
258
|
+
def readline(self, size: None | int = -1, /) -> bytes:
|
|
238
259
|
if self.closed:
|
|
239
260
|
raise ValueError("I/O operation on closed file.")
|
|
240
261
|
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
241
262
|
return b""
|
|
242
|
-
if self.
|
|
263
|
+
if self.file_closed:
|
|
243
264
|
self.reconnect()
|
|
244
265
|
if size is None or size < 0:
|
|
245
266
|
data = self.file.readline()
|
|
@@ -254,14 +275,14 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
254
275
|
raise ValueError("I/O operation on closed file.")
|
|
255
276
|
if not self.chunked and self.tell() >= self.length:
|
|
256
277
|
return []
|
|
257
|
-
if self.
|
|
278
|
+
if self.file_closed:
|
|
258
279
|
self.reconnect()
|
|
259
280
|
ls = self.file.readlines(hint)
|
|
260
281
|
if ls:
|
|
261
282
|
self._add_start(sum(map(len, ls)))
|
|
262
283
|
return ls
|
|
263
284
|
|
|
264
|
-
def reconnect(self, /, start:
|
|
285
|
+
def reconnect(self, /, start: None | int = None) -> int:
|
|
265
286
|
if not self._seekable:
|
|
266
287
|
if start is None and self.tell() or start:
|
|
267
288
|
raise OSError(errno.EOPNOTSUPP, "Unsupport for reconnection of non-seekable streams.")
|
|
@@ -328,9 +349,15 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
328
349
|
return self._seekable
|
|
329
350
|
|
|
330
351
|
def tell(self, /) -> int:
|
|
331
|
-
|
|
352
|
+
if self.file_can_tell:
|
|
353
|
+
start = self.start
|
|
354
|
+
if start >= self.length:
|
|
355
|
+
return start
|
|
356
|
+
return start + self.file.tell()
|
|
357
|
+
else:
|
|
358
|
+
return self.start
|
|
332
359
|
|
|
333
|
-
def truncate(self, size:
|
|
360
|
+
def truncate(self, size: None | int = None, /):
|
|
334
361
|
raise UnsupportedOperation(errno.ENOTSUP, "truncate")
|
|
335
362
|
|
|
336
363
|
def writable(self, /) -> bool:
|
|
@@ -342,15 +369,109 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
342
369
|
def writelines(self, lines, /):
|
|
343
370
|
raise UnsupportedOperation(errno.ENOTSUP, "writelines")
|
|
344
371
|
|
|
372
|
+
@overload
|
|
373
|
+
@classmethod
|
|
374
|
+
def open(
|
|
375
|
+
cls,
|
|
376
|
+
/,
|
|
377
|
+
url: str | Callable[[], str],
|
|
378
|
+
mode: Literal["br", "rb"],
|
|
379
|
+
*,
|
|
380
|
+
buffering: None | int = None,
|
|
381
|
+
encoding: None | str = None,
|
|
382
|
+
errors: None | str = None,
|
|
383
|
+
newline: None | str = None,
|
|
384
|
+
headers: None | Mapping = None,
|
|
385
|
+
start: int = 0,
|
|
386
|
+
seek_threshold: int = 1 << 20,
|
|
387
|
+
urlopen: Callable = urlopen,
|
|
388
|
+
) -> Self | BufferedReader:
|
|
389
|
+
...
|
|
390
|
+
@overload
|
|
391
|
+
@classmethod
|
|
392
|
+
def open(
|
|
393
|
+
cls,
|
|
394
|
+
/,
|
|
395
|
+
url: str | Callable[[], str],
|
|
396
|
+
mode: Literal["r", "rt", "tr"] = "r",
|
|
397
|
+
*,
|
|
398
|
+
buffering: None | int = None,
|
|
399
|
+
encoding: None | str = None,
|
|
400
|
+
errors: None | str = None,
|
|
401
|
+
newline: None | str = None,
|
|
402
|
+
headers: None | Mapping = None,
|
|
403
|
+
start: int = 0,
|
|
404
|
+
seek_threshold: int = 1 << 20,
|
|
405
|
+
urlopen: Callable = urlopen,
|
|
406
|
+
) -> TextIOWrapper:
|
|
407
|
+
...
|
|
408
|
+
@classmethod
|
|
409
|
+
def open(
|
|
410
|
+
cls,
|
|
411
|
+
/,
|
|
412
|
+
url: str | Callable[[], str],
|
|
413
|
+
mode: Literal["r", "rt", "tr", "br", "rb"] = "r",
|
|
414
|
+
*,
|
|
415
|
+
buffering: None | int = None,
|
|
416
|
+
encoding: None | str = None,
|
|
417
|
+
errors: None | str = None,
|
|
418
|
+
newline: None | str = None,
|
|
419
|
+
headers: None | Mapping = None,
|
|
420
|
+
start: int = 0,
|
|
421
|
+
seek_threshold: int = 1 << 20,
|
|
422
|
+
urlopen: Callable = urlopen,
|
|
423
|
+
) -> Self | BufferedReader | TextIOWrapper:
|
|
424
|
+
file = cls(
|
|
425
|
+
url=url,
|
|
426
|
+
headers=headers,
|
|
427
|
+
start=start,
|
|
428
|
+
seek_threshold=seek_threshold,
|
|
429
|
+
urlopen=urlopen,
|
|
430
|
+
)
|
|
431
|
+
if mode not in ("r", "rt", "tr", "rb", "br"):
|
|
432
|
+
raise OSError(errno.EINVAL, f"invalid (or unsupported) mode: {mode!r}")
|
|
433
|
+
return file.wrap(
|
|
434
|
+
text_mode="b" not in mode, # type: ignore
|
|
435
|
+
buffering=buffering,
|
|
436
|
+
encoding=encoding,
|
|
437
|
+
errors=errors,
|
|
438
|
+
newline=newline,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
@overload
|
|
345
442
|
def wrap(
|
|
346
443
|
self,
|
|
347
444
|
/,
|
|
348
|
-
text_mode:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
445
|
+
text_mode: Literal[False] = False,
|
|
446
|
+
*,
|
|
447
|
+
buffering: None | int = None,
|
|
448
|
+
encoding: None | str = None,
|
|
449
|
+
errors: None | str = None,
|
|
450
|
+
newline: None | str = None,
|
|
451
|
+
) -> Self | BufferedReader:
|
|
452
|
+
...
|
|
453
|
+
@overload
|
|
454
|
+
def wrap(
|
|
455
|
+
self,
|
|
456
|
+
/,
|
|
457
|
+
text_mode: Literal[True],
|
|
458
|
+
*,
|
|
459
|
+
buffering: None | int = None,
|
|
460
|
+
encoding: None | str = None,
|
|
461
|
+
errors: None | str = None,
|
|
462
|
+
newline: None | str = None,
|
|
463
|
+
) -> TextIOWrapper:
|
|
464
|
+
...
|
|
465
|
+
def wrap(
|
|
466
|
+
self,
|
|
467
|
+
/,
|
|
468
|
+
text_mode: Literal[False, True] = False,
|
|
469
|
+
*,
|
|
470
|
+
buffering: None | int = None,
|
|
471
|
+
encoding: None | str = None,
|
|
472
|
+
errors: None | str = None,
|
|
473
|
+
newline: None | str = None,
|
|
474
|
+
) -> Self | BufferedReader | TextIOWrapper:
|
|
354
475
|
if buffering is None:
|
|
355
476
|
if text_mode:
|
|
356
477
|
buffering = DEFAULT_BUFFER_SIZE
|
|
@@ -386,8 +507,437 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
386
507
|
return buffer
|
|
387
508
|
|
|
388
509
|
|
|
510
|
+
class AsyncHTTPFileReader(HTTPFileReader):
|
|
511
|
+
url: str | Callable[[], Awaitable[str]] # type: ignore
|
|
512
|
+
|
|
513
|
+
def __init__(
|
|
514
|
+
self,
|
|
515
|
+
/,
|
|
516
|
+
url: str | Callable[[], str] | Callable[[], Awaitable[str]],
|
|
517
|
+
headers: None | Mapping = None,
|
|
518
|
+
start: int = 0,
|
|
519
|
+
seek_threshold: int = 1 << 20,
|
|
520
|
+
urlopen: Callable = urlopen,
|
|
521
|
+
):
|
|
522
|
+
run_async(self._init(
|
|
523
|
+
url=url,
|
|
524
|
+
headers=headers,
|
|
525
|
+
start=start,
|
|
526
|
+
seek_threshold=seek_threshold,
|
|
527
|
+
urlopen=urlopen,
|
|
528
|
+
))
|
|
529
|
+
|
|
530
|
+
async def __aenter__(self, /) -> Self:
|
|
531
|
+
return self
|
|
532
|
+
|
|
533
|
+
async def __aexit__(self, /, *exc_info):
|
|
534
|
+
await self.aclose()
|
|
535
|
+
|
|
536
|
+
def __aiter__(self, /) -> Self:
|
|
537
|
+
return self
|
|
538
|
+
|
|
539
|
+
async def __anext__(self, /) -> bytes:
|
|
540
|
+
if line := await self.readline():
|
|
541
|
+
return line
|
|
542
|
+
else:
|
|
543
|
+
raise StopAsyncIteration
|
|
544
|
+
|
|
545
|
+
async def _init(
|
|
546
|
+
self,
|
|
547
|
+
/,
|
|
548
|
+
url: str | Callable[[], str] | Callable[[], Awaitable[str]],
|
|
549
|
+
headers: None | Mapping = None,
|
|
550
|
+
start: int = 0,
|
|
551
|
+
seek_threshold: int = 1 << 20,
|
|
552
|
+
urlopen = None,
|
|
553
|
+
):
|
|
554
|
+
if urlopen is None:
|
|
555
|
+
urlopen = signature(type(self).__init__).parameters["urlopen"].default
|
|
556
|
+
if headers:
|
|
557
|
+
headers = {**headers, "Accept-Encoding": "identity"}
|
|
558
|
+
else:
|
|
559
|
+
headers = {"Accept-Encoding": "identity"}
|
|
560
|
+
if start > 0:
|
|
561
|
+
headers["Range"] = f"bytes={start}-"
|
|
562
|
+
elif start < 0:
|
|
563
|
+
headers["Range"] = f"bytes={start}"
|
|
564
|
+
if callable(url):
|
|
565
|
+
geturl = url
|
|
566
|
+
async def url() -> str:
|
|
567
|
+
url = geturl()
|
|
568
|
+
if isawaitable(url):
|
|
569
|
+
url = await url
|
|
570
|
+
headers_extra = getattr(url, "headers")
|
|
571
|
+
if headers_extra:
|
|
572
|
+
headers.update(headers_extra)
|
|
573
|
+
return cast(str, url)
|
|
574
|
+
elif hasattr(url, "headers"):
|
|
575
|
+
headers_extra = getattr(url, "headers")
|
|
576
|
+
if headers_extra:
|
|
577
|
+
headers.update(headers_extra)
|
|
578
|
+
urlopen = ensure_async(urlopen, threaded=True)
|
|
579
|
+
response = await urlopen((await url()) if callable(url) else url, headers=headers)
|
|
580
|
+
if start:
|
|
581
|
+
rng = get_range(response)
|
|
582
|
+
if not rng:
|
|
583
|
+
raise OSError(errno.ESPIPE, "non-seekable")
|
|
584
|
+
start = rng[0]
|
|
585
|
+
self.__dict__.update(
|
|
586
|
+
url = url,
|
|
587
|
+
response = response,
|
|
588
|
+
length = get_total_length(response) or 0,
|
|
589
|
+
chunked = is_chunked(response),
|
|
590
|
+
start = start,
|
|
591
|
+
closed = False,
|
|
592
|
+
urlopen = urlopen,
|
|
593
|
+
headers = MappingProxyType(headers),
|
|
594
|
+
seek_threshold = max(seek_threshold, 0),
|
|
595
|
+
_seekable = is_range_request(response),
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
async def aclose(self, /):
|
|
599
|
+
if not self.closed:
|
|
600
|
+
self.__dict__["closed"] = True
|
|
601
|
+
await self.close_response()
|
|
602
|
+
|
|
603
|
+
def close(self, /):
|
|
604
|
+
if not self.closed:
|
|
605
|
+
self.__dict__["closed"] = True
|
|
606
|
+
try:
|
|
607
|
+
ret = self.response.aclose()
|
|
608
|
+
except (AttributeError, TypeError):
|
|
609
|
+
ret = self.response.close()
|
|
610
|
+
if isawaitable(ret):
|
|
611
|
+
run_async(ret)
|
|
612
|
+
|
|
613
|
+
async def close_response(self, /):
|
|
614
|
+
try:
|
|
615
|
+
ret = self.response.aclose()
|
|
616
|
+
except (AttributeError, TypeError):
|
|
617
|
+
ret = self.response.close()
|
|
618
|
+
if isawaitable(ret):
|
|
619
|
+
await ret
|
|
620
|
+
|
|
621
|
+
async def flush(self, /):
|
|
622
|
+
return await ensure_async(self.file.flush, threaded=True)()
|
|
623
|
+
|
|
624
|
+
async def read(self, size: int = -1, /) -> bytes: # type: ignore
|
|
625
|
+
if self.closed:
|
|
626
|
+
raise ValueError("I/O operation on closed file.")
|
|
627
|
+
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
628
|
+
return b""
|
|
629
|
+
if self.file_closed:
|
|
630
|
+
await self.reconnect()
|
|
631
|
+
if size is None or size < 0:
|
|
632
|
+
data = await ensure_async(self.file.read, threaded=True)()
|
|
633
|
+
else:
|
|
634
|
+
data = await ensure_async(self.file.read, threaded=True)(size)
|
|
635
|
+
if data:
|
|
636
|
+
self._add_start(len(data))
|
|
637
|
+
return data
|
|
638
|
+
|
|
639
|
+
async def readinto(self, buffer, /) -> int: # type: ignore
|
|
640
|
+
if self.closed:
|
|
641
|
+
raise ValueError("I/O operation on closed file.")
|
|
642
|
+
if not buffer or not self.chunked and self.tell() >= self.length:
|
|
643
|
+
return 0
|
|
644
|
+
if self.file_closed:
|
|
645
|
+
await self.reconnect()
|
|
646
|
+
size = await ensure_async(self.file.readinto, threaded=True)(buffer)
|
|
647
|
+
if size:
|
|
648
|
+
self._add_start(size)
|
|
649
|
+
return size
|
|
650
|
+
|
|
651
|
+
async def readline(self, size: None | int = -1, /) -> bytes: # type: ignore
|
|
652
|
+
if self.closed:
|
|
653
|
+
raise ValueError("I/O operation on closed file.")
|
|
654
|
+
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
655
|
+
return b""
|
|
656
|
+
if self.file_closed:
|
|
657
|
+
await self.reconnect()
|
|
658
|
+
if size is None or size < 0:
|
|
659
|
+
data = await ensure_async(self.file.readline, threaded=True)()
|
|
660
|
+
else:
|
|
661
|
+
data = await ensure_async(self.file.readline, threaded=True)(size)
|
|
662
|
+
if data:
|
|
663
|
+
self._add_start(len(data))
|
|
664
|
+
return data
|
|
665
|
+
|
|
666
|
+
async def readlines(self, hint: int = -1, /) -> list[bytes]: # type: ignore
|
|
667
|
+
if self.closed:
|
|
668
|
+
raise ValueError("I/O operation on closed file.")
|
|
669
|
+
if not self.chunked and self.tell() >= self.length:
|
|
670
|
+
return []
|
|
671
|
+
if self.file_closed:
|
|
672
|
+
await self.reconnect()
|
|
673
|
+
ls = await ensure_async(self.file.readlines, threaded=True)(hint)
|
|
674
|
+
if ls:
|
|
675
|
+
self._add_start(sum(map(len, ls)))
|
|
676
|
+
return ls
|
|
677
|
+
|
|
678
|
+
async def reconnect(self, /, start: None | int = None) -> int: # type: ignore
|
|
679
|
+
if not self._seekable:
|
|
680
|
+
if start is None and self.tell() or start:
|
|
681
|
+
raise OSError(errno.EOPNOTSUPP, "Unsupport for reconnection of non-seekable streams.")
|
|
682
|
+
start = 0
|
|
683
|
+
if start is None:
|
|
684
|
+
start = self.tell()
|
|
685
|
+
elif start < 0:
|
|
686
|
+
start = self.length + start
|
|
687
|
+
if start < 0:
|
|
688
|
+
start = 0
|
|
689
|
+
if start >= self.length:
|
|
690
|
+
self.__dict__.update(start=start)
|
|
691
|
+
return start
|
|
692
|
+
await self.close_response()
|
|
693
|
+
url = self.url
|
|
694
|
+
response = await self.urlopen(
|
|
695
|
+
(await url()) if callable(url) else url,
|
|
696
|
+
headers={**self.headers, "Range": f"bytes={start}-"}
|
|
697
|
+
)
|
|
698
|
+
length_new = get_total_length(response)
|
|
699
|
+
if self.length != length_new:
|
|
700
|
+
raise OSError(errno.EIO, f"file size changed: {self.length} -> {length_new}")
|
|
701
|
+
self.__dict__.update(
|
|
702
|
+
response=response,
|
|
703
|
+
start=start,
|
|
704
|
+
closed=False,
|
|
705
|
+
)
|
|
706
|
+
return start
|
|
707
|
+
|
|
708
|
+
async def seek(self, pos: int, whence: int = 0, /) -> int: # type: ignore
|
|
709
|
+
if self.closed:
|
|
710
|
+
raise ValueError("I/O operation on closed file.")
|
|
711
|
+
if not self._seekable:
|
|
712
|
+
raise OSError(errno.EINVAL, "not a seekable stream")
|
|
713
|
+
if whence == 0:
|
|
714
|
+
if pos < 0:
|
|
715
|
+
raise OSError(errno.EINVAL, f"negative seek start: {pos!r}")
|
|
716
|
+
old_pos = self.tell()
|
|
717
|
+
if old_pos == pos:
|
|
718
|
+
return pos
|
|
719
|
+
if pos > old_pos and (size := pos - old_pos) <= self.seek_threshold:
|
|
720
|
+
if size <= COPY_BUFSIZE:
|
|
721
|
+
await self.read(size)
|
|
722
|
+
else:
|
|
723
|
+
buf = bytearray(COPY_BUFSIZE)
|
|
724
|
+
readinto = self.readinto
|
|
725
|
+
while size > COPY_BUFSIZE:
|
|
726
|
+
await readinto(buf)
|
|
727
|
+
size -= COPY_BUFSIZE
|
|
728
|
+
await self.read(size)
|
|
729
|
+
else:
|
|
730
|
+
await self.reconnect(pos)
|
|
731
|
+
return pos
|
|
732
|
+
elif whence == 1:
|
|
733
|
+
if pos == 0:
|
|
734
|
+
return self.tell()
|
|
735
|
+
return await self.seek(self.tell() + pos)
|
|
736
|
+
elif whence == 2:
|
|
737
|
+
return await self.seek(self.length + pos)
|
|
738
|
+
else:
|
|
739
|
+
raise OSError(errno.EINVAL, f"whence value unsupported: {whence!r}")
|
|
740
|
+
|
|
741
|
+
@classmethod
|
|
742
|
+
async def new(
|
|
743
|
+
cls,
|
|
744
|
+
/,
|
|
745
|
+
url: str | Callable[[], str],
|
|
746
|
+
headers: None | Mapping = None,
|
|
747
|
+
start: int = 0,
|
|
748
|
+
seek_threshold: int = 1 << 20,
|
|
749
|
+
urlopen = None,
|
|
750
|
+
) -> Self: # type: ignore
|
|
751
|
+
self = cls.__new__(cls)
|
|
752
|
+
await self._init(
|
|
753
|
+
url=url,
|
|
754
|
+
headers=headers,
|
|
755
|
+
start=start,
|
|
756
|
+
seek_threshold=seek_threshold,
|
|
757
|
+
urlopen=urlopen,
|
|
758
|
+
)
|
|
759
|
+
return self
|
|
760
|
+
|
|
761
|
+
@overload # type: ignore
|
|
762
|
+
@classmethod
|
|
763
|
+
async def open(
|
|
764
|
+
cls,
|
|
765
|
+
/,
|
|
766
|
+
url: str | Callable[[], str],
|
|
767
|
+
mode: Literal["br", "rb"],
|
|
768
|
+
*,
|
|
769
|
+
buffering: None | int = None,
|
|
770
|
+
encoding: None | str = None,
|
|
771
|
+
errors: None | str = None,
|
|
772
|
+
newline: None | str = None,
|
|
773
|
+
headers: None | Mapping = None,
|
|
774
|
+
start: int = 0,
|
|
775
|
+
seek_threshold: int = 1 << 20,
|
|
776
|
+
urlopen: Callable = urlopen,
|
|
777
|
+
) -> Self | AsyncBufferedReader:
|
|
778
|
+
...
|
|
779
|
+
@overload
|
|
780
|
+
@classmethod
|
|
781
|
+
async def open(
|
|
782
|
+
cls,
|
|
783
|
+
/,
|
|
784
|
+
url: str | Callable[[], str],
|
|
785
|
+
mode: Literal["r", "rt", "tr"] = "r",
|
|
786
|
+
*,
|
|
787
|
+
buffering: None | int = None,
|
|
788
|
+
encoding: None | str = None,
|
|
789
|
+
errors: None | str = None,
|
|
790
|
+
newline: None | str = None,
|
|
791
|
+
headers: None | Mapping = None,
|
|
792
|
+
start: int = 0,
|
|
793
|
+
seek_threshold: int = 1 << 20,
|
|
794
|
+
urlopen: Callable = urlopen,
|
|
795
|
+
) -> AsyncTextIOWrapper:
|
|
796
|
+
...
|
|
797
|
+
@classmethod
|
|
798
|
+
async def open(
|
|
799
|
+
cls,
|
|
800
|
+
/,
|
|
801
|
+
url: str | Callable[[], str],
|
|
802
|
+
mode: Literal["r", "rt", "tr", "br", "rb"] = "r",
|
|
803
|
+
buffering: None | int = None,
|
|
804
|
+
encoding: None | str = None,
|
|
805
|
+
errors: None | str = None,
|
|
806
|
+
newline: None | str = None,
|
|
807
|
+
headers: None | Mapping = None,
|
|
808
|
+
start: int = 0,
|
|
809
|
+
seek_threshold: int = 1 << 20,
|
|
810
|
+
urlopen: Callable = urlopen,
|
|
811
|
+
) -> Self | AsyncBufferedReader | AsyncTextIOWrapper:
|
|
812
|
+
file = await cls.new(
|
|
813
|
+
url=url,
|
|
814
|
+
headers=headers,
|
|
815
|
+
start=start,
|
|
816
|
+
seek_threshold=seek_threshold,
|
|
817
|
+
urlopen=urlopen,
|
|
818
|
+
)
|
|
819
|
+
if mode not in ("r", "rt", "tr", "rb", "br"):
|
|
820
|
+
raise OSError(errno.EINVAL, f"invalid (or unsupported) mode: {mode!r}")
|
|
821
|
+
return file.wrap(
|
|
822
|
+
text_mode="b" not in mode, # type: ignore
|
|
823
|
+
buffering=buffering,
|
|
824
|
+
encoding=encoding,
|
|
825
|
+
errors=errors,
|
|
826
|
+
newline=newline,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
@overload
|
|
830
|
+
def wrap(
|
|
831
|
+
self,
|
|
832
|
+
/,
|
|
833
|
+
text_mode: Literal[False] = False,
|
|
834
|
+
*,
|
|
835
|
+
buffering: None | int = None,
|
|
836
|
+
encoding: None | str = None,
|
|
837
|
+
errors: None | str = None,
|
|
838
|
+
newline: None | str = None,
|
|
839
|
+
) -> Self | AsyncBufferedReader:
|
|
840
|
+
...
|
|
841
|
+
@overload
|
|
842
|
+
def wrap(
|
|
843
|
+
self,
|
|
844
|
+
/,
|
|
845
|
+
text_mode: Literal[True],
|
|
846
|
+
*,
|
|
847
|
+
buffering: None | int = None,
|
|
848
|
+
encoding: None | str = None,
|
|
849
|
+
errors: None | str = None,
|
|
850
|
+
newline: None | str = None,
|
|
851
|
+
) -> AsyncTextIOWrapper:
|
|
852
|
+
...
|
|
853
|
+
def wrap(
|
|
854
|
+
self,
|
|
855
|
+
/,
|
|
856
|
+
text_mode: bool = False,
|
|
857
|
+
*,
|
|
858
|
+
buffering: None | int = None,
|
|
859
|
+
encoding: None | str = None,
|
|
860
|
+
errors: None | str = None,
|
|
861
|
+
newline: None | str = None,
|
|
862
|
+
) -> Self | AsyncBufferedReader | AsyncTextIOWrapper:
|
|
863
|
+
if buffering is None:
|
|
864
|
+
if text_mode:
|
|
865
|
+
buffering = DEFAULT_BUFFER_SIZE
|
|
866
|
+
else:
|
|
867
|
+
buffering = 0
|
|
868
|
+
if buffering == 0:
|
|
869
|
+
if text_mode:
|
|
870
|
+
raise OSError(errno.EINVAL, "can't have unbuffered text I/O")
|
|
871
|
+
return self
|
|
872
|
+
line_buffering = False
|
|
873
|
+
buffer_size: int
|
|
874
|
+
if buffering < 0:
|
|
875
|
+
buffer_size = DEFAULT_BUFFER_SIZE
|
|
876
|
+
elif buffering == 1:
|
|
877
|
+
if not text_mode:
|
|
878
|
+
warn("line buffering (buffering=1) isn't supported in binary mode, "
|
|
879
|
+
"the default buffer size will be used", RuntimeWarning)
|
|
880
|
+
buffer_size = DEFAULT_BUFFER_SIZE
|
|
881
|
+
line_buffering = True
|
|
882
|
+
else:
|
|
883
|
+
buffer_size = buffering
|
|
884
|
+
raw = self
|
|
885
|
+
buffer = AsyncBufferedReader(raw, buffer_size)
|
|
886
|
+
if text_mode:
|
|
887
|
+
return AsyncTextIOWrapper(
|
|
888
|
+
buffer,
|
|
889
|
+
encoding=encoding,
|
|
890
|
+
errors=errors,
|
|
891
|
+
newline=newline,
|
|
892
|
+
line_buffering=line_buffering,
|
|
893
|
+
)
|
|
894
|
+
else:
|
|
895
|
+
return buffer
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
from urllib.error import HTTPError
|
|
900
|
+
from urllib3 import request as urllib3_request
|
|
901
|
+
from urllib3.poolmanager import PoolManager
|
|
902
|
+
|
|
903
|
+
class Urllib3FileReader(HTTPFileReader):
|
|
904
|
+
|
|
905
|
+
def __init__(
|
|
906
|
+
self,
|
|
907
|
+
/,
|
|
908
|
+
url: str | Callable[[], str],
|
|
909
|
+
headers: None | Mapping = None,
|
|
910
|
+
start: int = 0,
|
|
911
|
+
seek_threshold: int = 1 << 20,
|
|
912
|
+
urlopen = partial(urllib3_request, "GET"), # type: ignore
|
|
913
|
+
):
|
|
914
|
+
if isinstance(urlopen, PoolManager):
|
|
915
|
+
urlopen = partial(urlopen.request, "GET", preload_content=False)
|
|
916
|
+
else:
|
|
917
|
+
urlopen = partial(urlopen, preload_content=False)
|
|
918
|
+
def urlopen_wrapper(url: str, headers: None | Mapping = headers):
|
|
919
|
+
resp = urlopen(url, headers=headers)
|
|
920
|
+
if resp.status >= 400:
|
|
921
|
+
raise HTTPError(resp.url, resp.status, resp.reason, resp.headers, resp)
|
|
922
|
+
return resp
|
|
923
|
+
super().__init__(
|
|
924
|
+
url,
|
|
925
|
+
headers=headers,
|
|
926
|
+
start=start,
|
|
927
|
+
seek_threshold=seek_threshold,
|
|
928
|
+
urlopen=urlopen_wrapper,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
__all__.append("Urllib3FileReader")
|
|
932
|
+
except ImportError:
|
|
933
|
+
pass
|
|
934
|
+
|
|
935
|
+
|
|
389
936
|
try:
|
|
390
|
-
from requests import Session
|
|
937
|
+
from requests import request as requests_request, Session
|
|
938
|
+
|
|
939
|
+
if "__del__" not in Session.__dict__:
|
|
940
|
+
setattr(Session, "__del__", lambda self, /: self.close())
|
|
391
941
|
|
|
392
942
|
class RequestsFileReader(HTTPFileReader):
|
|
393
943
|
|
|
@@ -395,13 +945,17 @@ try:
|
|
|
395
945
|
self,
|
|
396
946
|
/,
|
|
397
947
|
url: str | Callable[[], str],
|
|
398
|
-
headers:
|
|
948
|
+
headers: None | Mapping = None,
|
|
399
949
|
start: int = 0,
|
|
400
950
|
seek_threshold: int = 1 << 20,
|
|
401
|
-
urlopen
|
|
951
|
+
urlopen = partial(requests_request, "GET"),
|
|
402
952
|
):
|
|
403
|
-
|
|
404
|
-
|
|
953
|
+
if isinstance(urlopen, Session):
|
|
954
|
+
urlopen = partial(urlopen.request, "GET", stream=True)
|
|
955
|
+
else:
|
|
956
|
+
urlopen = partial(urlopen, stream=True)
|
|
957
|
+
def urlopen_wrapper(url: str, headers: None | Mapping = headers):
|
|
958
|
+
resp = urlopen(url, headers=headers)
|
|
405
959
|
resp.raise_for_status()
|
|
406
960
|
return resp
|
|
407
961
|
super().__init__(
|
|
@@ -412,41 +966,229 @@ try:
|
|
|
412
966
|
urlopen=urlopen_wrapper,
|
|
413
967
|
)
|
|
414
968
|
|
|
415
|
-
def _add_start(self, delta: int, /):
|
|
416
|
-
pass
|
|
417
|
-
|
|
418
969
|
@funcproperty
|
|
419
970
|
def file(self, /) -> BinaryIO:
|
|
420
971
|
return self.response.raw
|
|
421
972
|
|
|
422
|
-
def tell(self, /) -> int:
|
|
423
|
-
start = self.start
|
|
424
|
-
if start >= self.length:
|
|
425
|
-
return start
|
|
426
|
-
return start + self.file.tell()
|
|
427
|
-
|
|
428
973
|
__all__.append("RequestsFileReader")
|
|
429
974
|
except ImportError:
|
|
430
975
|
pass
|
|
431
976
|
|
|
432
977
|
|
|
433
978
|
try:
|
|
434
|
-
from aiohttp import ClientSession
|
|
979
|
+
from aiohttp import request as aiohttp_request, ClientSession
|
|
980
|
+
|
|
981
|
+
class AiohttpFileReader(AsyncHTTPFileReader):
|
|
982
|
+
|
|
983
|
+
def __init__(
|
|
984
|
+
self,
|
|
985
|
+
/,
|
|
986
|
+
url: str | Callable[[], str] | Callable[[], Awaitable[str]],
|
|
987
|
+
headers: None | Mapping = None,
|
|
988
|
+
start: int = 0,
|
|
989
|
+
seek_threshold: int = 1 << 20,
|
|
990
|
+
urlopen = partial(aiohttp_request, "GET"),
|
|
991
|
+
):
|
|
992
|
+
super().__init__(
|
|
993
|
+
url=url,
|
|
994
|
+
headers=headers,
|
|
995
|
+
start=start,
|
|
996
|
+
seek_threshold=seek_threshold,
|
|
997
|
+
urlopen=urlopen,
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
async def _init(
|
|
1001
|
+
self,
|
|
1002
|
+
/,
|
|
1003
|
+
url: str | Callable[[], str] | Callable[[], Awaitable[str]],
|
|
1004
|
+
headers: None | Mapping = None,
|
|
1005
|
+
start: int = 0,
|
|
1006
|
+
seek_threshold: int = 1 << 20,
|
|
1007
|
+
urlopen = None,
|
|
1008
|
+
):
|
|
1009
|
+
if urlopen is None:
|
|
1010
|
+
urlopen = signature(type(self).__init__).parameters["urlopen"].default
|
|
1011
|
+
close_session = True
|
|
1012
|
+
if isinstance(urlopen, ClientSession):
|
|
1013
|
+
urlopen = urlopen.get
|
|
1014
|
+
do_not_close_session = False
|
|
1015
|
+
if close_session:
|
|
1016
|
+
func = urlopen
|
|
1017
|
+
if isinstance(func, partial):
|
|
1018
|
+
func = func.func
|
|
1019
|
+
close_session = not (isinstance(func, MethodType) and isinstance(func.__self__, ClientSession))
|
|
1020
|
+
async def urlopen_wrapper(url: str, headers: None | Mapping = headers):
|
|
1021
|
+
resp = await urlopen(url, headers=headers).__aenter__()
|
|
1022
|
+
async def aclose():
|
|
1023
|
+
if close_session:
|
|
1024
|
+
try:
|
|
1025
|
+
await resp._session.close()
|
|
1026
|
+
except AttributeError:
|
|
1027
|
+
pass
|
|
1028
|
+
resp.close()
|
|
1029
|
+
resp.aclose = aclose
|
|
1030
|
+
resp.raise_for_status()
|
|
1031
|
+
return resp
|
|
1032
|
+
await super()._init(
|
|
1033
|
+
url=url,
|
|
1034
|
+
headers=headers,
|
|
1035
|
+
start=start,
|
|
1036
|
+
seek_threshold=seek_threshold,
|
|
1037
|
+
urlopen=urlopen_wrapper,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
@funcproperty
|
|
1041
|
+
def file(self, /) -> BinaryIO:
|
|
1042
|
+
return self.response.content
|
|
1043
|
+
|
|
1044
|
+
@funcproperty
|
|
1045
|
+
def file_closed(self, /) -> bool:
|
|
1046
|
+
return self.response.closed
|
|
435
1047
|
|
|
436
|
-
|
|
1048
|
+
__all__.append("AiohttpFileReader")
|
|
437
1049
|
except ImportError:
|
|
438
1050
|
pass
|
|
439
1051
|
|
|
440
1052
|
|
|
441
1053
|
try:
|
|
442
|
-
from
|
|
1054
|
+
from contextlib import asynccontextmanager
|
|
1055
|
+
from filewrap import bytes_iter_to_reader, bytes_iter_to_async_reader
|
|
1056
|
+
from httpx import stream, Client, AsyncClient
|
|
1057
|
+
|
|
1058
|
+
if "__del__" not in Client.__dict__:
|
|
1059
|
+
setattr(Client, "__del__", lambda self: self.close())
|
|
1060
|
+
if "__del__" not in AsyncClient.__dict__:
|
|
1061
|
+
setattr(AsyncClient, "__del__", lambda self: run_async(self.aclose()))
|
|
1062
|
+
|
|
1063
|
+
def async_stream(
|
|
1064
|
+
method,
|
|
1065
|
+
url,
|
|
1066
|
+
params = None,
|
|
1067
|
+
content = None,
|
|
1068
|
+
data = None,
|
|
1069
|
+
files = None,
|
|
1070
|
+
json = None,
|
|
1071
|
+
headers = None,
|
|
1072
|
+
cookies = None,
|
|
1073
|
+
auth = None,
|
|
1074
|
+
proxy = None,
|
|
1075
|
+
proxies = None,
|
|
1076
|
+
timeout = 5.0,
|
|
1077
|
+
follow_redirects = False,
|
|
1078
|
+
verify = True,
|
|
1079
|
+
cert = None,
|
|
1080
|
+
trust_env = True,
|
|
1081
|
+
):
|
|
1082
|
+
client = AsyncClient(
|
|
1083
|
+
cookies=cookies,
|
|
1084
|
+
proxy=proxy,
|
|
1085
|
+
proxies=proxies,
|
|
1086
|
+
cert=cert,
|
|
1087
|
+
verify=verify,
|
|
1088
|
+
timeout=timeout,
|
|
1089
|
+
trust_env=trust_env,
|
|
1090
|
+
)
|
|
1091
|
+
return client.stream(
|
|
1092
|
+
method=method,
|
|
1093
|
+
url=url,
|
|
1094
|
+
content=content,
|
|
1095
|
+
data=data,
|
|
1096
|
+
files=files,
|
|
1097
|
+
json=json,
|
|
1098
|
+
params=params,
|
|
1099
|
+
headers=headers,
|
|
1100
|
+
auth=auth,
|
|
1101
|
+
follow_redirects=follow_redirects,
|
|
1102
|
+
)
|
|
443
1103
|
|
|
444
|
-
|
|
1104
|
+
class HttpxFileReader(HTTPFileReader):
|
|
1105
|
+
|
|
1106
|
+
def __init__(
|
|
1107
|
+
self,
|
|
1108
|
+
/,
|
|
1109
|
+
url: str | Callable[[], str],
|
|
1110
|
+
headers: None | Mapping = None,
|
|
1111
|
+
start: int = 0,
|
|
1112
|
+
seek_threshold: int = 1 << 20,
|
|
1113
|
+
urlopen = partial(stream, "GET"),
|
|
1114
|
+
):
|
|
1115
|
+
if isinstance(urlopen, Client):
|
|
1116
|
+
urlopen = partial(urlopen.stream, "GET")
|
|
1117
|
+
def urlopen_wrapper(url: str, headers: None | Mapping = headers):
|
|
1118
|
+
context = urlopen(url, headers=headers)
|
|
1119
|
+
resp = context.__enter__()
|
|
1120
|
+
resp.raise_for_status()
|
|
1121
|
+
resp.context = context
|
|
1122
|
+
file = bytes_iter_to_reader(resp.iter_raw())
|
|
1123
|
+
self.__dict__["file"] = file
|
|
1124
|
+
return resp
|
|
1125
|
+
super().__init__(
|
|
1126
|
+
url,
|
|
1127
|
+
headers=headers,
|
|
1128
|
+
start=start,
|
|
1129
|
+
seek_threshold=seek_threshold,
|
|
1130
|
+
urlopen=urlopen_wrapper,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
@funcproperty
|
|
1134
|
+
def file_closed(self, /) -> bool:
|
|
1135
|
+
return self.response.is_closed
|
|
1136
|
+
|
|
1137
|
+
class AsyncHttpxFileReader(AsyncHTTPFileReader):
|
|
1138
|
+
|
|
1139
|
+
def __init__(
|
|
1140
|
+
self,
|
|
1141
|
+
/,
|
|
1142
|
+
url: str | Callable[[], str] | Callable[[], Awaitable[str]],
|
|
1143
|
+
headers: None | Mapping = None,
|
|
1144
|
+
start: int = 0,
|
|
1145
|
+
seek_threshold: int = 1 << 20,
|
|
1146
|
+
urlopen = partial(async_stream, "GET"),
|
|
1147
|
+
):
|
|
1148
|
+
super().__init__(
|
|
1149
|
+
url=url,
|
|
1150
|
+
headers=headers,
|
|
1151
|
+
start=start,
|
|
1152
|
+
seek_threshold=seek_threshold,
|
|
1153
|
+
urlopen=urlopen,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
async def _init(
|
|
1157
|
+
self,
|
|
1158
|
+
/,
|
|
1159
|
+
url: str | Callable[[], str] | Callable[[], Awaitable[str]],
|
|
1160
|
+
headers: None | Mapping = None,
|
|
1161
|
+
start: int = 0,
|
|
1162
|
+
seek_threshold: int = 1 << 20,
|
|
1163
|
+
urlopen = None,
|
|
1164
|
+
):
|
|
1165
|
+
if urlopen is None:
|
|
1166
|
+
urlopen = signature(type(self).__init__).parameters["urlopen"].default
|
|
1167
|
+
if isinstance(urlopen, AsyncClient):
|
|
1168
|
+
urlopen = partial(urlopen.stream, "GET")
|
|
1169
|
+
async def urlopen_wrapper(url: str, headers: None | Mapping = headers):
|
|
1170
|
+
context = urlopen(url, headers=headers)
|
|
1171
|
+
resp = await context.__aenter__()
|
|
1172
|
+
resp.raise_for_status()
|
|
1173
|
+
resp.context = context
|
|
1174
|
+
file = bytes_iter_to_async_reader(resp.aiter_raw())
|
|
1175
|
+
self.__dict__["file"] = file
|
|
1176
|
+
return resp
|
|
1177
|
+
await super()._init(
|
|
1178
|
+
url=url,
|
|
1179
|
+
headers=headers,
|
|
1180
|
+
start=start,
|
|
1181
|
+
seek_threshold=seek_threshold,
|
|
1182
|
+
urlopen=urlopen_wrapper,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
@funcproperty
|
|
1186
|
+
def file_closed(self, /) -> bool:
|
|
1187
|
+
return self.response.is_closed
|
|
1188
|
+
|
|
1189
|
+
__all__.append("HttpxFileReader")
|
|
1190
|
+
__all__.append("AsyncHttpxFileReader")
|
|
445
1191
|
except ImportError:
|
|
446
1192
|
pass
|
|
447
1193
|
|
|
448
|
-
|
|
449
|
-
# TODO: 实现 AsyncHTTPFileReader
|
|
450
|
-
# TODO: 支持异步文件,使用 aiohttp,参考 aiofiles 的接口实现
|
|
451
1194
|
# TODO: 设计实现一个 HTTPFileWriter,用于实现上传,关闭后视为上传完成
|
|
452
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-httpfile
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Python httpfile.
|
|
5
5
|
Home-page: https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-httpfile
|
|
6
6
|
License: MIT
|
|
@@ -22,8 +22,10 @@ Classifier: Topic :: Software Development
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Requires-Dist: http_response
|
|
25
|
-
Requires-Dist: python-
|
|
26
|
-
Requires-Dist: python-
|
|
25
|
+
Requires-Dist: python-asynctools (>=0.0.5)
|
|
26
|
+
Requires-Dist: python-filewrap (>=0.2.1)
|
|
27
|
+
Requires-Dist: python-property (>=0.0.2)
|
|
28
|
+
Requires-Dist: python-urlopen (>=0.0.7)
|
|
27
29
|
Project-URL: Repository, https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-httpfile
|
|
28
30
|
Description-Content-Type: text/markdown
|
|
29
31
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
2
|
+
httpfile/__init__.py,sha256=Cp1mdmn7hLNIUsvScclByZZC0hNZh-ggljiYFH1PLVU,37815
|
|
3
|
+
httpfile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
python_httpfile-0.0.3.dist-info/LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
5
|
+
python_httpfile-0.0.3.dist-info/METADATA,sha256=5a1WYcujAY1_K8zdnseHhqzSjrUSPaw_a8sc78kR3lM,1518
|
|
6
|
+
python_httpfile-0.0.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
7
|
+
python_httpfile-0.0.3.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
2
|
-
httpfile/__init__.py,sha256=cZZjfftX2nWqEZQ0jjDnrTOW2NLbfnQy8ZEnP52y_tY,13427
|
|
3
|
-
httpfile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
python_httpfile-0.0.2.dist-info/LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
5
|
-
python_httpfile-0.0.2.dist-info/METADATA,sha256=-hanQvHDGCiX4-fYjubZmioS7o3nAnGPtSkF47eEXr0,1414
|
|
6
|
-
python_httpfile-0.0.2.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
7
|
-
python_httpfile-0.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|