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.
Files changed (49) hide show
  1. httk/core/__init__.py +112 -0
  2. httk/core/_discover.py +35 -0
  3. httk/core/_plugins.py +95 -0
  4. httk/core/dataloader.py +263 -0
  5. httk/core/datastream/__init__.py +80 -0
  6. httk/core/datastream/bytestream_api.py +33 -0
  7. httk/core/datastream/bytestream_backend.py +12 -0
  8. httk/core/datastream/bytestream_bytes.py +52 -0
  9. httk/core/datastream/bytestream_bytes_view.py +31 -0
  10. httk/core/datastream/bytestream_common.py +38 -0
  11. httk/core/datastream/bytestream_file.py +53 -0
  12. httk/core/datastream/bytestream_file_view.py +189 -0
  13. httk/core/datastream/bytestream_filename.py +53 -0
  14. httk/core/datastream/bytestream_filename_view.py +34 -0
  15. httk/core/datastream/bytestream_like.py +17 -0
  16. httk/core/datastream/bytestream_request.py +69 -0
  17. httk/core/datastream/bytestream_request_view.py +44 -0
  18. httk/core/datastream/bytestream_url.py +74 -0
  19. httk/core/datastream/bytestream_url_view.py +35 -0
  20. httk/core/datastream/bytestream_view.py +16 -0
  21. httk/core/datastream/compression.py +183 -0
  22. httk/core/datastream/textstream_api.py +33 -0
  23. httk/core/datastream/textstream_backend.py +12 -0
  24. httk/core/datastream/textstream_common.py +37 -0
  25. httk/core/datastream/textstream_file.py +45 -0
  26. httk/core/datastream/textstream_file_view.py +201 -0
  27. httk/core/datastream/textstream_filename.py +55 -0
  28. httk/core/datastream/textstream_filename_view.py +35 -0
  29. httk/core/datastream/textstream_like.py +15 -0
  30. httk/core/datastream/textstream_request.py +72 -0
  31. httk/core/datastream/textstream_request_view.py +44 -0
  32. httk/core/datastream/textstream_string.py +47 -0
  33. httk/core/datastream/textstream_string_view.py +31 -0
  34. httk/core/datastream/textstream_url.py +77 -0
  35. httk/core/datastream/textstream_url_view.py +35 -0
  36. httk/core/datastream/textstream_view.py +16 -0
  37. httk/core/loading.py +29 -0
  38. httk/core/py.typed +0 -0
  39. httk/core/register.py +30 -0
  40. httk/core/views/__init__.py +5 -0
  41. httk/core/views/backend.py +49 -0
  42. httk/core/views/unwrapping.py +12 -0
  43. httk/core/views/view.py +56 -0
  44. httk/handlers/core/__init__.py +0 -0
  45. httk_core-2.0.0.dist-info/METADATA +76 -0
  46. httk_core-2.0.0.dist-info/RECORD +49 -0
  47. httk_core-2.0.0.dist-info/WHEEL +5 -0
  48. httk_core-2.0.0.dist-info/licenses/LICENSE +661 -0
  49. 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