fsspec-data 0.2.3__cp310-abi3-win_amd64.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.
- fsspec_data/__init__.py +36 -0
- fsspec_data/_version.py +1 -0
- fsspec_data/backends/__init__.py +2 -0
- fsspec_data/backends/base.py +222 -0
- fsspec_data/backends/http.py +4 -0
- fsspec_data/backends/sqlalchemy.py +4 -0
- fsspec_data/filesystem.py +172 -0
- fsspec_data/fsspec_data.pyd +0 -0
- fsspec_data/interchange.py +306 -0
- fsspec_data/tests/__init__.py +0 -0
- fsspec_data/tests/conftest.py +6 -0
- fsspec_data/tests/test_all.py +396 -0
- fsspec_data/tests/test_filesystem.py +210 -0
- fsspec_data-0.2.3.dist-info/METADATA +71 -0
- fsspec_data-0.2.3.dist-info/RECORD +18 -0
- fsspec_data-0.2.3.dist-info/WHEEL +4 -0
- fsspec_data-0.2.3.dist-info/entry_points.txt +2 -0
- fsspec_data-0.2.3.dist-info/licenses/LICENSE +201 -0
fsspec_data/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .filesystem import DataFileSystem
|
|
2
|
+
from .interchange import (
|
|
3
|
+
DEFAULT_REGISTRY,
|
|
4
|
+
Codec,
|
|
5
|
+
CodecCapabilities,
|
|
6
|
+
CodecRegistry,
|
|
7
|
+
DataFormat,
|
|
8
|
+
DecodedBatches,
|
|
9
|
+
DecodedBatchStream,
|
|
10
|
+
FieldMapping,
|
|
11
|
+
InterchangePlan,
|
|
12
|
+
InterchangeRequest,
|
|
13
|
+
PlannedBatchStream,
|
|
14
|
+
SchemaPolicy,
|
|
15
|
+
plan_schema,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "0.2.3"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Codec",
|
|
22
|
+
"CodecCapabilities",
|
|
23
|
+
"CodecRegistry",
|
|
24
|
+
"DEFAULT_REGISTRY",
|
|
25
|
+
"DataFormat",
|
|
26
|
+
"DataFileSystem",
|
|
27
|
+
"DecodedBatchStream",
|
|
28
|
+
"DecodedBatches",
|
|
29
|
+
"FieldMapping",
|
|
30
|
+
"InterchangePlan",
|
|
31
|
+
"InterchangeRequest",
|
|
32
|
+
"PlannedBatchStream",
|
|
33
|
+
"SchemaPolicy",
|
|
34
|
+
"__version__",
|
|
35
|
+
"plan_schema",
|
|
36
|
+
]
|
fsspec_data/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from fsspec import AbstractFileSystem
|
|
2
|
+
from fsspec.spec import AbstractBufferedFile
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseFileSystem(AbstractFileSystem):
|
|
6
|
+
"""
|
|
7
|
+
Docs: https://filesystem-spec.readthedocs.io/en/latest/developer.html#implementing-a-backend
|
|
8
|
+
Minimal: https://github.com/fsspec/filesystem_spec/blob/45dcfa99e2dc320bf072c28bef767b71f8299a4a/fsspec/implementations/github.py#L10
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# def start_transaction(self):
|
|
12
|
+
# def end_transaction(self):
|
|
13
|
+
# def invalidate_cache(self, path=None):
|
|
14
|
+
|
|
15
|
+
def mkdir(self, path, create_parents=True, **kwargs):
|
|
16
|
+
"""Create directory entry at path
|
|
17
|
+
|
|
18
|
+
For systems that don't have true directories, may create an for
|
|
19
|
+
this instance only and not touch the real filesystem
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
path: str
|
|
24
|
+
location
|
|
25
|
+
create_parents: bool
|
|
26
|
+
if True, this is equivalent to ``makedirs``
|
|
27
|
+
kwargs:
|
|
28
|
+
may be permissions, etc.
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def makedirs(self, path, exist_ok=False):
|
|
33
|
+
"""Recursively make directories
|
|
34
|
+
|
|
35
|
+
Creates directory at path and any intervening required directories.
|
|
36
|
+
Raises exception if, for instance, the path already exists but is a
|
|
37
|
+
file.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
path: str
|
|
42
|
+
leaf directory name
|
|
43
|
+
exist_ok: bool (False)
|
|
44
|
+
If False, will error if the target already exists
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def rmdir(self, path):
|
|
49
|
+
"""Remove a directory, if empty"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def ls(self, path, detail=True, **kwargs):
|
|
53
|
+
"""List objects at path.
|
|
54
|
+
|
|
55
|
+
This should include subdirectories and files at that location. The
|
|
56
|
+
difference between a file and a directory must be clear when details
|
|
57
|
+
are requested.
|
|
58
|
+
|
|
59
|
+
The specific keys, or perhaps a FileInfo class, or similar, is TBD,
|
|
60
|
+
but must be consistent across implementations.
|
|
61
|
+
Must include:
|
|
62
|
+
|
|
63
|
+
- full path to the entry (without protocol)
|
|
64
|
+
- size of the entry, in bytes. If the value cannot be determined, will
|
|
65
|
+
be ``None``.
|
|
66
|
+
- type of entry, "file", "directory" or other
|
|
67
|
+
|
|
68
|
+
Additional information
|
|
69
|
+
may be present, aproriate to the file-system, e.g., generation,
|
|
70
|
+
checksum, etc.
|
|
71
|
+
|
|
72
|
+
May use refresh=True|False to allow use of self._ls_from_cache to
|
|
73
|
+
check for a saved listing and avoid calling the backend. This would be
|
|
74
|
+
common where listing may be expensive.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
path: str
|
|
79
|
+
detail: bool
|
|
80
|
+
if True, gives a list of dictionaries, where each is the same as
|
|
81
|
+
the result of ``info(path)``. If False, gives a list of paths
|
|
82
|
+
(str).
|
|
83
|
+
kwargs: may have additional backend-specific options, such as version
|
|
84
|
+
information
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
List of strings if detail is False, or list of directory information
|
|
89
|
+
dicts if detail is True.
|
|
90
|
+
"""
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
def cp_file(self, path1, path2, **kwargs):
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
def _rm(self, path):
|
|
97
|
+
"""Delete one file"""
|
|
98
|
+
# this is the old name for the method, prefer rm_file
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
def created(self, path):
|
|
102
|
+
"""Return the created timestamp of a file as a datetime.datetime"""
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
def modified(self, path):
|
|
106
|
+
"""Return the modified timestamp of a file as a datetime.datetime"""
|
|
107
|
+
raise NotImplementedError
|
|
108
|
+
|
|
109
|
+
# def walk(self, path, maxdepth=None, **kwargs):
|
|
110
|
+
# def find(self, path, maxdepth=None, withdirs=False, **kwargs):
|
|
111
|
+
# def du(self, path, total=True, maxdepth=None, **kwargs):
|
|
112
|
+
# def glob(self, path, **kwargs):
|
|
113
|
+
# def exists(self, path, **kwargs):
|
|
114
|
+
# def lexists(self, path, **kwargs):
|
|
115
|
+
# def info(self, path, **kwargs):
|
|
116
|
+
# def checksum(self, path):
|
|
117
|
+
# def size(self, path):
|
|
118
|
+
# def sizes(self, paths):
|
|
119
|
+
# def isdir(self, path):
|
|
120
|
+
# def isfile(self, path):
|
|
121
|
+
# def cat_file(self, path, start=None, end=None, **kwargs):
|
|
122
|
+
# def pipe_file(self, path, value, **kwargs):
|
|
123
|
+
# def pipe(self, path, value=None, **kwargs):
|
|
124
|
+
# def cat_ranges(self, paths, starts, ends, max_gap=None, **kwargs):
|
|
125
|
+
# def cat(self, path, recursive=False, on_error="raise", **kwargs):
|
|
126
|
+
# def get_file(self, rpath, lpath, callback=_DEFAULT_CALLBACK, **kwargs):
|
|
127
|
+
# def get(self, rpath, lpath, recursive=False, callback=_DEFAULT_CALLBACK, **kwargs):
|
|
128
|
+
# def put_file(self, lpath, rpath, callback=_DEFAULT_CALLBACK, **kwargs):
|
|
129
|
+
# def put(self, lpath, rpath, recursive=False, callback=_DEFAULT_CALLBACK, **kwargs):
|
|
130
|
+
# def head(self, path, size=1024):
|
|
131
|
+
# def tail(self, path, size=1024):
|
|
132
|
+
# def copy(self, path1, path2, recursive=False, on_error=None, **kwargs):
|
|
133
|
+
# def expand_path(self, path, recursive=False, maxdepth=None):
|
|
134
|
+
# def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs):
|
|
135
|
+
# def rm_file(self, path):
|
|
136
|
+
# def rm(self, path, recursive=False, maxdepth=None):
|
|
137
|
+
# def _parent(cls, path):
|
|
138
|
+
# def _open(
|
|
139
|
+
# def open(
|
|
140
|
+
# def touch(self, path, truncate=True, **kwargs):
|
|
141
|
+
# def ukey(self, path):
|
|
142
|
+
# def read_block(self, fn, offset, length, delimiter=None):
|
|
143
|
+
# def to_json(self):
|
|
144
|
+
# def from_json(blob):
|
|
145
|
+
# def get_mapper(self, root="", check=False, create=False, missing_exceptions=None):
|
|
146
|
+
# ------------------------------------------------------------------------
|
|
147
|
+
# Aliases
|
|
148
|
+
# def makedir(self, path, create_parents=True, **kwargs):
|
|
149
|
+
# def mkdirs(self, path, exist_ok=False):
|
|
150
|
+
# def listdir(self, path, detail=True, **kwargs):
|
|
151
|
+
# def cp(self, path1, path2, **kwargs):
|
|
152
|
+
# def move(self, path1, path2, **kwargs):
|
|
153
|
+
# def stat(self, path, **kwargs):
|
|
154
|
+
# def disk_usage(self, path, total=True, maxdepth=None, **kwargs):
|
|
155
|
+
# def rename(self, path1, path2, **kwargs):
|
|
156
|
+
# def delete(self, path, recursive=False, maxdepth=None):
|
|
157
|
+
# def upload(self, lpath, rpath, recursive=False, **kwargs):
|
|
158
|
+
# def download(self, rpath, lpath, recursive=False, **kwargs):
|
|
159
|
+
|
|
160
|
+
def sign(self, path, expiration=100, **kwargs):
|
|
161
|
+
"""Create a signed URL representing the given path
|
|
162
|
+
|
|
163
|
+
Some implementations allow temporary URLs to be generated, as a
|
|
164
|
+
way of delegating credentials.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
path : str
|
|
169
|
+
The path on the filesystem
|
|
170
|
+
expiration : int
|
|
171
|
+
Number of seconds to enable the URL for (if supported)
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
URL : str
|
|
176
|
+
The signed URL
|
|
177
|
+
|
|
178
|
+
Raises
|
|
179
|
+
------
|
|
180
|
+
NotImplementedError : if method is not implemented for a filesystem
|
|
181
|
+
"""
|
|
182
|
+
raise NotImplementedError("Sign is not implemented for this filesystem")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class BaseBufferedFile(AbstractBufferedFile):
|
|
186
|
+
# def details(self):
|
|
187
|
+
# def details(self, value):
|
|
188
|
+
# def full_name(self):
|
|
189
|
+
# def closed(self):
|
|
190
|
+
# def closed(self, c):
|
|
191
|
+
|
|
192
|
+
def commit(self):
|
|
193
|
+
"""Move from temp to final destination"""
|
|
194
|
+
|
|
195
|
+
def discard(self):
|
|
196
|
+
"""Throw away temporary file"""
|
|
197
|
+
|
|
198
|
+
# def info(self):
|
|
199
|
+
# def tell(self):
|
|
200
|
+
# def seek(self, loc, whence=0):
|
|
201
|
+
# def write(self, data):
|
|
202
|
+
# def flush(self, force=False):
|
|
203
|
+
# def _upload_chunk(self, final=False):
|
|
204
|
+
|
|
205
|
+
def _initiate_upload(self):
|
|
206
|
+
"""Create remote file/upload"""
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
def _fetch_range(self, start, end):
|
|
210
|
+
"""Get the specified set of bytes from remote"""
|
|
211
|
+
raise NotImplementedError
|
|
212
|
+
|
|
213
|
+
# def read(self, length=-1):
|
|
214
|
+
# def readinto(self, b):
|
|
215
|
+
# def readuntil(self, char=b"\n", blocks=None):
|
|
216
|
+
# def readline(self):
|
|
217
|
+
# def readlines(self):
|
|
218
|
+
# def readinto1(self, b):
|
|
219
|
+
# def close(self):
|
|
220
|
+
# def readable(self):
|
|
221
|
+
# def seekable(self):
|
|
222
|
+
# def writable(self):
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pyarrow as pa
|
|
8
|
+
from fsspec import AbstractFileSystem, filesystem
|
|
9
|
+
from fsspec.core import url_to_fs
|
|
10
|
+
from fsspec.implementations.chained import ChainedFileSystem
|
|
11
|
+
|
|
12
|
+
from .interchange import DEFAULT_REGISTRY, DataFormat, InterchangeRequest, SchemaPolicy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DataFileSystem(ChainedFileSystem):
|
|
16
|
+
"""Read-only format and schema conversion layered over another filesystem."""
|
|
17
|
+
|
|
18
|
+
protocol = "fsspec-data"
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
fo: str,
|
|
23
|
+
target_protocol: str | None = None,
|
|
24
|
+
target_options: dict[str, Any] | None = None,
|
|
25
|
+
fs: AbstractFileSystem | None = None,
|
|
26
|
+
provided_format: DataFormat | str | None = None,
|
|
27
|
+
requested_format: DataFormat | str | None = None,
|
|
28
|
+
provided_schema: pa.Schema | Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None,
|
|
29
|
+
requested_schema: pa.Schema | Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None,
|
|
30
|
+
schema_policy: SchemaPolicy | str = SchemaPolicy.EXACT,
|
|
31
|
+
batch_size: int = 1024,
|
|
32
|
+
row_limit: int | None = None,
|
|
33
|
+
byte_limit: int | None = None,
|
|
34
|
+
spool_max_size: int = 8 * 1024 * 1024,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__(**kwargs)
|
|
38
|
+
if fs is not None and target_protocol is not None:
|
|
39
|
+
raise ValueError("provide either fs or target_protocol, not both")
|
|
40
|
+
if fs is None:
|
|
41
|
+
if target_protocol is None:
|
|
42
|
+
fs, fo = url_to_fs(fo, **(target_options or {}))
|
|
43
|
+
else:
|
|
44
|
+
fs = filesystem(target_protocol, **(target_options or {}))
|
|
45
|
+
|
|
46
|
+
self.fs = fs
|
|
47
|
+
self.fo = fs._strip_protocol(fo)
|
|
48
|
+
self.provided_format = DataFormat(provided_format) if provided_format is not None else None
|
|
49
|
+
self.requested_format = DataFormat(requested_format) if requested_format is not None else None
|
|
50
|
+
self.provided_schema = _schema_from_options(provided_schema)
|
|
51
|
+
self.requested_schema = _schema_from_options(requested_schema)
|
|
52
|
+
self.schema_policy = SchemaPolicy(schema_policy)
|
|
53
|
+
self.batch_size = batch_size
|
|
54
|
+
self.row_limit = row_limit
|
|
55
|
+
self.byte_limit = byte_limit
|
|
56
|
+
self.spool_max_size = spool_max_size
|
|
57
|
+
self._sizes: dict[str, int] = {}
|
|
58
|
+
|
|
59
|
+
def _open(
|
|
60
|
+
self,
|
|
61
|
+
path: str,
|
|
62
|
+
mode: str = "rb",
|
|
63
|
+
block_size: int | None = None,
|
|
64
|
+
autocommit: bool = True,
|
|
65
|
+
cache_options: dict[str, Any] | None = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
):
|
|
68
|
+
del block_size, autocommit, cache_options, kwargs
|
|
69
|
+
if mode != "rb":
|
|
70
|
+
raise ValueError("fsspec-data is a read-only filesystem")
|
|
71
|
+
|
|
72
|
+
path = self._strip_protocol(path)
|
|
73
|
+
file = tempfile.SpooledTemporaryFile(max_size=self.spool_max_size, mode="w+b")
|
|
74
|
+
try:
|
|
75
|
+
self._convert(path, file)
|
|
76
|
+
self._sizes[path] = file.tell()
|
|
77
|
+
file.seek(0)
|
|
78
|
+
except Exception:
|
|
79
|
+
file.close()
|
|
80
|
+
raise
|
|
81
|
+
return file
|
|
82
|
+
|
|
83
|
+
def info(self, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
84
|
+
del kwargs
|
|
85
|
+
path = self._strip_protocol(path)
|
|
86
|
+
size = self._sizes.get(path)
|
|
87
|
+
if size is None:
|
|
88
|
+
with self._open(path) as file:
|
|
89
|
+
file.seek(0, 2)
|
|
90
|
+
size = file.tell()
|
|
91
|
+
return {"name": path, "size": size, "type": "file"}
|
|
92
|
+
|
|
93
|
+
def ls(self, path: str, detail: bool = True, **kwargs: Any):
|
|
94
|
+
path = self._strip_protocol(path)
|
|
95
|
+
if not path:
|
|
96
|
+
return []
|
|
97
|
+
entry = self.info(path, **kwargs)
|
|
98
|
+
return [entry] if detail else [entry["name"]]
|
|
99
|
+
|
|
100
|
+
def _convert(self, path: str, output) -> None:
|
|
101
|
+
provided_format = self.provided_format or _format_from_path(self.fo)
|
|
102
|
+
requested_format = self.requested_format or _format_from_path(path)
|
|
103
|
+
with self.fs.open(self.fo, "rb") as source:
|
|
104
|
+
decoded = DEFAULT_REGISTRY.get(provided_format).iter_batches(
|
|
105
|
+
source,
|
|
106
|
+
schema=self.provided_schema,
|
|
107
|
+
batch_size=self.batch_size,
|
|
108
|
+
row_limit=self.row_limit,
|
|
109
|
+
byte_limit=self.byte_limit,
|
|
110
|
+
)
|
|
111
|
+
if self.provided_schema is not None and not decoded.schema.equals(self.provided_schema, check_metadata=False):
|
|
112
|
+
raise ValueError("provided schema does not match the source schema")
|
|
113
|
+
|
|
114
|
+
provided_schema = self.provided_schema or decoded.schema
|
|
115
|
+
requested_schema = self.requested_schema or provided_schema
|
|
116
|
+
plan = InterchangeRequest(
|
|
117
|
+
provided_format,
|
|
118
|
+
requested_format,
|
|
119
|
+
provided_schema,
|
|
120
|
+
requested_schema,
|
|
121
|
+
self.schema_policy,
|
|
122
|
+
).plan()
|
|
123
|
+
batches = (plan.apply_batch(batch) for batch in decoded)
|
|
124
|
+
DEFAULT_REGISTRY.get(requested_format).encode_batches_to(batches, output, schema=requested_schema)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_SUFFIX_FORMATS = {
|
|
128
|
+
".arrow": DataFormat.ARROW,
|
|
129
|
+
".csv": DataFormat.CSV,
|
|
130
|
+
".ipc": DataFormat.ARROW,
|
|
131
|
+
".jsonl": DataFormat.JSONL,
|
|
132
|
+
".ndjson": DataFormat.JSONL,
|
|
133
|
+
".parquet": DataFormat.PARQUET,
|
|
134
|
+
".pq": DataFormat.PARQUET,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _format_from_path(path: str) -> DataFormat:
|
|
139
|
+
suffix = "." + path.rsplit(".", 1)[-1].lower() if "." in path.rsplit("/", 1)[-1] else ""
|
|
140
|
+
try:
|
|
141
|
+
return _SUFFIX_FORMATS[suffix]
|
|
142
|
+
except KeyError as error:
|
|
143
|
+
raise ValueError(f"cannot infer tabular format from path {path!r}") from error
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _schema_from_options(
|
|
147
|
+
value: pa.Schema | Mapping[str, Any] | Sequence[Mapping[str, Any]] | None,
|
|
148
|
+
) -> pa.Schema | None:
|
|
149
|
+
if value is None or isinstance(value, pa.Schema):
|
|
150
|
+
return value
|
|
151
|
+
fields = value.get("fields") if isinstance(value, Mapping) else value
|
|
152
|
+
if not isinstance(fields, Sequence) or isinstance(fields, (str, bytes)):
|
|
153
|
+
raise TypeError("schema options must contain a sequence of fields")
|
|
154
|
+
return pa.schema([_field_from_options(field) for field in fields])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _field_from_options(value: Mapping[str, Any]) -> pa.Field:
|
|
158
|
+
if not isinstance(value, Mapping):
|
|
159
|
+
raise TypeError("schema fields must be mappings")
|
|
160
|
+
try:
|
|
161
|
+
name = value["name"]
|
|
162
|
+
data_type = value["type"]
|
|
163
|
+
except KeyError as error:
|
|
164
|
+
raise ValueError(f"schema field is missing {error.args[0]!r}") from error
|
|
165
|
+
if not isinstance(name, str) or not isinstance(data_type, (str, pa.DataType)):
|
|
166
|
+
raise TypeError("schema field name and type must be strings or PyArrow data types")
|
|
167
|
+
if isinstance(data_type, str):
|
|
168
|
+
data_type = pa.type_for_alias(data_type)
|
|
169
|
+
nullable = value.get("nullable", True)
|
|
170
|
+
if not isinstance(nullable, bool):
|
|
171
|
+
raise TypeError("schema field nullable must be a boolean")
|
|
172
|
+
return pa.field(name, data_type, nullable=nullable)
|
|
Binary file
|