httk-core 2.0.0__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.
- httk/core/__init__.py +112 -0
- httk/core/_discover.py +35 -0
- httk/core/_plugins.py +95 -0
- httk/core/dataloader.py +263 -0
- httk/core/datastream/__init__.py +80 -0
- httk/core/datastream/bytestream_api.py +33 -0
- httk/core/datastream/bytestream_backend.py +12 -0
- httk/core/datastream/bytestream_bytes.py +52 -0
- httk/core/datastream/bytestream_bytes_view.py +31 -0
- httk/core/datastream/bytestream_common.py +38 -0
- httk/core/datastream/bytestream_file.py +53 -0
- httk/core/datastream/bytestream_file_view.py +189 -0
- httk/core/datastream/bytestream_filename.py +53 -0
- httk/core/datastream/bytestream_filename_view.py +34 -0
- httk/core/datastream/bytestream_like.py +17 -0
- httk/core/datastream/bytestream_request.py +69 -0
- httk/core/datastream/bytestream_request_view.py +44 -0
- httk/core/datastream/bytestream_url.py +74 -0
- httk/core/datastream/bytestream_url_view.py +35 -0
- httk/core/datastream/bytestream_view.py +16 -0
- httk/core/datastream/compression.py +183 -0
- httk/core/datastream/textstream_api.py +33 -0
- httk/core/datastream/textstream_backend.py +12 -0
- httk/core/datastream/textstream_common.py +37 -0
- httk/core/datastream/textstream_file.py +45 -0
- httk/core/datastream/textstream_file_view.py +201 -0
- httk/core/datastream/textstream_filename.py +55 -0
- httk/core/datastream/textstream_filename_view.py +35 -0
- httk/core/datastream/textstream_like.py +15 -0
- httk/core/datastream/textstream_request.py +72 -0
- httk/core/datastream/textstream_request_view.py +44 -0
- httk/core/datastream/textstream_string.py +47 -0
- httk/core/datastream/textstream_string_view.py +31 -0
- httk/core/datastream/textstream_url.py +77 -0
- httk/core/datastream/textstream_url_view.py +35 -0
- httk/core/datastream/textstream_view.py +16 -0
- httk/core/loading.py +29 -0
- httk/core/py.typed +0 -0
- httk/core/register.py +30 -0
- httk/core/views/__init__.py +5 -0
- httk/core/views/backend.py +49 -0
- httk/core/views/unwrapping.py +12 -0
- httk/core/views/view.py +56 -0
- httk/handlers/core/__init__.py +0 -0
- httk_core-2.0.0.dist-info/METADATA +76 -0
- httk_core-2.0.0.dist-info/RECORD +49 -0
- httk_core-2.0.0.dist-info/WHEEL +5 -0
- httk_core-2.0.0.dist-info/licenses/LICENSE +661 -0
- httk_core-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
from ..views import unwrap
|
|
4
|
+
from .bytestream_backend import BytestreamBackend
|
|
5
|
+
from .bytestream_like import BytestreamLike
|
|
6
|
+
from .bytestream_view import BytestreamView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BytestreamBytesView(BytestreamView, bytes):
|
|
10
|
+
"""
|
|
11
|
+
A view presenting underlying streaming byte data as bytes.
|
|
12
|
+
This view can be used both to pass bytes in place of streaming data, and for reading streaming data into bytes.
|
|
13
|
+
Note: this view is not lazy (this is impossible for views inheriting bytes, since bytes is immutable), hence all
|
|
14
|
+
streaming data is read immediately upon creating this view.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_backend: BytestreamBackend
|
|
18
|
+
|
|
19
|
+
def __new__(cls, obj: BytestreamLike, **hints: Any) -> Self:
|
|
20
|
+
if isinstance(obj, cls):
|
|
21
|
+
return obj
|
|
22
|
+
backend = cls._prepare_backend(obj, hints)
|
|
23
|
+
instance = super().__new__(cls, backend.read())
|
|
24
|
+
instance._backend = backend
|
|
25
|
+
return instance
|
|
26
|
+
|
|
27
|
+
def __init__(self, obj: BytestreamLike, **hints: Any) -> None:
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
def unwrap(self) -> Any:
|
|
31
|
+
return unwrap(self._backend)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BytestreamCommon(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Common superclass for many of the implementations of backends for streaming byte data.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
_f: io.IOBase | None
|
|
13
|
+
_underlying: io.IOBase | None
|
|
14
|
+
_closed: bool
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def _ensure_f(self) -> io.IOBase:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def unwrap(self) -> io.IOBase:
|
|
21
|
+
return self._ensure_f()
|
|
22
|
+
|
|
23
|
+
def read(self, size: int = -1) -> bytes:
|
|
24
|
+
return cast(bytes, self._ensure_f().read(size))
|
|
25
|
+
|
|
26
|
+
def close(self) -> None:
|
|
27
|
+
if self._f is not None and not self._f.closed:
|
|
28
|
+
self._f.close()
|
|
29
|
+
# A decompression wrapper does not close the source stream it reads from, so close it too.
|
|
30
|
+
if self._underlying is not None and self._underlying is not self._f and not self._underlying.closed:
|
|
31
|
+
self._underlying.close()
|
|
32
|
+
self._closed = True
|
|
33
|
+
|
|
34
|
+
def seek(self, offset: int, whence: int = os.SEEK_SET) -> int:
|
|
35
|
+
return cast(int, self._ensure_f().seek(offset, whence))
|
|
36
|
+
|
|
37
|
+
def tell(self) -> int:
|
|
38
|
+
return cast(int, self._ensure_f().tell())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .bytestream_backend import BytestreamBackend
|
|
5
|
+
from .bytestream_common import BytestreamCommon
|
|
6
|
+
from .compression import open_compressed, validate_compression
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BytestreamFile(BytestreamCommon, BytestreamBackend):
|
|
10
|
+
"""
|
|
11
|
+
Backend for file-based (io.IOBase-conforming) streaming byte data.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_source: io.IOBase
|
|
15
|
+
_compression: str
|
|
16
|
+
_f: io.IOBase | None
|
|
17
|
+
_underlying: io.IOBase | None
|
|
18
|
+
_closed: bool
|
|
19
|
+
|
|
20
|
+
# mypy does not allow to type annotate __new__ as `Self | None` for some reason
|
|
21
|
+
def __new__(cls, obj: io.IOBase, **hints: Any) -> Any:
|
|
22
|
+
if not isinstance(obj, io.IOBase):
|
|
23
|
+
return None
|
|
24
|
+
if hints and hints.get("kind", "file") != "file":
|
|
25
|
+
return None
|
|
26
|
+
return super().__new__(cls)
|
|
27
|
+
|
|
28
|
+
def __init__(self, obj: io.IOBase, **hints: Any) -> None:
|
|
29
|
+
self._source = obj
|
|
30
|
+
self._compression = hints.get("compression", "auto")
|
|
31
|
+
validate_compression(self._compression)
|
|
32
|
+
self._f = None
|
|
33
|
+
# We adopt (and hence close) the caller's stream; keep it here so close() reaches it
|
|
34
|
+
# even when a decompression wrapper is layered on top and when no read ever happens.
|
|
35
|
+
self._underlying = obj
|
|
36
|
+
self._closed = False
|
|
37
|
+
|
|
38
|
+
def _ensure_f(self) -> io.IOBase:
|
|
39
|
+
if self._closed or self._source.closed:
|
|
40
|
+
raise ValueError("I/O operation on closed stream")
|
|
41
|
+
if self._f is None:
|
|
42
|
+
name = getattr(self._source, "name", None)
|
|
43
|
+
self._f = open_compressed(self._source, compression=self._compression, name=name)
|
|
44
|
+
return self._f
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def name(self) -> str | None:
|
|
48
|
+
self._ensure_f()
|
|
49
|
+
return getattr(self._source, "name", None)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def closed(self) -> bool:
|
|
53
|
+
return self._closed or self._source.closed
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import Any, Iterator, NoReturn, Self
|
|
3
|
+
|
|
4
|
+
from ..views import unwrap
|
|
5
|
+
from .bytestream_api import BytestreamAPI
|
|
6
|
+
from .bytestream_backend import BytestreamBackend
|
|
7
|
+
from .bytestream_like import BytestreamLike
|
|
8
|
+
from .bytestream_view import BytestreamView
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BytestreamFileView(BytestreamView, io.IOBase, BytestreamAPI):
|
|
12
|
+
"""
|
|
13
|
+
A view presenting an underlying data streaming backend via an io.IOBase-like API.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
_backend: BytestreamBackend
|
|
17
|
+
_readline_buffer: bytes
|
|
18
|
+
|
|
19
|
+
def __new__(cls, obj: BytestreamLike, **hints: Any) -> Self:
|
|
20
|
+
if isinstance(obj, cls):
|
|
21
|
+
return obj
|
|
22
|
+
backend = cls._prepare_backend(obj, hints)
|
|
23
|
+
instance = super().__new__(cls)
|
|
24
|
+
instance._backend = backend
|
|
25
|
+
instance._readline_buffer = b""
|
|
26
|
+
return instance
|
|
27
|
+
|
|
28
|
+
def __init__(self, obj: BytestreamLike, **hints: Any) -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def unwrap(self) -> Any:
|
|
32
|
+
return unwrap(self._backend)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def name(self) -> str | None:
|
|
36
|
+
return self._backend.name
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def closed(self) -> bool:
|
|
40
|
+
return self._backend.closed
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
self._backend.close()
|
|
44
|
+
|
|
45
|
+
def readable(self) -> bool:
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
def writable(self) -> bool:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def seekable(self) -> bool:
|
|
52
|
+
return hasattr(self._backend, "seek") and hasattr(self._backend, "tell")
|
|
53
|
+
|
|
54
|
+
def flush(self) -> None:
|
|
55
|
+
if self.closed:
|
|
56
|
+
raise ValueError("I/O operation on closed file.")
|
|
57
|
+
flush = getattr(self._backend, "flush", None)
|
|
58
|
+
if flush:
|
|
59
|
+
flush()
|
|
60
|
+
|
|
61
|
+
def read(self, size: int | None = -1) -> bytes:
|
|
62
|
+
if self.closed:
|
|
63
|
+
raise ValueError("I/O operation on closed file.")
|
|
64
|
+
|
|
65
|
+
if size is None or size < 0:
|
|
66
|
+
if self._readline_buffer:
|
|
67
|
+
prefix = self._readline_buffer
|
|
68
|
+
self._readline_buffer = b""
|
|
69
|
+
return prefix + self._backend.read()
|
|
70
|
+
return self._backend.read()
|
|
71
|
+
|
|
72
|
+
if size == 0:
|
|
73
|
+
return b""
|
|
74
|
+
|
|
75
|
+
if self._readline_buffer:
|
|
76
|
+
if len(self._readline_buffer) >= size:
|
|
77
|
+
out = self._readline_buffer[:size]
|
|
78
|
+
self._readline_buffer = self._readline_buffer[size:]
|
|
79
|
+
return out
|
|
80
|
+
prefix = self._readline_buffer
|
|
81
|
+
self._readline_buffer = b""
|
|
82
|
+
return prefix + self._backend.read(size - len(prefix))
|
|
83
|
+
|
|
84
|
+
return self._backend.read(size)
|
|
85
|
+
|
|
86
|
+
def readline(self, size: int | None = -1) -> bytes:
|
|
87
|
+
if self.closed:
|
|
88
|
+
raise ValueError("I/O operation on closed file.")
|
|
89
|
+
if size == 0:
|
|
90
|
+
return b""
|
|
91
|
+
if size is None:
|
|
92
|
+
size = -1
|
|
93
|
+
|
|
94
|
+
parts: list[bytes] = []
|
|
95
|
+
total = 0
|
|
96
|
+
|
|
97
|
+
while True:
|
|
98
|
+
if self._readline_buffer:
|
|
99
|
+
chunk = self._readline_buffer
|
|
100
|
+
self._readline_buffer = b""
|
|
101
|
+
else:
|
|
102
|
+
to_read = 8192
|
|
103
|
+
if size >= 0:
|
|
104
|
+
remaining = size - total
|
|
105
|
+
if remaining <= 0:
|
|
106
|
+
break
|
|
107
|
+
to_read = min(to_read, remaining)
|
|
108
|
+
chunk = self._backend.read(to_read)
|
|
109
|
+
|
|
110
|
+
if chunk == b"":
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
newline_pos = chunk.find(b"\n")
|
|
114
|
+
if newline_pos != -1:
|
|
115
|
+
newline_pos += 1
|
|
116
|
+
take = chunk[:newline_pos]
|
|
117
|
+
rest = chunk[newline_pos:]
|
|
118
|
+
|
|
119
|
+
if size >= 0 and total + len(take) > size:
|
|
120
|
+
cutoff = size - total
|
|
121
|
+
parts.append(take[:cutoff])
|
|
122
|
+
self._readline_buffer = take[cutoff:] + rest
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
parts.append(take)
|
|
126
|
+
self._readline_buffer = rest
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
if size >= 0 and total + len(chunk) > size:
|
|
130
|
+
cutoff = size - total
|
|
131
|
+
parts.append(chunk[:cutoff])
|
|
132
|
+
self._readline_buffer = chunk[cutoff:]
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
parts.append(chunk)
|
|
136
|
+
total += len(chunk)
|
|
137
|
+
|
|
138
|
+
return b"".join(parts)
|
|
139
|
+
|
|
140
|
+
def readlines(self, hint: int = -1) -> list[bytes]:
|
|
141
|
+
if self.closed:
|
|
142
|
+
raise ValueError("I/O operation on closed file.")
|
|
143
|
+
|
|
144
|
+
lines: list[bytes] = []
|
|
145
|
+
total = 0
|
|
146
|
+
|
|
147
|
+
while True:
|
|
148
|
+
line = self.readline()
|
|
149
|
+
if line == b"":
|
|
150
|
+
break
|
|
151
|
+
lines.append(line)
|
|
152
|
+
total += len(line)
|
|
153
|
+
if hint >= 0 and total >= hint:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
return lines
|
|
157
|
+
|
|
158
|
+
def __iter__(self) -> Iterator[bytes]:
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def __next__(self) -> bytes:
|
|
162
|
+
line = self.readline()
|
|
163
|
+
if line == b"":
|
|
164
|
+
raise StopIteration
|
|
165
|
+
return line
|
|
166
|
+
|
|
167
|
+
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
|
|
168
|
+
if self.closed:
|
|
169
|
+
raise ValueError("I/O operation on closed file.")
|
|
170
|
+
seek = getattr(self._backend, "seek", None)
|
|
171
|
+
if not seek:
|
|
172
|
+
raise io.UnsupportedOperation("underlying stream is not seekable")
|
|
173
|
+
|
|
174
|
+
pos = seek(offset, whence)
|
|
175
|
+
self._readline_buffer = b""
|
|
176
|
+
return pos
|
|
177
|
+
|
|
178
|
+
def tell(self) -> int:
|
|
179
|
+
if self.closed:
|
|
180
|
+
raise ValueError("I/O operation on closed file.")
|
|
181
|
+
tell = getattr(self._backend, "tell", None)
|
|
182
|
+
if not tell:
|
|
183
|
+
raise io.UnsupportedOperation("underlying stream does not support tell()")
|
|
184
|
+
|
|
185
|
+
pos = tell()
|
|
186
|
+
return pos - len(self._readline_buffer)
|
|
187
|
+
|
|
188
|
+
def detach(self) -> NoReturn:
|
|
189
|
+
raise io.UnsupportedOperation("detach")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .bytestream_backend import BytestreamBackend
|
|
6
|
+
from .bytestream_common import BytestreamCommon
|
|
7
|
+
from .compression import open_compressed, validate_compression
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BytestreamFilename(BytestreamCommon, BytestreamBackend):
|
|
11
|
+
"""
|
|
12
|
+
Backend for streaming byte data via operations on a file specified by a filename.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_filename: str
|
|
16
|
+
_compression: str
|
|
17
|
+
_f: io.IOBase | None
|
|
18
|
+
_underlying: io.IOBase | None
|
|
19
|
+
_closed: bool
|
|
20
|
+
|
|
21
|
+
# mypy does not allow to type annotate __new__ as `Self | None` for some reason
|
|
22
|
+
def __new__(cls, filename: str | Path, **hints: Any) -> Any:
|
|
23
|
+
if not isinstance(filename, str | Path):
|
|
24
|
+
return None
|
|
25
|
+
if hints and hints.get("kind", "filename") != "filename":
|
|
26
|
+
return None
|
|
27
|
+
return super().__new__(cls)
|
|
28
|
+
|
|
29
|
+
def __init__(self, filename: str | Path, **hints: Any) -> None:
|
|
30
|
+
self._filename = str(filename)
|
|
31
|
+
self._compression = hints.get("compression", "extension")
|
|
32
|
+
validate_compression(self._compression)
|
|
33
|
+
self._f = None
|
|
34
|
+
self._underlying = None
|
|
35
|
+
self._closed = False
|
|
36
|
+
|
|
37
|
+
def _ensure_f(self) -> io.IOBase:
|
|
38
|
+
if self._closed:
|
|
39
|
+
raise ValueError("I/O operation on closed stream")
|
|
40
|
+
if self._f is None:
|
|
41
|
+
raw = open(self._filename, "rb")
|
|
42
|
+
opened = open_compressed(raw, compression=self._compression, name=self._filename)
|
|
43
|
+
self._underlying = raw if opened is not raw else None
|
|
44
|
+
self._f = opened
|
|
45
|
+
return self._f
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def name(self) -> str | None:
|
|
49
|
+
return self._filename
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def closed(self) -> bool:
|
|
53
|
+
return self._closed
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
from ..views import unwrap
|
|
4
|
+
from .bytestream_backend import BytestreamBackend
|
|
5
|
+
from .bytestream_like import BytestreamLike
|
|
6
|
+
from .bytestream_view import BytestreamView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BytestreamFilenameView(BytestreamView, str):
|
|
10
|
+
"""
|
|
11
|
+
A view presenting an underlying data streaming backend via a filename.
|
|
12
|
+
This view is mostly useful for providing filenames to functions that will open them.
|
|
13
|
+
Note: this view is not lazy (this is impossible for views inheriting str, since str is immutable).
|
|
14
|
+
|
|
15
|
+
Raises TypeError if created with a streaming data source that does not come with a name.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_backend: BytestreamBackend
|
|
19
|
+
|
|
20
|
+
def __new__(cls, obj: BytestreamLike, **hints: Any) -> Self:
|
|
21
|
+
if isinstance(obj, cls):
|
|
22
|
+
return obj
|
|
23
|
+
backend = cls._prepare_backend(obj, hints)
|
|
24
|
+
if backend.name is None:
|
|
25
|
+
raise TypeError("This backend cannot be represented as a filename (no underlying filename)")
|
|
26
|
+
instance = super().__new__(cls, backend.name)
|
|
27
|
+
instance._backend = backend
|
|
28
|
+
return instance
|
|
29
|
+
|
|
30
|
+
def __init__(self, obj: BytestreamLike, **hints: Any) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
def unwrap(self) -> Any:
|
|
34
|
+
return unwrap(self._backend)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import pathlib
|
|
3
|
+
import urllib.request
|
|
4
|
+
|
|
5
|
+
from . import bytestream_backend, bytestream_view
|
|
6
|
+
|
|
7
|
+
type BytestreamLike = (
|
|
8
|
+
bytestream_backend.BytestreamBackend
|
|
9
|
+
| bytestream_view.BytestreamView
|
|
10
|
+
| io.IOBase
|
|
11
|
+
| io.BytesIO
|
|
12
|
+
| bytes
|
|
13
|
+
| bytearray
|
|
14
|
+
| str
|
|
15
|
+
| pathlib.Path
|
|
16
|
+
| urllib.request.Request
|
|
17
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import urllib.request
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from .bytestream_backend import BytestreamBackend
|
|
7
|
+
from .bytestream_common import BytestreamCommon
|
|
8
|
+
from .compression import open_compressed, validate_compression
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BytestreamRequest(BytestreamCommon, BytestreamBackend):
|
|
12
|
+
"""
|
|
13
|
+
Backend for streaming byte data fetched via a urllib.request.Request.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
_request: urllib.request.Request
|
|
17
|
+
_timeout: float | None
|
|
18
|
+
_compression: str
|
|
19
|
+
_f: io.IOBase | None
|
|
20
|
+
_underlying: io.IOBase | None
|
|
21
|
+
_closed: bool
|
|
22
|
+
|
|
23
|
+
# mypy does not allow to type annotate __new__ as `Self | None` for some reason
|
|
24
|
+
def __new__(cls, request: urllib.request.Request, **hints: Any) -> Any:
|
|
25
|
+
if not isinstance(request, urllib.request.Request):
|
|
26
|
+
return None
|
|
27
|
+
if hints and hints.get("kind", "request") != "request":
|
|
28
|
+
return None
|
|
29
|
+
return super().__new__(cls)
|
|
30
|
+
|
|
31
|
+
def __init__(self, request: urllib.request.Request, **hints: Any) -> None:
|
|
32
|
+
self._request = request
|
|
33
|
+
self._timeout = hints.get("timeout")
|
|
34
|
+
self._compression = hints.get("compression", "auto")
|
|
35
|
+
validate_compression(self._compression)
|
|
36
|
+
self._f = None
|
|
37
|
+
self._underlying = None
|
|
38
|
+
self._closed = False
|
|
39
|
+
|
|
40
|
+
def _ensure_f(self) -> io.IOBase:
|
|
41
|
+
if self._closed:
|
|
42
|
+
raise ValueError("I/O operation on closed stream")
|
|
43
|
+
if self._f is None:
|
|
44
|
+
if self._timeout is None:
|
|
45
|
+
resp = urllib.request.urlopen(self._request)
|
|
46
|
+
else:
|
|
47
|
+
resp = urllib.request.urlopen(self._request, timeout=self._timeout)
|
|
48
|
+
raw = cast(io.IOBase, resp)
|
|
49
|
+
name = urllib.parse.urlsplit(self._request.full_url).path
|
|
50
|
+
opened = open_compressed(raw, compression=self._compression, name=name)
|
|
51
|
+
self._underlying = raw if opened is not raw else None
|
|
52
|
+
self._f = opened
|
|
53
|
+
return self._f
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def name(self) -> str | None:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def url(self) -> str:
|
|
61
|
+
return self._request.full_url
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def request(self) -> urllib.request.Request:
|
|
65
|
+
return self._request
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def closed(self) -> bool:
|
|
69
|
+
return self._closed
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import urllib.request
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from ..views import unwrap
|
|
5
|
+
from .bytestream_backend import BytestreamBackend
|
|
6
|
+
from .bytestream_like import BytestreamLike
|
|
7
|
+
from .bytestream_view import BytestreamView
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BytestreamRequestView(BytestreamView, urllib.request.Request):
|
|
11
|
+
"""
|
|
12
|
+
A view presenting an underlying data streaming backend via a urllib.request.Request.
|
|
13
|
+
This view is mostly useful for providing a Request to functions that will open it.
|
|
14
|
+
Note: this view is not lazy (it does not fetch); it only mirrors the underlying request/URL.
|
|
15
|
+
|
|
16
|
+
Raises TypeError if created with a streaming data source that does not come with a URL.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_backend: BytestreamBackend
|
|
20
|
+
|
|
21
|
+
def __new__(cls, obj: BytestreamLike, **hints: Any) -> Self:
|
|
22
|
+
if isinstance(obj, cls):
|
|
23
|
+
return obj
|
|
24
|
+
backend = cls._prepare_backend(obj, hints)
|
|
25
|
+
request = getattr(backend, "request", None)
|
|
26
|
+
if request is not None:
|
|
27
|
+
full_url, data, headers = request.full_url, request.data, request.headers
|
|
28
|
+
else:
|
|
29
|
+
url = getattr(backend, "url", None)
|
|
30
|
+
if url is None:
|
|
31
|
+
raise TypeError("This backend cannot be represented as a Request (no underlying URL)")
|
|
32
|
+
full_url, data, headers = url, None, {}
|
|
33
|
+
instance = super().__new__(cls)
|
|
34
|
+
# Request is mutable, so its state is initialized here in __new__ (keeping __init__ a no-op),
|
|
35
|
+
# so that rewrapping an existing view via cls(view) does not re-initialize it.
|
|
36
|
+
urllib.request.Request.__init__(instance, full_url, data=data, headers=headers)
|
|
37
|
+
instance._backend = backend
|
|
38
|
+
return instance
|
|
39
|
+
|
|
40
|
+
def __init__(self, obj: BytestreamLike, **hints: Any) -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def unwrap(self) -> Any:
|
|
44
|
+
return unwrap(self._backend)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import urllib.request
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from .bytestream_backend import BytestreamBackend
|
|
7
|
+
from .bytestream_common import BytestreamCommon
|
|
8
|
+
from .compression import open_compressed, validate_compression
|
|
9
|
+
|
|
10
|
+
_URL_SCHEMES = ("http", "https", "ftp", "file")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BytestreamURL(BytestreamCommon, BytestreamBackend):
|
|
14
|
+
"""
|
|
15
|
+
Backend for streaming byte data fetched from a URL string.
|
|
16
|
+
A bare string is interpreted as a URL when its scheme is one of http, https, ftp, or file,
|
|
17
|
+
or when an explicit kind="url" hint is given.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_url: str
|
|
21
|
+
_timeout: float | None
|
|
22
|
+
_compression: str
|
|
23
|
+
_f: io.IOBase | None
|
|
24
|
+
_underlying: io.IOBase | None
|
|
25
|
+
_closed: bool
|
|
26
|
+
|
|
27
|
+
# mypy does not allow to type annotate __new__ as `Self | None` for some reason
|
|
28
|
+
def __new__(cls, url: str, **hints: Any) -> Any:
|
|
29
|
+
if not isinstance(url, str):
|
|
30
|
+
return None
|
|
31
|
+
kind = hints.get("kind")
|
|
32
|
+
if kind == "url":
|
|
33
|
+
if not urllib.parse.urlsplit(url).scheme:
|
|
34
|
+
return None
|
|
35
|
+
return super().__new__(cls)
|
|
36
|
+
if kind is None and urllib.parse.urlsplit(url).scheme in _URL_SCHEMES:
|
|
37
|
+
return super().__new__(cls)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def __init__(self, url: str, **hints: Any) -> None:
|
|
41
|
+
self._url = url
|
|
42
|
+
self._timeout = hints.get("timeout")
|
|
43
|
+
self._compression = hints.get("compression", "auto")
|
|
44
|
+
validate_compression(self._compression)
|
|
45
|
+
self._f = None
|
|
46
|
+
self._underlying = None
|
|
47
|
+
self._closed = False
|
|
48
|
+
|
|
49
|
+
def _ensure_f(self) -> io.IOBase:
|
|
50
|
+
if self._closed:
|
|
51
|
+
raise ValueError("I/O operation on closed stream")
|
|
52
|
+
if self._f is None:
|
|
53
|
+
if self._timeout is None:
|
|
54
|
+
resp = urllib.request.urlopen(self._url)
|
|
55
|
+
else:
|
|
56
|
+
resp = urllib.request.urlopen(self._url, timeout=self._timeout)
|
|
57
|
+
raw = cast(io.IOBase, resp)
|
|
58
|
+
name = urllib.parse.urlsplit(self._url).path
|
|
59
|
+
opened = open_compressed(raw, compression=self._compression, name=name)
|
|
60
|
+
self._underlying = raw if opened is not raw else None
|
|
61
|
+
self._f = opened
|
|
62
|
+
return self._f
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def name(self) -> str | None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def url(self) -> str:
|
|
70
|
+
return self._url
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def closed(self) -> bool:
|
|
74
|
+
return self._closed
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
from ..views import unwrap
|
|
4
|
+
from .bytestream_backend import BytestreamBackend
|
|
5
|
+
from .bytestream_like import BytestreamLike
|
|
6
|
+
from .bytestream_view import BytestreamView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BytestreamURLView(BytestreamView, str):
|
|
10
|
+
"""
|
|
11
|
+
A view presenting an underlying data streaming backend via a URL string.
|
|
12
|
+
This view is mostly useful for providing a URL to functions that will open it.
|
|
13
|
+
Note: this view is not lazy (this is impossible for views inheriting str, since str is immutable).
|
|
14
|
+
|
|
15
|
+
Raises TypeError if created with a streaming data source that does not come with a URL.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_backend: BytestreamBackend
|
|
19
|
+
|
|
20
|
+
def __new__(cls, obj: BytestreamLike, **hints: Any) -> Self:
|
|
21
|
+
if isinstance(obj, cls):
|
|
22
|
+
return obj
|
|
23
|
+
backend = cls._prepare_backend(obj, hints)
|
|
24
|
+
url = getattr(backend, "url", None)
|
|
25
|
+
if url is None:
|
|
26
|
+
raise TypeError("This backend cannot be represented as a URL (no underlying URL)")
|
|
27
|
+
instance = super().__new__(cls, url)
|
|
28
|
+
instance._backend = backend
|
|
29
|
+
return instance
|
|
30
|
+
|
|
31
|
+
def __init__(self, obj: BytestreamLike, **hints: Any) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
def unwrap(self) -> Any:
|
|
35
|
+
return unwrap(self._backend)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import ClassVar, Self
|
|
2
|
+
|
|
3
|
+
from ..views import View
|
|
4
|
+
from .bytestream_backend import BytestreamBackend
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BytestreamView(View[BytestreamBackend]):
|
|
8
|
+
"""
|
|
9
|
+
Abstract base class for all views of streaming byte data.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
_backend_base_cls: ClassVar[type[BytestreamBackend]] = BytestreamBackend # type: ignore[type-abstract]
|
|
13
|
+
_view_base_cls: ClassVar[type[Self]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
BytestreamView._view_base_cls = BytestreamView
|