python-httpfile 0.0.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.
- LICENSE +21 -0
- httpfile/__init__.py +410 -0
- httpfile/py.typed +0 -0
- python_httpfile-0.0.1.dist-info/LICENSE +21 -0
- python_httpfile-0.0.1.dist-info/METADATA +48 -0
- python_httpfile-0.0.1.dist-info/RECORD +7 -0
- python_httpfile-0.0.1.dist-info/WHEEL +4 -0
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ChenyangGao <https://github.com/ChenyangGao>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
httpfile/__init__.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
|
|
4
|
+
__author__ = "ChenyangGao <https://chenyanggao.github.io>"
|
|
5
|
+
__version__ = (0, 0, 1)
|
|
6
|
+
__all__ = ["HTTPFileReader", "RequestsFileReader"]
|
|
7
|
+
|
|
8
|
+
import errno
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
11
|
+
from functools import cached_property, partial
|
|
12
|
+
from http.client import HTTPResponse
|
|
13
|
+
from io import (
|
|
14
|
+
BufferedReader, RawIOBase, TextIOWrapper, UnsupportedOperation, DEFAULT_BUFFER_SIZE,
|
|
15
|
+
)
|
|
16
|
+
from os import fstat, stat, PathLike
|
|
17
|
+
from shutil import COPY_BUFSIZE # type: ignore
|
|
18
|
+
from typing import Any, BinaryIO, IO, Optional, Protocol, Self, TypeVar
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
from warnings import warn
|
|
21
|
+
|
|
22
|
+
from aiohttp import ClientSession
|
|
23
|
+
from filewrap import bio_skip_bytes
|
|
24
|
+
from http_response import get_filename, get_length, get_range, get_total_length, is_chunked, is_range_request
|
|
25
|
+
from property import funcproperty
|
|
26
|
+
from requests import Session
|
|
27
|
+
from urlopen import urlopen
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_filesize(file, /, dont_read: bool = True) -> int:
|
|
31
|
+
if isinstance(file, (bytes, str, PathLike)):
|
|
32
|
+
return stat(file).st_size
|
|
33
|
+
curpos = 0
|
|
34
|
+
try:
|
|
35
|
+
curpos = file.seek(0, 1)
|
|
36
|
+
seekable = True
|
|
37
|
+
except Exception:
|
|
38
|
+
seekable = False
|
|
39
|
+
if not seekable:
|
|
40
|
+
try:
|
|
41
|
+
curpos = file.tell()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
try:
|
|
45
|
+
return len(file) - curpos
|
|
46
|
+
except TypeError:
|
|
47
|
+
pass
|
|
48
|
+
if hasattr(file, "fileno"):
|
|
49
|
+
try:
|
|
50
|
+
return fstat(file.fileno()).st_size - curpos
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
if hasattr(file, "headers"):
|
|
54
|
+
l = get_length(file)
|
|
55
|
+
if l is not None:
|
|
56
|
+
return l - curpos
|
|
57
|
+
if seekable:
|
|
58
|
+
try:
|
|
59
|
+
return file.seek(0, 2) - curpos
|
|
60
|
+
finally:
|
|
61
|
+
file.seek(curpos)
|
|
62
|
+
if dont_read:
|
|
63
|
+
return -1
|
|
64
|
+
total = 0
|
|
65
|
+
if hasattr(file, "readinto"):
|
|
66
|
+
readinto = file.readinto
|
|
67
|
+
buf = bytearray(COPY_BUFSIZE)
|
|
68
|
+
while (size := readinto(buf)):
|
|
69
|
+
total += size
|
|
70
|
+
elif hasattr(file, "read"):
|
|
71
|
+
read = file.read
|
|
72
|
+
while (chunk := read(COPY_BUFSIZE)):
|
|
73
|
+
total += len(chunk)
|
|
74
|
+
else:
|
|
75
|
+
return -1
|
|
76
|
+
return total
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HTTPFileReader(RawIOBase, BinaryIO):
|
|
80
|
+
url: str | Callable[[], str]
|
|
81
|
+
response: Any
|
|
82
|
+
length: int
|
|
83
|
+
chunked: bool
|
|
84
|
+
start: int
|
|
85
|
+
urlopen: Callable
|
|
86
|
+
headers: Mapping
|
|
87
|
+
seek_threshold: int
|
|
88
|
+
_seekable: bool
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
/,
|
|
93
|
+
url: str | Callable[[], str],
|
|
94
|
+
headers: Optional[Mapping] = None,
|
|
95
|
+
start: int = 0,
|
|
96
|
+
# NOTE: If the offset of the forward seek is not higher than this value,
|
|
97
|
+
# it will be directly read and discarded, default to 1 MB
|
|
98
|
+
seek_threshold: int = 1 << 20,
|
|
99
|
+
urlopen: Callable[..., HTTPResponse] = urlopen,
|
|
100
|
+
):
|
|
101
|
+
if headers:
|
|
102
|
+
headers = {**headers, "Accept-Encoding": "identity"}
|
|
103
|
+
else:
|
|
104
|
+
headers = {"Accept-Encoding": "identity"}
|
|
105
|
+
if start > 0:
|
|
106
|
+
headers["Range"] = f"bytes={start}-"
|
|
107
|
+
elif start < 0:
|
|
108
|
+
headers["Range"] = f"bytes={start}"
|
|
109
|
+
response = urlopen(url() if callable(url) else url, headers=headers)
|
|
110
|
+
if start:
|
|
111
|
+
rng = get_range(response)
|
|
112
|
+
if not rng:
|
|
113
|
+
raise OSError(errno.ESPIPE, "non-seekable")
|
|
114
|
+
start = rng[0]
|
|
115
|
+
self.__dict__.update(
|
|
116
|
+
url = url,
|
|
117
|
+
response = response,
|
|
118
|
+
length = get_total_length(response) or 0,
|
|
119
|
+
chunked = is_chunked(response),
|
|
120
|
+
start = start,
|
|
121
|
+
closed = False,
|
|
122
|
+
urlopen = urlopen,
|
|
123
|
+
headers = MappingProxyType(headers),
|
|
124
|
+
seek_threshold = max(seek_threshold, 0),
|
|
125
|
+
_seekable = is_range_request(response),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def __del__(self, /):
|
|
129
|
+
try:
|
|
130
|
+
self.close()
|
|
131
|
+
except:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
def __enter__(self, /):
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def __exit__(self, /, *exc_info):
|
|
138
|
+
self.close()
|
|
139
|
+
|
|
140
|
+
def __iter__(self, /):
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __len__(self, /) -> int:
|
|
144
|
+
return self.length
|
|
145
|
+
|
|
146
|
+
def __next__(self, /) -> bytes:
|
|
147
|
+
line = self.readline()
|
|
148
|
+
if line:
|
|
149
|
+
return line
|
|
150
|
+
else:
|
|
151
|
+
raise StopIteration
|
|
152
|
+
|
|
153
|
+
def __repr__(self, /) -> str:
|
|
154
|
+
cls = type(self)
|
|
155
|
+
module = cls.__module__
|
|
156
|
+
name = cls.__qualname__
|
|
157
|
+
if module != "__main__":
|
|
158
|
+
name = module + "." + name
|
|
159
|
+
return f"{name}({self.url!r}, urlopen={self.urlopen!r}, headers={self.headers!r})"
|
|
160
|
+
|
|
161
|
+
def __setattr__(self, attr, val, /):
|
|
162
|
+
raise TypeError("can't set attribute")
|
|
163
|
+
|
|
164
|
+
def _add_start(self, delta: int, /):
|
|
165
|
+
self.__dict__["start"] += delta
|
|
166
|
+
|
|
167
|
+
def close(self, /):
|
|
168
|
+
self.response.close()
|
|
169
|
+
self.__dict__["closed"] = True
|
|
170
|
+
|
|
171
|
+
@funcproperty
|
|
172
|
+
def closed(self, /):
|
|
173
|
+
return self.__dict__["closed"]
|
|
174
|
+
|
|
175
|
+
@funcproperty
|
|
176
|
+
def file(self, /) -> BinaryIO:
|
|
177
|
+
return self.response
|
|
178
|
+
|
|
179
|
+
@funcproperty
|
|
180
|
+
def fileno(self, /):
|
|
181
|
+
return self.file.fileno()
|
|
182
|
+
|
|
183
|
+
def flush(self, /):
|
|
184
|
+
return self.file.flush()
|
|
185
|
+
|
|
186
|
+
def isatty(self, /) -> bool:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
@cached_property
|
|
190
|
+
def mode(self, /) -> str:
|
|
191
|
+
return "rb"
|
|
192
|
+
|
|
193
|
+
@cached_property
|
|
194
|
+
def name(self, /) -> str:
|
|
195
|
+
return get_filename(self.response)
|
|
196
|
+
|
|
197
|
+
def read(self, size: int = -1, /) -> bytes:
|
|
198
|
+
if self.closed:
|
|
199
|
+
raise ValueError("I/O operation on closed file.")
|
|
200
|
+
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
201
|
+
return b""
|
|
202
|
+
if self.file.closed:
|
|
203
|
+
self.reconnect()
|
|
204
|
+
if size is None or size < 0:
|
|
205
|
+
data = self.file.read()
|
|
206
|
+
else:
|
|
207
|
+
data = self.file.read(size)
|
|
208
|
+
if data:
|
|
209
|
+
self._add_start(len(data))
|
|
210
|
+
return data
|
|
211
|
+
|
|
212
|
+
def readable(self, /) -> bool:
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
def readinto(self, buffer, /) -> int:
|
|
216
|
+
if self.closed:
|
|
217
|
+
raise ValueError("I/O operation on closed file.")
|
|
218
|
+
if not self.chunked and self.tell() >= self.length:
|
|
219
|
+
return 0
|
|
220
|
+
if self.file.closed:
|
|
221
|
+
self.reconnect()
|
|
222
|
+
size = self.file.readinto(buffer)
|
|
223
|
+
if size:
|
|
224
|
+
self._add_start(size)
|
|
225
|
+
return size
|
|
226
|
+
|
|
227
|
+
def readline(self, size: Optional[int] = -1, /) -> bytes:
|
|
228
|
+
if self.closed:
|
|
229
|
+
raise ValueError("I/O operation on closed file.")
|
|
230
|
+
if size == 0 or not self.chunked and self.tell() >= self.length:
|
|
231
|
+
return b""
|
|
232
|
+
if self.file.closed:
|
|
233
|
+
self.reconnect()
|
|
234
|
+
if size is None or size < 0:
|
|
235
|
+
data = self.file.readline()
|
|
236
|
+
else:
|
|
237
|
+
data = self.file.readline(size)
|
|
238
|
+
if data:
|
|
239
|
+
self._add_start(len(data))
|
|
240
|
+
return data
|
|
241
|
+
|
|
242
|
+
def readlines(self, hint: int = -1, /) -> list[bytes]:
|
|
243
|
+
if self.closed:
|
|
244
|
+
raise ValueError("I/O operation on closed file.")
|
|
245
|
+
if not self.chunked and self.tell() >= self.length:
|
|
246
|
+
return []
|
|
247
|
+
if self.file.closed:
|
|
248
|
+
self.reconnect()
|
|
249
|
+
ls = self.file.readlines(hint)
|
|
250
|
+
if ls:
|
|
251
|
+
self._add_start(sum(map(len, ls)))
|
|
252
|
+
return ls
|
|
253
|
+
|
|
254
|
+
def reconnect(self, /, start: Optional[int] = None) -> int:
|
|
255
|
+
if not self._seekable:
|
|
256
|
+
if start is None and self.tell() or start:
|
|
257
|
+
raise OSError(errno.EOPNOTSUPP, "Unsupport for reconnection of non-seekable streams.")
|
|
258
|
+
start = 0
|
|
259
|
+
if start is None:
|
|
260
|
+
start = self.tell()
|
|
261
|
+
elif start < 0:
|
|
262
|
+
start = self.length + start
|
|
263
|
+
if start < 0:
|
|
264
|
+
start = 0
|
|
265
|
+
if start >= self.length:
|
|
266
|
+
self.__dict__.update(start=start)
|
|
267
|
+
return start
|
|
268
|
+
self.response.close()
|
|
269
|
+
url = self.url
|
|
270
|
+
response = self.urlopen(
|
|
271
|
+
url() if callable(url) else url,
|
|
272
|
+
headers={**self.headers, "Range": f"bytes={start}-"}
|
|
273
|
+
)
|
|
274
|
+
length_new = get_total_length(response)
|
|
275
|
+
if self.length != length_new:
|
|
276
|
+
raise OSError(errno.EIO, f"file size changed: {self.length} -> {length_new}")
|
|
277
|
+
self.__dict__.update(
|
|
278
|
+
response=response,
|
|
279
|
+
start=start,
|
|
280
|
+
closed=False,
|
|
281
|
+
)
|
|
282
|
+
return start
|
|
283
|
+
|
|
284
|
+
def seek(self, pos: int, whence: int = 0, /) -> int:
|
|
285
|
+
if self.closed:
|
|
286
|
+
raise ValueError("I/O operation on closed file.")
|
|
287
|
+
if not self._seekable:
|
|
288
|
+
raise OSError(errno.EINVAL, "not a seekable stream")
|
|
289
|
+
if whence == 0:
|
|
290
|
+
if pos < 0:
|
|
291
|
+
raise OSError(errno.EINVAL, f"negative seek start: {pos!r}")
|
|
292
|
+
old_pos = self.tell()
|
|
293
|
+
if old_pos == pos:
|
|
294
|
+
return pos
|
|
295
|
+
if pos > old_pos and pos - old_pos <= self.seek_threshold:
|
|
296
|
+
bio_skip_bytes(self, pos - old_pos)
|
|
297
|
+
else:
|
|
298
|
+
self.reconnect(pos)
|
|
299
|
+
return pos
|
|
300
|
+
elif whence == 1:
|
|
301
|
+
if pos == 0:
|
|
302
|
+
return self.tell()
|
|
303
|
+
return self.seek(self.tell() + pos)
|
|
304
|
+
elif whence == 2:
|
|
305
|
+
return self.seek(self.length + pos)
|
|
306
|
+
else:
|
|
307
|
+
raise OSError(errno.EINVAL, f"whence value unsupported: {whence!r}")
|
|
308
|
+
|
|
309
|
+
def seekable(self, /) -> bool:
|
|
310
|
+
return self._seekable
|
|
311
|
+
|
|
312
|
+
def tell(self, /) -> int:
|
|
313
|
+
return self.start
|
|
314
|
+
|
|
315
|
+
def truncate(self, size: Optional[int] = None, /):
|
|
316
|
+
raise UnsupportedOperation(errno.ENOTSUP, "truncate")
|
|
317
|
+
|
|
318
|
+
def writable(self, /) -> bool:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
def write(self, b, /) -> int:
|
|
322
|
+
raise UnsupportedOperation(errno.ENOTSUP, "write")
|
|
323
|
+
|
|
324
|
+
def writelines(self, lines, /):
|
|
325
|
+
raise UnsupportedOperation(errno.ENOTSUP, "writelines")
|
|
326
|
+
|
|
327
|
+
def wrap(
|
|
328
|
+
self,
|
|
329
|
+
/,
|
|
330
|
+
text_mode: bool = False,
|
|
331
|
+
buffering: Optional[int] = None,
|
|
332
|
+
encoding: Optional[str] = None,
|
|
333
|
+
errors: Optional[str] = None,
|
|
334
|
+
newline: Optional[str] = None,
|
|
335
|
+
) -> Self | IO:
|
|
336
|
+
if buffering is None:
|
|
337
|
+
if text_mode:
|
|
338
|
+
buffering = DEFAULT_BUFFER_SIZE
|
|
339
|
+
else:
|
|
340
|
+
buffering = 0
|
|
341
|
+
if buffering == 0:
|
|
342
|
+
if text_mode:
|
|
343
|
+
raise OSError(errno.EINVAL, "can't have unbuffered text I/O")
|
|
344
|
+
return self
|
|
345
|
+
line_buffering = False
|
|
346
|
+
buffer_size: int
|
|
347
|
+
if buffering < 0:
|
|
348
|
+
buffer_size = DEFAULT_BUFFER_SIZE
|
|
349
|
+
elif buffering == 1:
|
|
350
|
+
if not text_mode:
|
|
351
|
+
warn("line buffering (buffering=1) isn't supported in binary mode, "
|
|
352
|
+
"the default buffer size will be used", RuntimeWarning)
|
|
353
|
+
buffer_size = DEFAULT_BUFFER_SIZE
|
|
354
|
+
line_buffering = True
|
|
355
|
+
else:
|
|
356
|
+
buffer_size = buffering
|
|
357
|
+
raw = self
|
|
358
|
+
buffer = BufferedReader(raw, buffer_size)
|
|
359
|
+
if text_mode:
|
|
360
|
+
return TextIOWrapper(
|
|
361
|
+
buffer,
|
|
362
|
+
encoding=encoding,
|
|
363
|
+
errors=errors,
|
|
364
|
+
newline=newline,
|
|
365
|
+
line_buffering=line_buffering,
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
return buffer
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class RequestsFileReader(HTTPFileReader):
|
|
372
|
+
|
|
373
|
+
def __init__(
|
|
374
|
+
self,
|
|
375
|
+
/,
|
|
376
|
+
url: str | Callable[[], str],
|
|
377
|
+
headers: Optional[Mapping] = None,
|
|
378
|
+
start: int = 0,
|
|
379
|
+
seek_threshold: int = 1 << 20,
|
|
380
|
+
urlopen: Callable = Session().get,
|
|
381
|
+
):
|
|
382
|
+
def urlopen_wrapper(url: str, headers: Optional[Mapping] = headers):
|
|
383
|
+
resp = urlopen(url, headers=headers, stream=True)
|
|
384
|
+
resp.raise_for_status()
|
|
385
|
+
return resp
|
|
386
|
+
super().__init__(
|
|
387
|
+
url,
|
|
388
|
+
headers=headers,
|
|
389
|
+
start=start,
|
|
390
|
+
seek_threshold=seek_threshold,
|
|
391
|
+
urlopen=urlopen_wrapper,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def _add_start(self, delta: int, /):
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
@funcproperty
|
|
398
|
+
def file(self, /) -> BinaryIO:
|
|
399
|
+
return self.response.raw
|
|
400
|
+
|
|
401
|
+
def tell(self, /) -> int:
|
|
402
|
+
start = self.start
|
|
403
|
+
if start >= self.length:
|
|
404
|
+
return start
|
|
405
|
+
return start + self.file.tell()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# TODO: 支持异步文件,使用 aiohttp,参考 aiofiles 的接口实现
|
|
409
|
+
# TODO: 设计实现一个 HTTPFileWriter,用于实现上传,关闭后视为上传完成
|
|
410
|
+
|
httpfile/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ChenyangGao <https://github.com/ChenyangGao>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-httpfile
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python httpfile classes.
|
|
5
|
+
Home-page: https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-httpfile
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: http,file,wrapper
|
|
8
|
+
Author: ChenyangGao
|
|
9
|
+
Author-email: wosiwujm@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Dist: aiohttp
|
|
25
|
+
Requires-Dist: http_response
|
|
26
|
+
Requires-Dist: python-filewrap
|
|
27
|
+
Requires-Dist: python-property
|
|
28
|
+
Requires-Dist: python-urlopen
|
|
29
|
+
Requires-Dist: requests
|
|
30
|
+
Project-URL: Repository, https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-httpfile
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Python httpfile classes.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
You can install from [pypi](https://pypi.org/project/python-httpfile/)
|
|
38
|
+
|
|
39
|
+
```console
|
|
40
|
+
pip install -U python-httpfile
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import httpfile
|
|
47
|
+
```
|
|
48
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
2
|
+
httpfile/__init__.py,sha256=KYXYVFaUhu06R3cBEb9FPGmPuLrMVUFWKPx5aovija4,12280
|
|
3
|
+
httpfile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
python_httpfile-0.0.1.dist-info/LICENSE,sha256=o5242_N2TgDsWwFhPn7yr8YJNF7XsJM5NxUMtcT97bc,1100
|
|
5
|
+
python_httpfile-0.0.1.dist-info/METADATA,sha256=ZWLh_Fh4q5FjF2P_2B2Szl2KjYs16XtzCbzjLSxTgIs,1508
|
|
6
|
+
python_httpfile-0.0.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
7
|
+
python_httpfile-0.0.1.dist-info/RECORD,,
|