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
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)
|
httk/core/dataloader.py
ADDED
|
@@ -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
|