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
httk/core/__init__.py ADDED
@@ -0,0 +1,112 @@
1
+ #
2
+ # The high-throughput toolkit (httk)
3
+ # Copyright (C) 2012-2024 the httk AUTHORS
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import pkgutil
19
+
20
+ import httk
21
+
22
+ from . import _discover
23
+ from .dataloader import DataLoader, DataRecord, DatasetMeta, DecodeObjectCallback
24
+ from .datastream import (
25
+ BytestreamBackend,
26
+ BytestreamBytes,
27
+ BytestreamBytesView,
28
+ BytestreamCommon,
29
+ BytestreamFile,
30
+ BytestreamFilename,
31
+ BytestreamFilenameView,
32
+ BytestreamFileView,
33
+ BytestreamLike,
34
+ BytestreamRequest,
35
+ BytestreamRequestView,
36
+ BytestreamURL,
37
+ BytestreamURLView,
38
+ BytestreamView,
39
+ CompressionCodec,
40
+ TextstreamBackend,
41
+ TextstreamCommon,
42
+ TextstreamFile,
43
+ TextstreamFilename,
44
+ TextstreamFilenameView,
45
+ TextstreamFileView,
46
+ TextstreamLike,
47
+ TextstreamRequest,
48
+ TextstreamRequestView,
49
+ TextstreamString,
50
+ TextstreamStringView,
51
+ TextstreamURL,
52
+ TextstreamURLView,
53
+ TextstreamView,
54
+ known_compressions,
55
+ register_compression,
56
+ )
57
+ from .loading import load
58
+ from .views import Backend, View, unwrap
59
+
60
+ _discover.discover_and_register()
61
+
62
+
63
+ def _discover_modules():
64
+ prefix = httk.__name__ + "."
65
+ names = [m.name for m in pkgutil.iter_modules(httk.__path__, prefix) if m.ispkg]
66
+ return names
67
+
68
+
69
+ subpackages = _discover_modules()
70
+
71
+ __all__ = [
72
+ "load",
73
+ "subpackages",
74
+ "DataLoader",
75
+ "DataRecord",
76
+ "DatasetMeta",
77
+ "DecodeObjectCallback",
78
+ "Backend",
79
+ "View",
80
+ "unwrap",
81
+ "BytestreamView",
82
+ "BytestreamFileView",
83
+ "BytestreamFilenameView",
84
+ "BytestreamBytesView",
85
+ "BytestreamRequestView",
86
+ "BytestreamURLView",
87
+ "BytestreamBackend",
88
+ "BytestreamCommon",
89
+ "BytestreamFile",
90
+ "BytestreamFilename",
91
+ "BytestreamBytes",
92
+ "BytestreamRequest",
93
+ "BytestreamURL",
94
+ "BytestreamLike",
95
+ "TextstreamView",
96
+ "TextstreamFileView",
97
+ "TextstreamFilenameView",
98
+ "TextstreamStringView",
99
+ "TextstreamRequestView",
100
+ "TextstreamURLView",
101
+ "TextstreamBackend",
102
+ "TextstreamCommon",
103
+ "TextstreamFile",
104
+ "TextstreamFilename",
105
+ "TextstreamString",
106
+ "TextstreamRequest",
107
+ "TextstreamURL",
108
+ "TextstreamLike",
109
+ "CompressionCodec",
110
+ "register_compression",
111
+ "known_compressions",
112
+ ]
httk/core/_discover.py ADDED
@@ -0,0 +1,35 @@
1
+ #
2
+ # The high-throughput toolkit (httk)
3
+ # Copyright (C) 2012-2024 the httk AUTHORS
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import importlib
19
+ import importlib.util
20
+ import pkgutil
21
+
22
+
23
+ def discover_and_register():
24
+ import httk.handlers
25
+
26
+ prefix = "httk.handlers."
27
+ for m in pkgutil.iter_modules(httk.handlers.__path__, prefix):
28
+ if not m.ispkg:
29
+ continue
30
+
31
+ spec = importlib.util.find_spec(m.name)
32
+ if spec is None:
33
+ continue
34
+
35
+ _mod = importlib.import_module(m.name) # imports only that handler package chain
httk/core/_plugins.py ADDED
@@ -0,0 +1,95 @@
1
+ #
2
+ # The high-throughput toolkit (httk)
3
+ # Copyright (C) 2012-2024 the httk AUTHORS
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ from dataclasses import dataclass
19
+ from importlib import import_module
20
+ from typing import Any, Callable
21
+
22
+ CallableRef = str | Callable[..., Any]
23
+
24
+
25
+ def resolve_callable(ref: CallableRef) -> Callable[..., Any]:
26
+ """
27
+ Resolve a callable reference.
28
+
29
+ Accepts:
30
+ - a callable object
31
+ - a string of form "module.submodule:callable_name"
32
+ """
33
+ if callable(ref):
34
+ return ref
35
+ if not isinstance(ref, str):
36
+ raise TypeError(f"Expected callable or str reference, got {type(ref)!r}")
37
+
38
+ module_name, _, attr = ref.partition(":")
39
+ if not module_name or not attr:
40
+ raise ValueError(f"Invalid reference {ref!r}; expected 'module:callable'.")
41
+
42
+ obj = getattr(import_module(module_name), attr)
43
+ if not callable(obj):
44
+ raise TypeError(f"Resolved {ref!r} to non-callable object {obj!r}")
45
+ return obj
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class PluginSpec:
50
+ """
51
+ A minimal plugin spec.
52
+
53
+ - `key`: selection key (e.g., a file extension like ".cif", or a format name like "cif")
54
+ - `handler`: callable or "module:callable" reference (lazy)
55
+ """
56
+
57
+ key: str
58
+ handler: CallableRef
59
+ name: str | None = None # optional display name
60
+
61
+
62
+ class PluginRegistry:
63
+ """
64
+ Registry mapping keys -> plugin specs.
65
+
66
+ Intended use:
67
+ - loaders: key is file extension ".cif"
68
+ - savers: key is file extension ".cif" or format name
69
+ - show/visualization: key is format/backend name
70
+ """
71
+
72
+ def __init__(self) -> None:
73
+ self._by_key: dict[str, PluginSpec] = {}
74
+
75
+ def register(self, *, key: str, handler: CallableRef, name: str | None = None) -> None:
76
+ k = key
77
+ self._by_key[k] = PluginSpec(key=k, handler=handler, name=name)
78
+
79
+ def keys(self) -> list[str]:
80
+ return sorted(self._by_key.keys())
81
+
82
+ def get(self, key: str) -> PluginSpec | None:
83
+ return self._by_key.get(key)
84
+
85
+ def require(self, key: str) -> PluginSpec:
86
+ spec = self.get(key)
87
+ if spec is None:
88
+ known = ", ".join(self.keys()) or "(none)"
89
+ raise ValueError(f"No plugin registered for {key!r}. Known: {known}")
90
+ return spec
91
+
92
+ def dispatch(self, key: str, *args: Any, **kwargs: Any) -> Any:
93
+ spec = self.require(key)
94
+ fn = resolve_callable(spec.handler)
95
+ return fn(*args, **kwargs)
@@ -0,0 +1,263 @@
1
+ #
2
+ # The high-throughput toolkit (httk)
3
+ # Copyright (C) 2012-2024 the httk AUTHORS
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import json
19
+ from collections.abc import Callable, Iterator, KeysView
20
+ from dataclasses import dataclass
21
+ from functools import cached_property
22
+ from pathlib import Path
23
+ from typing import Any, ClassVar
24
+
25
+ from .datastream import TextstreamFileView, TextstreamLike
26
+ from .datastream.compression import split_compression_suffix
27
+
28
+ type DecodeObjectCallback = Callable[[dict[str, Any], str], Any]
29
+ """Callback invoked as ``(dict_obj, jsonld_url)`` that returns the value to use in place of
30
+ ``dict_obj`` (return the input unchanged to decline)."""
31
+
32
+
33
+ class DataRecord:
34
+ """Read-only attribute and mapping view over a ``dict[str, Any]``.
35
+
36
+ Top-level keys are reachable both as attributes (``record.name``) and as items
37
+ (``record["name"]``); the wrapped values are the plain parsed JSON and are not
38
+ themselves wrapped. Supports iteration over keys, ``len()``, ``in``, and ``keys()``.
39
+ """
40
+
41
+ def __init__(self, data: dict[str, Any]) -> None:
42
+ self._data = data
43
+
44
+ def __getattr__(self, name: str) -> Any:
45
+ # Underscored names are never data keys; failing fast here also avoids recursing
46
+ # into self._data lookups when _data itself is not yet set (copy/unpickling).
47
+ if name.startswith("_"):
48
+ raise AttributeError(name)
49
+ try:
50
+ return self._data[name]
51
+ except KeyError:
52
+ raise AttributeError(name) from None
53
+
54
+ def __getitem__(self, key: str) -> Any:
55
+ return self._data[key]
56
+
57
+ def __iter__(self) -> Iterator[str]:
58
+ return iter(self._data)
59
+
60
+ def __len__(self) -> int:
61
+ return len(self._data)
62
+
63
+ def __contains__(self, key: object) -> bool:
64
+ return key in self._data
65
+
66
+ def keys(self) -> KeysView[str]:
67
+ return self._data.keys()
68
+
69
+ def __repr__(self) -> str:
70
+ return f"DataRecord({self._data!r})"
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class DatasetMeta:
75
+ """Header metadata extracted from a structured JSON-LD dataset document."""
76
+
77
+ context: dict[str, Any]
78
+ """The raw ``@context`` object."""
79
+
80
+ id: str | None
81
+ """The document ``@id``, or ``None`` if absent."""
82
+
83
+ type_: str | None
84
+ """The document ``@type``, or ``None`` if absent (trailing underscore avoids the builtin ``type``)."""
85
+
86
+ header: dict[str, Any]
87
+ """All remaining top-level keys except ``data``, ``indicies``, and ``@``-keys (titles, creator, license, provenance, ...)."""
88
+
89
+ dataset_ids: dict[str, str]
90
+ """Mapping of dataset name to its ``@id``."""
91
+
92
+ fields: dict[str, dict[str, str]]
93
+ """Mapping of dataset name to a mapping of field name to its property URL."""
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class _LoadedData:
98
+ data: Any
99
+ meta: DatasetMeta | None
100
+ index: DataRecord | None
101
+
102
+
103
+ def _build_meta(doc: dict[str, Any]) -> DatasetMeta:
104
+ context = doc.get("@context", {})
105
+ if not isinstance(context, dict):
106
+ context = {}
107
+
108
+ header = {key: value for key, value in doc.items() if key not in ("data", "indicies") and not key.startswith("@")}
109
+
110
+ dataset_ids: dict[str, str] = {}
111
+ fields: dict[str, dict[str, str]] = {}
112
+
113
+ data_context = context.get("data", {})
114
+ nested = data_context.get("@context", {}) if isinstance(data_context, dict) else {}
115
+ if isinstance(nested, dict):
116
+ for dataset_name, spec in nested.items():
117
+ if not isinstance(spec, dict):
118
+ continue
119
+ dataset_id = spec.get("@id")
120
+ if isinstance(dataset_id, str):
121
+ dataset_ids[dataset_name] = dataset_id
122
+ field_context = spec.get("@context")
123
+ if isinstance(field_context, dict):
124
+ field_urls = {field: url for field, url in field_context.items() if isinstance(url, str)}
125
+ if field_urls:
126
+ fields[dataset_name] = field_urls
127
+
128
+ return DatasetMeta(
129
+ context=context,
130
+ id=doc.get("@id"),
131
+ type_=doc.get("@type"),
132
+ header=header,
133
+ dataset_ids=dataset_ids,
134
+ fields=fields,
135
+ )
136
+
137
+
138
+ def _decode_entry(
139
+ entry: dict[str, Any],
140
+ field_urls: dict[str, str],
141
+ dataset_id: str | None,
142
+ decode: DecodeObjectCallback,
143
+ ) -> Any:
144
+ for field_name, url in field_urls.items():
145
+ value = entry.get(field_name)
146
+ if isinstance(value, dict):
147
+ entry[field_name] = decode(value, url)
148
+ if dataset_id is None:
149
+ return entry
150
+ return decode(entry, dataset_id)
151
+
152
+
153
+ def _apply_decode(data: dict[str, Any], meta: DatasetMeta, decode: DecodeObjectCallback) -> None:
154
+ for dataset_name, entries in data.items():
155
+ dataset_id = meta.dataset_ids.get(dataset_name)
156
+ field_urls = meta.fields.get(dataset_name, {})
157
+ if dataset_id is None and not field_urls:
158
+ continue
159
+ if isinstance(entries, dict):
160
+ data[dataset_name] = _decode_entry(entries, field_urls, dataset_id, decode)
161
+ elif isinstance(entries, list):
162
+ data[dataset_name] = [
163
+ _decode_entry(entry, field_urls, dataset_id, decode) if isinstance(entry, dict) else entry
164
+ for entry in entries
165
+ ]
166
+
167
+
168
+ class DataLoader:
169
+ """Lazy loader for httk dataset files, resolved only when data is first accessed.
170
+
171
+ A ``DataLoader`` is a declare-time placeholder: constructing it records its arguments
172
+ and performs no I/O. The source is read the first time ``data``, ``meta``, or ``index``
173
+ is accessed. Files are either plain JSON (any JSON value is exposed as ``data`` with
174
+ ``meta``/``index`` set to ``None``) or a structured JSON-LD document (with ``@context``,
175
+ header fields, ``data``, and optional ``indicies``) whose header is exposed via ``meta``,
176
+ datasets via ``data.<name>``, and lookup indices via ``index.<name>``.
177
+
178
+ Loaders that share an ``identifier`` deduplicate through a class-level registry: the
179
+ first load wins, and later loaders reusing that identifier return the same result while
180
+ their ``source`` and ``decode_object`` arguments are ignored. Keeping identifiers unique
181
+ is the caller's responsibility. Not thread-safe.
182
+
183
+ Format is resolved from the source name after stripping any compression suffix: a ``.json``
184
+ name (e.g. ``data.json`` or ``data.json.gz``) is parsed as JSON; any other recognizable
185
+ suffix raises ``ValueError``; a source with no determinable name is treated as JSON.
186
+ Compression is handled transparently by the stream layer, so ``.json.gz`` and similar load
187
+ directly. A ``str``/``Path`` source is interpreted as a filename unless its scheme marks it
188
+ as a URL (``http``, ``https``, ``ftp``, ``file``); pass ``kind="content"`` for literal
189
+ content or ``kind="filename"``/``kind="url"`` to force an interpretation.
190
+
191
+ Example:
192
+ symmetry_basics = DataLoader("symmetry_basics", "data/spacegroup_symbols.json")
193
+ spacegroups = symmetry_basics.data.spacegroups # first access triggers the load
194
+ """
195
+
196
+ _loaded: ClassVar[dict[str, _LoadedData]] = {}
197
+
198
+ def __init__(
199
+ self,
200
+ identifier: str,
201
+ source: TextstreamLike,
202
+ decode_object: DecodeObjectCallback | None = None,
203
+ **hints: Any,
204
+ ) -> None:
205
+ self._identifier = identifier
206
+ self._source = source
207
+ self._decode_object = decode_object
208
+ self._hints = hints
209
+
210
+ def _resolve_name(self) -> str | None:
211
+ source = self._source
212
+ if isinstance(source, (str, Path)):
213
+ # With kind="content" the string is the data itself, not a name.
214
+ if self._hints.get("kind") == "content":
215
+ return None
216
+ return str(source)
217
+ url = getattr(source, "url", None)
218
+ if isinstance(url, str):
219
+ return url
220
+ name = getattr(source, "name", None)
221
+ if isinstance(name, str):
222
+ return name
223
+ return None
224
+
225
+ def _load(self) -> _LoadedData:
226
+ if self._identifier in DataLoader._loaded:
227
+ return DataLoader._loaded[self._identifier]
228
+
229
+ name = self._resolve_name()
230
+ if name is not None:
231
+ base, _ = split_compression_suffix(name)
232
+ suffix = Path(base).suffix.lower()
233
+ if suffix not in ("", ".json"):
234
+ raise ValueError(f"unsupported data format: {suffix}")
235
+
236
+ doc = json.load(TextstreamFileView(self._source, **self._hints))
237
+
238
+ if isinstance(doc, dict) and "@context" in doc:
239
+ meta = _build_meta(doc)
240
+ data_section = doc.get("data")
241
+ if self._decode_object is not None and isinstance(data_section, dict):
242
+ _apply_decode(data_section, meta, self._decode_object)
243
+ data: Any = DataRecord(data_section) if isinstance(data_section, dict) else data_section
244
+ index_section = doc.get("indicies")
245
+ index = DataRecord(index_section) if isinstance(index_section, dict) else None
246
+ loaded = _LoadedData(data=data, meta=meta, index=index)
247
+ else:
248
+ loaded = _LoadedData(data=doc, meta=None, index=None)
249
+
250
+ DataLoader._loaded[self._identifier] = loaded
251
+ return loaded
252
+
253
+ @cached_property
254
+ def data(self) -> Any:
255
+ return self._load().data
256
+
257
+ @cached_property
258
+ def meta(self) -> DatasetMeta | None:
259
+ return self._load().meta
260
+
261
+ @cached_property
262
+ def index(self) -> DataRecord | None:
263
+ return self._load().index
@@ -0,0 +1,80 @@
1
+ from .bytestream_backend import BytestreamBackend
2
+ from .bytestream_bytes import BytestreamBytes
3
+ from .bytestream_bytes_view import BytestreamBytesView
4
+ from .bytestream_common import BytestreamCommon
5
+ from .bytestream_file import BytestreamFile
6
+ from .bytestream_file_view import BytestreamFileView
7
+ from .bytestream_filename import BytestreamFilename
8
+ from .bytestream_filename_view import BytestreamFilenameView
9
+ from .bytestream_like import BytestreamLike
10
+ from .bytestream_request import BytestreamRequest
11
+ from .bytestream_request_view import BytestreamRequestView
12
+ from .bytestream_url import BytestreamURL
13
+ from .bytestream_url_view import BytestreamURLView
14
+ from .bytestream_view import BytestreamView
15
+ from .compression import CompressionCodec, known_compressions, register_compression
16
+ from .textstream_backend import TextstreamBackend
17
+ from .textstream_common import TextstreamCommon
18
+ from .textstream_file import TextstreamFile
19
+ from .textstream_file_view import TextstreamFileView
20
+ from .textstream_filename import TextstreamFilename
21
+ from .textstream_filename_view import TextstreamFilenameView
22
+ from .textstream_like import TextstreamLike
23
+ from .textstream_request import TextstreamRequest
24
+ from .textstream_request_view import TextstreamRequestView
25
+ from .textstream_string import TextstreamString
26
+ from .textstream_string_view import TextstreamStringView
27
+ from .textstream_url import TextstreamURL
28
+ from .textstream_url_view import TextstreamURLView
29
+ from .textstream_view import TextstreamView
30
+
31
+ # *URL backends precede *Filename/*String so a bare scheme'd string dispatches to a URL;
32
+ # an explicit kind= hint still forces the intended interpretation either way.
33
+ BytestreamBackend.backend_classes = [
34
+ BytestreamFile,
35
+ BytestreamRequest,
36
+ BytestreamURL,
37
+ BytestreamFilename,
38
+ BytestreamBytes,
39
+ ]
40
+ TextstreamBackend.backend_classes = [
41
+ TextstreamFile,
42
+ TextstreamRequest,
43
+ TextstreamURL,
44
+ TextstreamFilename,
45
+ TextstreamString,
46
+ ]
47
+
48
+ __all__ = [
49
+ "BytestreamView",
50
+ "BytestreamFileView",
51
+ "BytestreamFilenameView",
52
+ "BytestreamBytesView",
53
+ "BytestreamRequestView",
54
+ "BytestreamURLView",
55
+ "BytestreamBackend",
56
+ "BytestreamCommon",
57
+ "BytestreamFile",
58
+ "BytestreamFilename",
59
+ "BytestreamBytes",
60
+ "BytestreamRequest",
61
+ "BytestreamURL",
62
+ "BytestreamLike",
63
+ "TextstreamView",
64
+ "TextstreamFileView",
65
+ "TextstreamFilenameView",
66
+ "TextstreamStringView",
67
+ "TextstreamRequestView",
68
+ "TextstreamURLView",
69
+ "TextstreamBackend",
70
+ "TextstreamCommon",
71
+ "TextstreamFile",
72
+ "TextstreamFilename",
73
+ "TextstreamString",
74
+ "TextstreamRequest",
75
+ "TextstreamURL",
76
+ "TextstreamLike",
77
+ "CompressionCodec",
78
+ "register_compression",
79
+ "known_compressions",
80
+ ]
@@ -0,0 +1,33 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BytestreamAPI(ABC):
5
+ """
6
+ Abstract base class for a bare minimum API for streamable byte data.
7
+
8
+ Supports:
9
+ * read, close, name, and closed with the meanings defined by io.IOBase
10
+
11
+ Since it is a *minimal* streaming data API it deliberately omits:
12
+ seek, tell, etc.; there should be no assumption that the underlying
13
+ data source is seekable. However, many backend implementations
14
+ may choose to support them.
15
+ """
16
+
17
+ @abstractmethod
18
+ def read(self, size: int = -1) -> bytes:
19
+ raise NotImplementedError
20
+
21
+ @abstractmethod
22
+ def close(self) -> None:
23
+ raise NotImplementedError
24
+
25
+ @property
26
+ @abstractmethod
27
+ def name(self) -> str | None:
28
+ raise NotImplementedError
29
+
30
+ @property
31
+ @abstractmethod
32
+ def closed(self) -> bool:
33
+ raise NotImplementedError
@@ -0,0 +1,12 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from ..views import Backend
4
+ from .bytestream_api import BytestreamAPI
5
+
6
+
7
+ class BytestreamBackend(Backend["BytestreamBackend"], BytestreamAPI):
8
+ """
9
+ Abstract base class for all backends of streaming byte data.
10
+ """
11
+
12
+ backend_classes: ClassVar[list[type[Backend[Any]]]]
@@ -0,0 +1,52 @@
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 BytestreamBytes(BytestreamCommon, BytestreamBackend):
10
+ """
11
+ Backend for streaming byte data backed by an actual bytes object.
12
+ """
13
+
14
+ b: bytes
15
+ _compression: str
16
+ _f: io.IOBase | None
17
+ _underlying: io.IOBase | None
18
+ _closed: bool
19
+
20
+ # Cannot type annotate __new__ as `Self | None` for some reason
21
+ def __new__(cls, content: bytes | bytearray, **hints: Any) -> Any:
22
+ if not isinstance(content, bytes | bytearray):
23
+ return None
24
+ if hints and hints.get("kind", "content") != "content":
25
+ return None
26
+ return super().__new__(cls)
27
+
28
+ def __init__(self, content: bytes | bytearray, **hints: Any) -> None:
29
+ self.b = bytes(content)
30
+ self._compression = hints.get("compression", "auto")
31
+ validate_compression(self._compression)
32
+ self._f = None
33
+ self._underlying = None
34
+ self._closed = False
35
+
36
+ def _ensure_f(self) -> io.IOBase:
37
+ if self._closed:
38
+ raise ValueError("I/O operation on closed stream")
39
+ if self._f is None:
40
+ raw: io.IOBase = io.BytesIO(self.b)
41
+ opened = open_compressed(raw, compression=self._compression, name=None)
42
+ self._underlying = raw if opened is not raw else None
43
+ self._f = opened
44
+ return self._f
45
+
46
+ @property
47
+ def name(self) -> str | None:
48
+ return None
49
+
50
+ @property
51
+ def closed(self) -> bool:
52
+ return self._closed