python-httpfile 0.0.2.1__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 +794 -51
- {python_httpfile-0.0.2.1.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.1.dist-info/RECORD +0 -7
- {python_httpfile-0.0.2.1.dist-info → python_httpfile-0.0.3.dist-info}/LICENSE +0 -0
- {python_httpfile-0.0.2.1.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,23 +174,36 @@ 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
|
|
@@ -208,7 +230,7 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
208
230
|
raise ValueError("I/O operation on closed file.")
|
|
209
231
|
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
210
232
|
return b""
|
|
211
|
-
if self.
|
|
233
|
+
if self.file_closed:
|
|
212
234
|
self.reconnect()
|
|
213
235
|
if size is None or size < 0:
|
|
214
236
|
data = self.file.read()
|
|
@@ -226,19 +248,19 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
226
248
|
raise ValueError("I/O operation on closed file.")
|
|
227
249
|
if not buffer or not self.chunked and self.tell() >= self.length:
|
|
228
250
|
return 0
|
|
229
|
-
if self.
|
|
251
|
+
if self.file_closed:
|
|
230
252
|
self.reconnect()
|
|
231
253
|
size = self.file.readinto(buffer)
|
|
232
254
|
if size:
|
|
233
255
|
self._add_start(size)
|
|
234
256
|
return size
|
|
235
257
|
|
|
236
|
-
def readline(self, size:
|
|
258
|
+
def readline(self, size: None | int = -1, /) -> bytes:
|
|
237
259
|
if self.closed:
|
|
238
260
|
raise ValueError("I/O operation on closed file.")
|
|
239
261
|
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
240
262
|
return b""
|
|
241
|
-
if self.
|
|
263
|
+
if self.file_closed:
|
|
242
264
|
self.reconnect()
|
|
243
265
|
if size is None or size < 0:
|
|
244
266
|
data = self.file.readline()
|
|
@@ -253,14 +275,14 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
253
275
|
raise ValueError("I/O operation on closed file.")
|
|
254
276
|
if not self.chunked and self.tell() >= self.length:
|
|
255
277
|
return []
|
|
256
|
-
if self.
|
|
278
|
+
if self.file_closed:
|
|
257
279
|
self.reconnect()
|
|
258
280
|
ls = self.file.readlines(hint)
|
|
259
281
|
if ls:
|
|
260
282
|
self._add_start(sum(map(len, ls)))
|
|
261
283
|
return ls
|
|
262
284
|
|
|
263
|
-
def reconnect(self, /, start:
|
|
285
|
+
def reconnect(self, /, start: None | int = None) -> int:
|
|
264
286
|
if not self._seekable:
|
|
265
287
|
if start is None and self.tell() or start:
|
|
266
288
|
raise OSError(errno.EOPNOTSUPP, "Unsupport for reconnection of non-seekable streams.")
|
|
@@ -327,9 +349,15 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
327
349
|
return self._seekable
|
|
328
350
|
|
|
329
351
|
def tell(self, /) -> int:
|
|
330
|
-
|
|
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
|
|
331
359
|
|
|
332
|
-
def truncate(self, size:
|
|
360
|
+
def truncate(self, size: None | int = None, /):
|
|
333
361
|
raise UnsupportedOperation(errno.ENOTSUP, "truncate")
|
|
334
362
|
|
|
335
363
|
def writable(self, /) -> bool:
|
|
@@ -341,15 +369,109 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
341
369
|
def writelines(self, lines, /):
|
|
342
370
|
raise UnsupportedOperation(errno.ENOTSUP, "writelines")
|
|
343
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
|
|
344
442
|
def wrap(
|
|
345
443
|
self,
|
|
346
444
|
/,
|
|
347
|
-
text_mode:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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:
|
|
353
475
|
if buffering is None:
|
|
354
476
|
if text_mode:
|
|
355
477
|
buffering = DEFAULT_BUFFER_SIZE
|
|
@@ -385,8 +507,437 @@ class HTTPFileReader(RawIOBase, BinaryIO):
|
|
|
385
507
|
return buffer
|
|
386
508
|
|
|
387
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
|
+
|
|
388
936
|
try:
|
|
389
|
-
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())
|
|
390
941
|
|
|
391
942
|
class RequestsFileReader(HTTPFileReader):
|
|
392
943
|
|
|
@@ -394,13 +945,17 @@ try:
|
|
|
394
945
|
self,
|
|
395
946
|
/,
|
|
396
947
|
url: str | Callable[[], str],
|
|
397
|
-
headers:
|
|
948
|
+
headers: None | Mapping = None,
|
|
398
949
|
start: int = 0,
|
|
399
950
|
seek_threshold: int = 1 << 20,
|
|
400
|
-
urlopen
|
|
951
|
+
urlopen = partial(requests_request, "GET"),
|
|
401
952
|
):
|
|
402
|
-
|
|
403
|
-
|
|
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)
|
|
404
959
|
resp.raise_for_status()
|
|
405
960
|
return resp
|
|
406
961
|
super().__init__(
|
|
@@ -411,41 +966,229 @@ try:
|
|
|
411
966
|
urlopen=urlopen_wrapper,
|
|
412
967
|
)
|
|
413
968
|
|
|
414
|
-
def _add_start(self, delta: int, /):
|
|
415
|
-
pass
|
|
416
|
-
|
|
417
969
|
@funcproperty
|
|
418
970
|
def file(self, /) -> BinaryIO:
|
|
419
971
|
return self.response.raw
|
|
420
972
|
|
|
421
|
-
def tell(self, /) -> int:
|
|
422
|
-
start = self.start
|
|
423
|
-
if start >= self.length:
|
|
424
|
-
return start
|
|
425
|
-
return start + self.file.tell()
|
|
426
|
-
|
|
427
973
|
__all__.append("RequestsFileReader")
|
|
428
974
|
except ImportError:
|
|
429
975
|
pass
|
|
430
976
|
|
|
431
977
|
|
|
432
978
|
try:
|
|
433
|
-
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
|
|
434
1047
|
|
|
435
|
-
|
|
1048
|
+
__all__.append("AiohttpFileReader")
|
|
436
1049
|
except ImportError:
|
|
437
1050
|
pass
|
|
438
1051
|
|
|
439
1052
|
|
|
440
1053
|
try:
|
|
441
|
-
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
|
+
)
|
|
442
1103
|
|
|
443
|
-
|
|
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")
|
|
444
1191
|
except ImportError:
|
|
445
1192
|
pass
|
|
446
1193
|
|
|
447
|
-
|
|
448
|
-
# TODO: 实现 AsyncHTTPFileReader
|
|
449
|
-
# TODO: 支持异步文件,使用 aiohttp,参考 aiofiles 的接口实现
|
|
450
1194
|
# TODO: 设计实现一个 HTTPFileWriter,用于实现上传,关闭后视为上传完成
|
|
451
|
-
|
|
@@ -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=9qTmQOShcWtnLtIsqL3NbIeH9M2tAWE9NquEjyFs9ZM,13416
|
|
3
|
-
httpfile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
python_httpfile-0.0.2.1.dist-info/LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
5
|
-
python_httpfile-0.0.2.1.dist-info/METADATA,sha256=tKxwg73IETWHCmf-F7--YKrQtlgn15r4iUxA7BzDkqQ,1416
|
|
6
|
-
python_httpfile-0.0.2.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
7
|
-
python_httpfile-0.0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|