chore: import upstream snapshot with attribution
Build documentation / build (push) Failing after 0s
CI / check_code_quality (push) Has been cancelled
CI / test (deps-latest, ubuntu-latest, integration) (push) Has been cancelled
CI / test (deps-latest, ubuntu-latest, unit) (push) Has been cancelled
CI / test (deps-latest, windows-latest, integration) (push) Has been cancelled
CI / test (deps-latest, windows-latest, unit) (push) Has been cancelled
CI / test (deps-minimum, ubuntu-latest, integration) (push) Has been cancelled
CI / test (deps-minimum, ubuntu-latest, unit) (push) Has been cancelled
CI / test (deps-minimum, windows-latest, integration) (push) Has been cancelled
CI / test (deps-minimum, windows-latest, unit) (push) Has been cancelled
CI / test_py314 (deps-latest, ubuntu-latest, unit) (push) Has been cancelled
CI / test_py314 (deps-latest, windows-latest, unit) (push) Has been cancelled
CI / test_py314_future (deps-latest, ubuntu-latest, unit) (push) Has been cancelled
CI / test_py314_future (deps-latest, windows-latest, unit) (push) Has been cancelled
Secret Leaks / trufflehog (push) Has been cancelled
Build documentation / build (push) Failing after 0s
CI / check_code_quality (push) Has been cancelled
CI / test (deps-latest, ubuntu-latest, integration) (push) Has been cancelled
CI / test (deps-latest, ubuntu-latest, unit) (push) Has been cancelled
CI / test (deps-latest, windows-latest, integration) (push) Has been cancelled
CI / test (deps-latest, windows-latest, unit) (push) Has been cancelled
CI / test (deps-minimum, ubuntu-latest, integration) (push) Has been cancelled
CI / test (deps-minimum, ubuntu-latest, unit) (push) Has been cancelled
CI / test (deps-minimum, windows-latest, integration) (push) Has been cancelled
CI / test (deps-minimum, windows-latest, unit) (push) Has been cancelled
CI / test_py314 (deps-latest, ubuntu-latest, unit) (push) Has been cancelled
CI / test_py314 (deps-latest, windows-latest, unit) (push) Has been cancelled
CI / test_py314_future (deps-latest, ubuntu-latest, unit) (push) Has been cancelled
CI / test_py314_future (deps-latest, windows-latest, unit) (push) Has been cancelled
Secret Leaks / trufflehog (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors and the TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "5.0.1.dev0"
|
||||
|
||||
from .arrow_dataset import Column, Dataset
|
||||
from .arrow_reader import ReadInstruction
|
||||
from .builder import ArrowBasedBuilder, BuilderConfig, DatasetBuilder, GeneratorBasedBuilder
|
||||
from .combine import concatenate_datasets, interleave_datasets
|
||||
from .dataset_dict import DatasetDict, IterableDatasetDict
|
||||
from .download import *
|
||||
from .features import *
|
||||
from .fingerprint import disable_caching, enable_caching, is_caching_enabled
|
||||
from .info import DatasetInfo
|
||||
from .inspect import (
|
||||
get_dataset_config_info,
|
||||
get_dataset_config_names,
|
||||
get_dataset_default_config_name,
|
||||
get_dataset_infos,
|
||||
get_dataset_split_names,
|
||||
)
|
||||
from .iterable_dataset import IterableColumn, IterableDataset
|
||||
from .load import load_dataset, load_dataset_builder, load_from_disk
|
||||
from .splits import (
|
||||
NamedSplit,
|
||||
NamedSplitAll,
|
||||
Split,
|
||||
SplitBase,
|
||||
SplitDict,
|
||||
SplitGenerator,
|
||||
SplitInfo,
|
||||
SubSplitInfo,
|
||||
percent,
|
||||
)
|
||||
from .utils import *
|
||||
from .utils import logging
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,620 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors and the TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
"""Arrow ArrowReader."""
|
||||
|
||||
import copy
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
from tqdm.contrib.concurrent import thread_map
|
||||
|
||||
from .download.download_config import DownloadConfig # noqa: F401
|
||||
from .naming import _split_re, filenames_for_dataset_split
|
||||
from .table import InMemoryTable, MemoryMappedTable, Table, concat_tables
|
||||
from .utils import logging
|
||||
from .utils import tqdm as hf_tqdm
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .info import DatasetInfo # noqa: F401
|
||||
from .splits import Split, SplitInfo # noqa: F401
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
HF_GCP_BASE_URL = "https://storage.googleapis.com/huggingface-nlp/cache/datasets"
|
||||
|
||||
_SUB_SPEC_RE = re.compile(
|
||||
rf"""
|
||||
^
|
||||
(?P<split>{_split_re[1:-1]})
|
||||
(\[
|
||||
((?P<from>-?[\d_]+)
|
||||
(?P<from_pct>%)?)?
|
||||
:
|
||||
((?P<to>-?[\d_]+)
|
||||
(?P<to_pct>%)?)?
|
||||
\])?(\((?P<rounding>[^\)]*)\))?
|
||||
$
|
||||
""", # remove ^ and $
|
||||
re.X,
|
||||
)
|
||||
|
||||
_ADDITION_SEP_RE = re.compile(r"\s*\+\s*")
|
||||
|
||||
|
||||
class DatasetNotOnHfGcsError(ConnectionError):
|
||||
"""When you can't get the dataset from the Hf google cloud storage"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingFilesOnHfGcsError(ConnectionError):
|
||||
"""When some files are missing on the Hf oogle cloud storage"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileInstructions:
|
||||
"""The file instructions associated with a split ReadInstruction.
|
||||
|
||||
Attributes:
|
||||
num_examples: `int`, The total number of examples
|
||||
file_instructions: List[dict(filename, skip, take)], the files information.
|
||||
The filenames contains the relative path, not absolute.
|
||||
skip/take indicates which example read in the file: `ds.slice(skip, take)`
|
||||
"""
|
||||
|
||||
num_examples: int
|
||||
file_instructions: list[dict]
|
||||
|
||||
|
||||
def make_file_instructions(
|
||||
name: str,
|
||||
split_infos: list["SplitInfo"],
|
||||
instruction: Union[str, "ReadInstruction"],
|
||||
filetype_suffix: Optional[str] = None,
|
||||
prefix_path: Optional[str] = None,
|
||||
) -> FileInstructions:
|
||||
"""Returns instructions of the split dict.
|
||||
|
||||
Args:
|
||||
name (`str`): Name of the dataset.
|
||||
split_infos (`list` of `[SplitInfo]`): Dataset splits information.
|
||||
instruction ([`ReadInstruction`] or `str`): Reading instruction for a dataset.
|
||||
filetype_suffix (`str`, *optional*): Suffix of dataset files, e.g. 'arrow' or 'parquet'.
|
||||
prefix_path (`str`, *optional*): Prefix of dataset files, e.g. directory name.
|
||||
|
||||
Returns:
|
||||
[`FileInstructions`]
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
raise TypeError(f"Expected str 'name', but got: {type(name).__name__}")
|
||||
elif not name:
|
||||
raise ValueError("Expected non-empty str 'name'")
|
||||
name2len = {info.name: info.num_examples for info in split_infos}
|
||||
name2shard_lengths = {info.name: info.shard_lengths for info in split_infos}
|
||||
name2filenames = {
|
||||
info.name: filenames_for_dataset_split(
|
||||
path=prefix_path,
|
||||
dataset_name=name,
|
||||
split=info.name,
|
||||
filetype_suffix=filetype_suffix,
|
||||
shard_lengths=name2shard_lengths[info.name],
|
||||
)
|
||||
for info in split_infos
|
||||
}
|
||||
if not isinstance(instruction, ReadInstruction):
|
||||
instruction = ReadInstruction.from_spec(instruction)
|
||||
# Create the absolute instruction (per split)
|
||||
absolute_instructions = instruction.to_absolute(name2len)
|
||||
|
||||
# For each split, return the files instruction (skip/take)
|
||||
file_instructions = []
|
||||
num_examples = 0
|
||||
for abs_instr in absolute_instructions:
|
||||
split_length = name2len[abs_instr.splitname]
|
||||
filenames = name2filenames[abs_instr.splitname]
|
||||
shard_lengths = name2shard_lengths[abs_instr.splitname]
|
||||
from_ = 0 if abs_instr.from_ is None else abs_instr.from_
|
||||
to = split_length if abs_instr.to is None else abs_instr.to
|
||||
if shard_lengths is None: # not sharded
|
||||
for filename in filenames:
|
||||
take = to - from_
|
||||
if take == 0:
|
||||
continue
|
||||
num_examples += take
|
||||
file_instructions.append({"filename": filename, "skip": from_, "take": take})
|
||||
else: # sharded
|
||||
index_start = 0 # Beginning (included) of moving window.
|
||||
index_end = 0 # End (excluded) of moving window.
|
||||
for filename, shard_length in zip(filenames, shard_lengths):
|
||||
index_end += shard_length
|
||||
if from_ < index_end and to > index_start: # There is something to take.
|
||||
skip = from_ - index_start if from_ > index_start else 0
|
||||
take = to - index_start - skip if to < index_end else -1
|
||||
if take == 0:
|
||||
continue
|
||||
file_instructions.append({"filename": filename, "skip": skip, "take": take})
|
||||
num_examples += shard_length - skip if take == -1 else take
|
||||
index_start += shard_length
|
||||
return FileInstructions(
|
||||
num_examples=num_examples,
|
||||
file_instructions=file_instructions,
|
||||
)
|
||||
|
||||
|
||||
class BaseReader:
|
||||
"""
|
||||
Build a Dataset object out of Instruction instance(s).
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, info: Optional["DatasetInfo"]):
|
||||
"""Initializes ArrowReader.
|
||||
|
||||
Args:
|
||||
path (str): path where tfrecords are stored.
|
||||
info (DatasetInfo): info about the dataset.
|
||||
"""
|
||||
self._path: str = path
|
||||
self._info: Optional["DatasetInfo"] = info
|
||||
self._filetype_suffix: Optional[str] = None
|
||||
|
||||
def _get_table_from_filename(self, filename_skip_take, in_memory=False) -> Table:
|
||||
"""Returns a Dataset instance from given (filename, skip, take)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _read_files(self, files, in_memory=False) -> Table:
|
||||
"""Returns Dataset for given file instructions.
|
||||
|
||||
Args:
|
||||
files: List[dict(filename, skip, take)], the files information.
|
||||
The filenames contain the absolute path, not relative.
|
||||
skip/take indicates which example read in the file: `ds.slice(skip, take)`
|
||||
in_memory (bool, default False): Whether to copy the data in-memory.
|
||||
"""
|
||||
if len(files) == 0 or not all(isinstance(f, dict) for f in files):
|
||||
raise ValueError("please provide valid file informations")
|
||||
files = copy.deepcopy(files)
|
||||
for f in files:
|
||||
f["filename"] = os.path.join(self._path, f["filename"])
|
||||
|
||||
pa_tables = thread_map(
|
||||
partial(self._get_table_from_filename, in_memory=in_memory),
|
||||
files,
|
||||
tqdm_class=hf_tqdm,
|
||||
desc="Loading dataset shards",
|
||||
# set `disable=None` rather than `disable=False` by default to disable progress bar when no TTY attached
|
||||
disable=len(files) <= 16 or None,
|
||||
)
|
||||
pa_tables = [t for t in pa_tables if len(t) > 0]
|
||||
if not pa_tables and (self._info is None or self._info.features is None):
|
||||
raise ValueError(
|
||||
"Tried to read an empty table. Please specify at least info.features to create an empty table with the right type."
|
||||
)
|
||||
pa_tables = pa_tables or [InMemoryTable.from_batches([], schema=pa.schema(self._info.features.type))]
|
||||
pa_table = concat_tables(pa_tables) if len(pa_tables) != 1 else pa_tables[0]
|
||||
return pa_table
|
||||
|
||||
def get_file_instructions(self, name, instruction, split_infos):
|
||||
"""Return list of dict {'filename': str, 'skip': int, 'take': int}"""
|
||||
file_instructions = make_file_instructions(
|
||||
name, split_infos, instruction, filetype_suffix=self._filetype_suffix, prefix_path=self._path
|
||||
)
|
||||
files = file_instructions.file_instructions
|
||||
return files
|
||||
|
||||
def read(
|
||||
self,
|
||||
name,
|
||||
instructions,
|
||||
split_infos,
|
||||
in_memory=False,
|
||||
):
|
||||
"""Returns Dataset instance(s).
|
||||
|
||||
Args:
|
||||
name (str): name of the dataset.
|
||||
instructions (ReadInstruction): instructions to read.
|
||||
Instruction can be string and will then be passed to the Instruction
|
||||
constructor as it.
|
||||
split_infos (list of SplitInfo proto): the available splits for dataset.
|
||||
in_memory (bool, default False): Whether to copy the data in-memory.
|
||||
|
||||
Returns:
|
||||
kwargs to build a single Dataset instance.
|
||||
"""
|
||||
|
||||
files = self.get_file_instructions(name, instructions, split_infos)
|
||||
if not files:
|
||||
msg = f'Instruction "{instructions}" corresponds to no data!'
|
||||
raise ValueError(msg)
|
||||
return self.read_files(files=files, original_instructions=instructions, in_memory=in_memory)
|
||||
|
||||
def read_files(
|
||||
self,
|
||||
files: list[dict],
|
||||
original_instructions: Union[None, "ReadInstruction", "Split"] = None,
|
||||
in_memory=False,
|
||||
):
|
||||
"""Returns single Dataset instance for the set of file instructions.
|
||||
|
||||
Args:
|
||||
files: List[dict(filename, skip, take)], the files information.
|
||||
The filenames contains the relative path, not absolute.
|
||||
skip/take indicates which example read in the file: `ds.skip().take()`
|
||||
original_instructions: store the original instructions used to build the dataset split in the dataset.
|
||||
in_memory (bool, default False): Whether to copy the data in-memory.
|
||||
|
||||
Returns:
|
||||
kwargs to build a Dataset instance.
|
||||
"""
|
||||
# Prepend path to filename
|
||||
pa_table = self._read_files(files, in_memory=in_memory)
|
||||
# If original_instructions is not None, convert it to a human-readable NamedSplit
|
||||
if original_instructions is not None:
|
||||
from .splits import Split # noqa
|
||||
|
||||
split = Split(str(original_instructions))
|
||||
else:
|
||||
split = None
|
||||
dataset_kwargs = {"arrow_table": pa_table, "info": self._info, "split": split}
|
||||
return dataset_kwargs
|
||||
|
||||
|
||||
class ArrowReader(BaseReader):
|
||||
"""
|
||||
Build a Dataset object out of Instruction instance(s).
|
||||
This Reader uses either memory mapping or file descriptors (in-memory) on arrow files.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, info: Optional["DatasetInfo"]):
|
||||
"""Initializes ArrowReader.
|
||||
|
||||
Args:
|
||||
path (str): path where Arrow files are stored.
|
||||
info (DatasetInfo): info about the dataset.
|
||||
"""
|
||||
super().__init__(path, info)
|
||||
self._filetype_suffix = "arrow"
|
||||
|
||||
def _get_table_from_filename(self, filename_skip_take, in_memory=False) -> Table:
|
||||
"""Returns a Dataset instance from given (filename, skip, take)."""
|
||||
filename, skip, take = (
|
||||
filename_skip_take["filename"],
|
||||
filename_skip_take["skip"] if "skip" in filename_skip_take else None,
|
||||
filename_skip_take["take"] if "take" in filename_skip_take else None,
|
||||
)
|
||||
table = ArrowReader.read_table(filename, in_memory=in_memory)
|
||||
if take == -1:
|
||||
take = len(table) - skip
|
||||
# here we don't want to slice an empty table, or it may segfault
|
||||
if skip is not None and take is not None and not (skip == 0 and take == len(table)):
|
||||
table = table.slice(skip, take)
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def read_table(filename, in_memory=False) -> Table:
|
||||
"""
|
||||
Read table from file.
|
||||
|
||||
Args:
|
||||
filename (str): File name of the table.
|
||||
in_memory (bool, default=False): Whether to copy the data in-memory.
|
||||
|
||||
Returns:
|
||||
pyarrow.Table
|
||||
"""
|
||||
table_cls = InMemoryTable if in_memory else MemoryMappedTable
|
||||
return table_cls.from_file(filename)
|
||||
|
||||
|
||||
class ParquetReader(BaseReader):
|
||||
"""
|
||||
Build a Dataset object out of Instruction instance(s).
|
||||
This Reader uses memory mapping on parquet files.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, info: Optional["DatasetInfo"]):
|
||||
"""Initializes ParquetReader.
|
||||
|
||||
Args:
|
||||
path (str): path where tfrecords are stored.
|
||||
info (DatasetInfo): info about the dataset.
|
||||
"""
|
||||
super().__init__(path, info)
|
||||
self._filetype_suffix = "parquet"
|
||||
|
||||
def _get_table_from_filename(self, filename_skip_take, **kwargs):
|
||||
"""Returns a Dataset instance from given (filename, skip, take)."""
|
||||
filename, skip, take = (
|
||||
filename_skip_take["filename"],
|
||||
filename_skip_take["skip"] if "skip" in filename_skip_take else None,
|
||||
filename_skip_take["take"] if "take" in filename_skip_take else None,
|
||||
)
|
||||
# Parquet read_table always loads data in memory, independently of memory_map
|
||||
pa_table = pq.read_table(filename, memory_map=True)
|
||||
# here we don't want to slice an empty table, or it may segfault
|
||||
if skip is not None and take is not None and not (skip == 0 and take == len(pa_table)):
|
||||
pa_table = pa_table.slice(skip, take)
|
||||
return pa_table
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _AbsoluteInstruction:
|
||||
"""A machine friendly slice: defined absolute positive boundaries."""
|
||||
|
||||
splitname: str
|
||||
from_: int # uint (starting index).
|
||||
to: int # uint (ending index).
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _RelativeInstruction:
|
||||
"""Represents a single parsed slicing instruction, can use % and negatives."""
|
||||
|
||||
splitname: str
|
||||
from_: Optional[int] = None # int (starting index) or None if no lower boundary.
|
||||
to: Optional[int] = None # int (ending index) or None if no upper boundary.
|
||||
unit: Optional[str] = None
|
||||
rounding: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.unit is not None and self.unit not in ["%", "abs"]:
|
||||
raise ValueError("unit must be either % or abs")
|
||||
if self.rounding is not None and self.rounding not in ["closest", "pct1_dropremainder"]:
|
||||
raise ValueError("rounding must be either closest or pct1_dropremainder")
|
||||
if self.unit != "%" and self.rounding is not None:
|
||||
raise ValueError("It is forbidden to specify rounding if not using percent slicing.")
|
||||
if self.unit == "%" and self.from_ is not None and abs(self.from_) > 100:
|
||||
raise ValueError("Percent slice boundaries must be > -100 and < 100.")
|
||||
if self.unit == "%" and self.to is not None and abs(self.to) > 100:
|
||||
raise ValueError("Percent slice boundaries must be > -100 and < 100.")
|
||||
# Update via __dict__ due to instance being "frozen"
|
||||
self.__dict__["rounding"] = "closest" if self.rounding is None and self.unit == "%" else self.rounding
|
||||
|
||||
|
||||
def _str_to_read_instruction(spec):
|
||||
"""Returns ReadInstruction for given string."""
|
||||
res = _SUB_SPEC_RE.match(spec)
|
||||
if not res:
|
||||
raise ValueError(f"Unrecognized instruction format: {spec}")
|
||||
unit = "%" if res.group("from_pct") or res.group("to_pct") else "abs"
|
||||
return ReadInstruction(
|
||||
split_name=res.group("split"),
|
||||
rounding=res.group("rounding"),
|
||||
from_=int(res.group("from")) if res.group("from") else None,
|
||||
to=int(res.group("to")) if res.group("to") else None,
|
||||
unit=unit,
|
||||
)
|
||||
|
||||
|
||||
def _pct_to_abs_pct1(boundary, num_examples):
|
||||
# Using math.trunc here, since -99.5% should give -99%, not -100%.
|
||||
if num_examples < 100:
|
||||
msg = (
|
||||
'Using "pct1_dropremainder" rounding on a split with less than 100 '
|
||||
"elements is forbidden: it always results in an empty dataset."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return boundary * math.trunc(num_examples / 100.0)
|
||||
|
||||
|
||||
def _pct_to_abs_closest(boundary, num_examples):
|
||||
return int(round(boundary * num_examples / 100.0))
|
||||
|
||||
|
||||
def _rel_to_abs_instr(rel_instr, name2len):
|
||||
"""Returns _AbsoluteInstruction instance for given RelativeInstruction.
|
||||
|
||||
Args:
|
||||
rel_instr: RelativeInstruction instance.
|
||||
name2len: dict {split_name: num_examples}.
|
||||
"""
|
||||
pct_to_abs = _pct_to_abs_closest if rel_instr.rounding == "closest" else _pct_to_abs_pct1
|
||||
split = rel_instr.splitname
|
||||
if split not in name2len:
|
||||
raise ValueError(f'Unknown split "{split}". Should be one of {list(name2len)}.')
|
||||
num_examples = name2len[split]
|
||||
from_ = rel_instr.from_
|
||||
to = rel_instr.to
|
||||
if rel_instr.unit == "%":
|
||||
from_ = 0 if from_ is None else pct_to_abs(from_, num_examples)
|
||||
to = num_examples if to is None else pct_to_abs(to, num_examples)
|
||||
else:
|
||||
from_ = 0 if from_ is None else from_
|
||||
to = num_examples if to is None else to
|
||||
if from_ < 0:
|
||||
from_ = max(num_examples + from_, 0)
|
||||
if to < 0:
|
||||
to = max(num_examples + to, 0)
|
||||
from_ = min(from_, num_examples)
|
||||
to = min(to, num_examples)
|
||||
return _AbsoluteInstruction(split, from_, to)
|
||||
|
||||
|
||||
class ReadInstruction:
|
||||
"""Reading instruction for a dataset.
|
||||
|
||||
Examples::
|
||||
|
||||
# The following lines are equivalent:
|
||||
ds = datasets.load_dataset('ylecun/mnist', split='test[:33%]')
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=datasets.ReadInstruction.from_spec('test[:33%]'))
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=datasets.ReadInstruction('test', to=33, unit='%'))
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=datasets.ReadInstruction(
|
||||
'test', from_=0, to=33, unit='%'))
|
||||
|
||||
# The following lines are equivalent:
|
||||
ds = datasets.load_dataset('ylecun/mnist', split='test[:33%]+train[1:-1]')
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=datasets.ReadInstruction.from_spec(
|
||||
'test[:33%]+train[1:-1]'))
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=(
|
||||
datasets.ReadInstruction('test', to=33, unit='%') +
|
||||
datasets.ReadInstruction('train', from_=1, to=-1, unit='abs')))
|
||||
|
||||
# The following lines are equivalent:
|
||||
ds = datasets.load_dataset('ylecun/mnist', split='test[:33%](pct1_dropremainder)')
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=datasets.ReadInstruction.from_spec(
|
||||
'test[:33%](pct1_dropremainder)'))
|
||||
ds = datasets.load_dataset('ylecun/mnist', split=datasets.ReadInstruction(
|
||||
'test', from_=0, to=33, unit='%', rounding="pct1_dropremainder"))
|
||||
|
||||
# 10-fold validation:
|
||||
tests = datasets.load_dataset(
|
||||
'ylecun/mnist',
|
||||
[datasets.ReadInstruction('train', from_=k, to=k+10, unit='%')
|
||||
for k in range(0, 100, 10)])
|
||||
trains = datasets.load_dataset(
|
||||
'ylecun/mnist',
|
||||
[datasets.ReadInstruction('train', to=k, unit='%') + datasets.ReadInstruction('train', from_=k+10, unit='%')
|
||||
for k in range(0, 100, 10)])
|
||||
|
||||
"""
|
||||
|
||||
def _init(self, relative_instructions):
|
||||
# Private initializer.
|
||||
self._relative_instructions = relative_instructions
|
||||
|
||||
@classmethod
|
||||
def _read_instruction_from_relative_instructions(cls, relative_instructions):
|
||||
"""Returns ReadInstruction obj initialized with relative_instructions."""
|
||||
# Use __new__ to bypass __init__ used by public API and not conveniant here.
|
||||
result = cls.__new__(cls)
|
||||
result._init(relative_instructions) # pylint: disable=protected-access
|
||||
return result
|
||||
|
||||
def __init__(self, split_name, rounding=None, from_=None, to=None, unit=None):
|
||||
"""Initialize ReadInstruction.
|
||||
|
||||
Args:
|
||||
split_name (str): name of the split to read. Eg: 'train'.
|
||||
rounding (str, optional): The rounding behaviour to use when percent slicing is
|
||||
used. Ignored when slicing with absolute indices.
|
||||
Possible values:
|
||||
- 'closest' (default): The specified percentages are rounded to the
|
||||
closest value. Use this if you want specified percents to be as
|
||||
much exact as possible.
|
||||
- 'pct1_dropremainder': the specified percentages are treated as
|
||||
multiple of 1%. Use this option if you want consistency. Eg:
|
||||
len(5%) == 5 * len(1%).
|
||||
Using this option, one might not be able to use the full set of
|
||||
examples, if the number of those is not a multiple of 100.
|
||||
from_ (int):
|
||||
to (int): alternative way of specifying slicing boundaries. If any of
|
||||
{from_, to, unit} argument is used, slicing cannot be specified as
|
||||
string.
|
||||
unit (str): optional, one of:
|
||||
'%': to set the slicing unit as percents of the split size.
|
||||
'abs': to set the slicing unit as absolute numbers.
|
||||
"""
|
||||
# This constructor is not always called. See factory method
|
||||
# `_read_instruction_from_relative_instructions`. Common init instructions
|
||||
# MUST be placed in the _init method.
|
||||
self._init([_RelativeInstruction(split_name, from_, to, unit, rounding)])
|
||||
|
||||
@classmethod
|
||||
def from_spec(cls, spec):
|
||||
"""Creates a `ReadInstruction` instance out of a string spec.
|
||||
|
||||
Args:
|
||||
spec (`str`):
|
||||
Split(s) + optional slice(s) to read + optional rounding
|
||||
if percents are used as the slicing unit. A slice can be specified,
|
||||
using absolute numbers (`int`) or percentages (`int`).
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
test: test split.
|
||||
test + validation: test split + validation split.
|
||||
test[10:]: test split, minus its first 10 records.
|
||||
test[:10%]: first 10% records of test split.
|
||||
test[:20%](pct1_dropremainder): first 10% records, rounded with the pct1_dropremainder rounding.
|
||||
test[:-5%]+train[40%:60%]: first 95% of test + middle 20% of train.
|
||||
```
|
||||
|
||||
Returns:
|
||||
ReadInstruction instance.
|
||||
"""
|
||||
spec = str(spec) # Need to convert to str in case of NamedSplit instance.
|
||||
subs = _ADDITION_SEP_RE.split(spec)
|
||||
if not subs:
|
||||
raise ValueError(f"No instructions could be built out of {spec}")
|
||||
instruction = _str_to_read_instruction(subs[0])
|
||||
return sum((_str_to_read_instruction(sub) for sub in subs[1:]), instruction)
|
||||
|
||||
def to_spec(self):
|
||||
rel_instr_specs = []
|
||||
for rel_instr in self._relative_instructions:
|
||||
rel_instr_spec = rel_instr.splitname
|
||||
if rel_instr.from_ is not None or rel_instr.to is not None:
|
||||
from_ = rel_instr.from_
|
||||
to = rel_instr.to
|
||||
unit = rel_instr.unit
|
||||
rounding = rel_instr.rounding
|
||||
unit = unit if unit == "%" else ""
|
||||
from_ = str(from_) + unit if from_ is not None else ""
|
||||
to = str(to) + unit if to is not None else ""
|
||||
slice_str = f"[{from_}:{to}]"
|
||||
rounding_str = (
|
||||
f"({rounding})" if unit == "%" and rounding is not None and rounding != "closest" else ""
|
||||
)
|
||||
rel_instr_spec += slice_str + rounding_str
|
||||
rel_instr_specs.append(rel_instr_spec)
|
||||
return "+".join(rel_instr_specs)
|
||||
|
||||
def __add__(self, other):
|
||||
"""Returns a new ReadInstruction obj, result of appending other to self."""
|
||||
if not isinstance(other, ReadInstruction):
|
||||
msg = "ReadInstruction can only be added to another ReadInstruction obj."
|
||||
raise TypeError(msg)
|
||||
self_ris = self._relative_instructions
|
||||
other_ris = other._relative_instructions # pylint: disable=protected-access
|
||||
if (
|
||||
self_ris[0].unit != "abs"
|
||||
and other_ris[0].unit != "abs"
|
||||
and self._relative_instructions[0].rounding != other_ris[0].rounding
|
||||
):
|
||||
raise ValueError("It is forbidden to sum ReadInstruction instances with different rounding values.")
|
||||
return self._read_instruction_from_relative_instructions(self_ris + other_ris)
|
||||
|
||||
def __str__(self):
|
||||
return self.to_spec()
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReadInstruction({self._relative_instructions})"
|
||||
|
||||
def to_absolute(self, name2len):
|
||||
"""Translate instruction into a list of absolute instructions.
|
||||
|
||||
Those absolute instructions are then to be added together.
|
||||
|
||||
Args:
|
||||
name2len (`dict`):
|
||||
Associating split names to number of examples.
|
||||
|
||||
Returns:
|
||||
list of _AbsoluteInstruction instances (corresponds to the + in spec).
|
||||
"""
|
||||
return [_rel_to_abs_instr(rel_instr, name2len) for rel_instr in self._relative_instructions]
|
||||
@@ -0,0 +1,828 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors and the TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
"""To write records into Parquet files."""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import fsspec
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.json as paj
|
||||
import pyarrow.parquet as pq
|
||||
from fsspec.core import url_to_fs
|
||||
|
||||
from . import config
|
||||
from .features import Audio, Features, Image, Pdf, Value, Video
|
||||
from .features.features import (
|
||||
FeatureType,
|
||||
List,
|
||||
_ArrayXDExtensionType,
|
||||
_visit,
|
||||
cast_to_python_objects,
|
||||
generate_from_arrow_type,
|
||||
get_nested_type,
|
||||
list_of_np_array_to_pyarrow_listarray,
|
||||
numpy_to_pyarrow_listarray,
|
||||
require_storage_embed,
|
||||
to_pyarrow_listarray,
|
||||
)
|
||||
from .filesystems import is_remote_filesystem
|
||||
from .info import DatasetInfo
|
||||
from .table import array_cast, cast_array_to_feature, embed_table_storage, table_cast
|
||||
from .utils import logging
|
||||
from .utils.json import (
|
||||
find_mixed_struct_types_field_paths,
|
||||
get_json_field_path_from_pyarrow_json_error,
|
||||
get_json_field_paths_from_feature,
|
||||
insert_json_field_path,
|
||||
json_encode_field,
|
||||
json_encode_fields_in_json_lines,
|
||||
set_json_types_in_feature,
|
||||
ujson_dumps,
|
||||
)
|
||||
from .utils.py_utils import asdict, convert_file_size_to_int, first_non_null_non_empty_value
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
type_ = type # keep python's type function
|
||||
|
||||
|
||||
def get_arrow_writer_batch_size_from_features(features: Optional[Features]) -> Optional[int]:
|
||||
"""
|
||||
Get the writer_batch_size that defines the maximum record batch size in the arrow files based on configuration values.
|
||||
The default value is 100 for image/audio datasets and 10 for videos.
|
||||
This allows to avoid overflows in arrow buffers.
|
||||
|
||||
Args:
|
||||
features (`datasets.Features` or `None`):
|
||||
Dataset Features from `datasets`.
|
||||
Returns:
|
||||
writer_batch_size (`Optional[int]`):
|
||||
Writer batch size to pass to a dataset builder.
|
||||
If `None`, then it will use the `datasets` default, i.e. `datasets.config.DEFAULT_MAX_BATCH_SIZE`.
|
||||
"""
|
||||
if not features:
|
||||
return None
|
||||
|
||||
batch_size = np.inf
|
||||
|
||||
def set_batch_size(feature: FeatureType) -> None:
|
||||
nonlocal batch_size
|
||||
if isinstance(feature, Image) and config.ARROW_RECORD_BATCH_SIZE_FOR_IMAGE_DATASETS is not None:
|
||||
batch_size = min(batch_size, config.ARROW_RECORD_BATCH_SIZE_FOR_IMAGE_DATASETS)
|
||||
elif isinstance(feature, Audio) and config.ARROW_RECORD_BATCH_SIZE_FOR_AUDIO_DATASETS is not None:
|
||||
batch_size = min(batch_size, config.ARROW_RECORD_BATCH_SIZE_FOR_AUDIO_DATASETS)
|
||||
elif isinstance(feature, Video) and config.ARROW_RECORD_BATCH_SIZE_FOR_VIDEO_DATASETS is not None:
|
||||
batch_size = min(batch_size, config.ARROW_RECORD_BATCH_SIZE_FOR_VIDEO_DATASETS)
|
||||
elif (
|
||||
isinstance(feature, Value)
|
||||
and feature.dtype == "binary"
|
||||
and config.ARROW_RECORD_BATCH_SIZE_FOR_BINARY_DATASETS is not None
|
||||
):
|
||||
batch_size = min(batch_size, config.ARROW_RECORD_BATCH_SIZE_FOR_BINARY_DATASETS)
|
||||
|
||||
_visit(features, set_batch_size)
|
||||
|
||||
return None if batch_size is np.inf else batch_size
|
||||
|
||||
|
||||
def get_writer_batch_size_from_features(features: Optional[Features]) -> Optional[int]:
|
||||
"""
|
||||
Get the writer_batch_size that defines the maximum row group size in the parquet files based on configuration values.
|
||||
By default these are not set, but it can be helpful to hard set those values in some cases.
|
||||
This allows to optimize random access to parquet file, since accessing 1 row requires
|
||||
to read its entire row group.
|
||||
|
||||
Args:
|
||||
features (`datasets.Features` or `None`):
|
||||
Dataset Features from `datasets`.
|
||||
Returns:
|
||||
writer_batch_size (`Optional[int]`):
|
||||
Writer batch size to pass to a parquet writer.
|
||||
If `None`, then it will use the `datasets` default, i.e. aiming for row groups of 100MB.
|
||||
"""
|
||||
if not features:
|
||||
return None
|
||||
|
||||
batch_size = np.inf
|
||||
|
||||
def set_batch_size(feature: FeatureType) -> None:
|
||||
nonlocal batch_size
|
||||
if isinstance(feature, Image) and config.PARQUET_ROW_GROUP_SIZE_FOR_IMAGE_DATASETS is not None:
|
||||
batch_size = min(batch_size, config.PARQUET_ROW_GROUP_SIZE_FOR_IMAGE_DATASETS)
|
||||
elif isinstance(feature, Audio) and config.PARQUET_ROW_GROUP_SIZE_FOR_AUDIO_DATASETS is not None:
|
||||
batch_size = min(batch_size, config.PARQUET_ROW_GROUP_SIZE_FOR_AUDIO_DATASETS)
|
||||
elif isinstance(feature, Video) and config.PARQUET_ROW_GROUP_SIZE_FOR_VIDEO_DATASETS is not None:
|
||||
batch_size = min(batch_size, config.PARQUET_ROW_GROUP_SIZE_FOR_VIDEO_DATASETS)
|
||||
elif (
|
||||
isinstance(feature, Value)
|
||||
and feature.dtype == "binary"
|
||||
and config.PARQUET_ROW_GROUP_SIZE_FOR_BINARY_DATASETS is not None
|
||||
):
|
||||
batch_size = min(batch_size, config.PARQUET_ROW_GROUP_SIZE_FOR_BINARY_DATASETS)
|
||||
|
||||
_visit(features, set_batch_size)
|
||||
|
||||
return None if batch_size is np.inf else batch_size
|
||||
|
||||
|
||||
def get_writer_batch_size_from_data_size(num_rows: int, num_bytes: int) -> int:
|
||||
"""
|
||||
Get the writer_batch_size that defines the maximum row group size in the parquet files.
|
||||
The default in `datasets` is aiming for row groups of maximum 100MB uncompressed.
|
||||
This allows to optimize random access to parquet file, since accessing 1 row requires
|
||||
to read its entire row group.
|
||||
|
||||
This can be improved to get optimized size for querying/iterating
|
||||
but at least it matches the dataset viewer expectations on HF.
|
||||
|
||||
Args:
|
||||
num_rows (`int`):
|
||||
Number of rows in the dataset.
|
||||
num_bytes (`int`):
|
||||
Number of bytes in the dataset.
|
||||
For dataset with external files to embed (image, audio, videos), this can also be an
|
||||
estimate from `dataset._estimate_nbytes()`.
|
||||
Returns:
|
||||
writer_batch_size (`Optional[int]`):
|
||||
Writer batch size to pass to a parquet writer.
|
||||
"""
|
||||
return max(1, num_rows * convert_file_size_to_int(config.MAX_ROW_GROUP_SIZE) // num_bytes) if num_bytes > 0 else 1
|
||||
|
||||
|
||||
class SchemaInferenceError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class TypedSequence:
|
||||
"""
|
||||
This data container generalizes the typing when instantiating pyarrow arrays, tables or batches.
|
||||
|
||||
More specifically it adds several features:
|
||||
- Support extension types like ``datasets.features.Array2DExtensionType``:
|
||||
By default pyarrow arrays don't return extension arrays. One has to call
|
||||
``pa.ExtensionArray.from_storage(type, pa.array(data, type.storage_type))``
|
||||
in order to get an extension array.
|
||||
- Support for ``try_type`` parameter that can be used instead of ``type``:
|
||||
When an array is transformed, we like to keep the same type as before if possible.
|
||||
For example when calling :func:`datasets.Dataset.map`, we don't want to change the type
|
||||
of each column by default.
|
||||
- Better error message when a pyarrow array overflows.
|
||||
|
||||
Example::
|
||||
|
||||
from datasets.features import Array2D, Array2DExtensionType, Value
|
||||
from datasets.arrow_writer import TypedSequence
|
||||
import pyarrow as pa
|
||||
|
||||
arr = pa.array(TypedSequence([1, 2, 3], type=Value("int32")))
|
||||
assert arr.type == pa.int32()
|
||||
|
||||
arr = pa.array(TypedSequence([1, 2, 3], try_type=Value("int32")))
|
||||
assert arr.type == pa.int32()
|
||||
|
||||
arr = pa.array(TypedSequence(["foo", "bar"], try_type=Value("int32")))
|
||||
assert arr.type == pa.string()
|
||||
|
||||
arr = pa.array(TypedSequence([[[1, 2, 3]]], type=Array2D((1, 3), "int64")))
|
||||
assert arr.type == Array2DExtensionType((1, 3), "int64")
|
||||
|
||||
table = pa.Table.from_pydict({
|
||||
"image": TypedSequence([[[1, 2, 3]]], type=Array2D((1, 3), "int64"))
|
||||
})
|
||||
assert table["image"].type == Array2DExtensionType((1, 3), "int64")
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Iterable,
|
||||
type: Optional[FeatureType] = None,
|
||||
try_type: Optional[FeatureType] = None,
|
||||
optimized_int_type: Optional[FeatureType] = None,
|
||||
on_mixed_types: Optional[Literal["use_json"]] = None,
|
||||
):
|
||||
# assert type is None or try_type is None,
|
||||
if type is not None and try_type is not None:
|
||||
raise ValueError("You cannot specify both type and try_type")
|
||||
# set attributes
|
||||
self.data = data
|
||||
self.type = type
|
||||
self.try_type = try_type # is ignored if it doesn't match the data
|
||||
self.optimized_int_type = optimized_int_type
|
||||
self.on_mixed_types = on_mixed_types
|
||||
# when trying a type (is ignored if data is not compatible)
|
||||
self.trying_type = self.try_type is not None
|
||||
self.trying_int_optimization = optimized_int_type is not None and type is None and try_type is None
|
||||
# used to get back the inferred type after __arrow_array__() is called once
|
||||
self._inferred_type = None
|
||||
|
||||
def get_inferred_type(self) -> FeatureType:
|
||||
"""Return the inferred feature type.
|
||||
This is done by converting the sequence to an Arrow array, and getting the corresponding
|
||||
feature type.
|
||||
|
||||
Since building the Arrow array can be expensive, the value of the inferred type is cached
|
||||
as soon as pa.array is called on the typed sequence.
|
||||
|
||||
Returns:
|
||||
FeatureType: inferred feature type of the sequence.
|
||||
"""
|
||||
if self._inferred_type is None:
|
||||
pa.array(self)
|
||||
return self._inferred_type
|
||||
|
||||
@staticmethod
|
||||
def _infer_custom_type_and_encode(data: Iterable) -> tuple[Iterable, Optional[FeatureType]]:
|
||||
"""Implement type inference for custom objects like PIL.Image.Image -> Image type.
|
||||
|
||||
This function is only used for custom python objects that can't be directly passed to build
|
||||
an Arrow array. In such cases is infers the feature type to use, and it encodes the data so
|
||||
that they can be passed to an Arrow array.
|
||||
|
||||
Args:
|
||||
data (Iterable): array of data to infer the type, e.g. a list of PIL images.
|
||||
|
||||
Returns:
|
||||
Tuple[Iterable, Optional[FeatureType]]: a tuple with:
|
||||
- the (possibly encoded) array, if the inferred feature type requires encoding
|
||||
- the inferred feature type if the array is made of supported custom objects like
|
||||
PIL images, else None.
|
||||
"""
|
||||
if config.PIL_AVAILABLE and "PIL" in sys.modules:
|
||||
import PIL.Image
|
||||
|
||||
non_null_idx, non_null_value = first_non_null_non_empty_value(data)
|
||||
if isinstance(non_null_value, PIL.Image.Image):
|
||||
return [Image().encode_example(value) if value is not None else None for value in data], Image()
|
||||
if isinstance(non_null_value, list) and isinstance(non_null_value[0], PIL.Image.Image):
|
||||
return [
|
||||
[Image().encode_example(x) for x in value] if value is not None else None for value in data
|
||||
], List(Image())
|
||||
if config.PDFPLUMBER_AVAILABLE and "pdfplumber" in sys.modules:
|
||||
import pdfplumber
|
||||
|
||||
non_null_idx, non_null_value = first_non_null_non_empty_value(data)
|
||||
if isinstance(non_null_value, pdfplumber.pdf.PDF):
|
||||
return [Pdf().encode_example(value) if value is not None else None for value in data], Pdf()
|
||||
if isinstance(non_null_value, list) and isinstance(non_null_value[0], pdfplumber.pdf.PDF):
|
||||
return [
|
||||
[Pdf().encode_example(x) for x in value] if value is not None else None for value in data
|
||||
], List(Pdf())
|
||||
return data, None
|
||||
|
||||
def __arrow_array__(self, type: Optional[pa.DataType] = None):
|
||||
out = self._arrow_array(type=type)
|
||||
if self._inferred_type is None:
|
||||
self._inferred_type = generate_from_arrow_type(out.type)
|
||||
return out
|
||||
|
||||
def _arrow_array(self, type: Optional[pa.DataType] = None):
|
||||
"""This function is called when calling pa.array(typed_sequence)"""
|
||||
|
||||
if type is not None:
|
||||
raise ValueError("TypedSequence is supposed to be used with pa.array(typed_sequence, type=None)")
|
||||
del type # make sure we don't use it
|
||||
data = self.data
|
||||
# automatic type inference for custom objects
|
||||
if self.type is None and self.try_type is None:
|
||||
data, self._inferred_type = self._infer_custom_type_and_encode(data)
|
||||
if self._inferred_type is None:
|
||||
type = self.try_type if self.trying_type else self.type
|
||||
else:
|
||||
type = self._inferred_type
|
||||
pa_type = get_nested_type(type) if type is not None else None
|
||||
optimized_int_pa_type = (
|
||||
get_nested_type(self.optimized_int_type) if self.optimized_int_type is not None else None
|
||||
)
|
||||
trying_cast_to_python_objects = False
|
||||
json_field_paths = []
|
||||
try:
|
||||
# custom pyarrow types
|
||||
if isinstance(pa_type, _ArrayXDExtensionType):
|
||||
storage = to_pyarrow_listarray(data, pa_type)
|
||||
return pa.ExtensionArray.from_storage(pa_type, storage)
|
||||
|
||||
# efficient np array to pyarrow array
|
||||
if isinstance(data, np.ndarray):
|
||||
out = numpy_to_pyarrow_listarray(data)
|
||||
elif isinstance(data, list) and data and isinstance(first_non_null_non_empty_value(data)[1], np.ndarray):
|
||||
out = list_of_np_array_to_pyarrow_listarray(data)
|
||||
else:
|
||||
trying_cast_to_python_objects = True
|
||||
examples = data
|
||||
# find fields to json-encode
|
||||
if self.on_mixed_types == "use_json" and type is None:
|
||||
json_field_paths = find_mixed_struct_types_field_paths(examples, allow_root=True)
|
||||
elif type is not None:
|
||||
json_field_paths = get_json_field_paths_from_feature(type)
|
||||
# json encode if needed
|
||||
if json_field_paths:
|
||||
for json_field_path in json_field_paths:
|
||||
examples = [json_encode_field(examples, json_field_path) for examples in examples]
|
||||
# to arrow array
|
||||
out = pa.array(cast_to_python_objects(examples, only_1d_for_numpy=True))
|
||||
# cast to json type if needed
|
||||
if json_field_paths:
|
||||
pa_table = pa.Table.from_arrays([out], names=["obj"])
|
||||
features = Features.from_arrow_schema(pa_table.schema)
|
||||
feature = set_json_types_in_feature(features["obj"], json_field_paths)
|
||||
pa_table = table_cast(pa_table, Features({"obj": feature}).arrow_schema)
|
||||
out = pa_table[0] # get the "obj" column
|
||||
# use smaller integer precisions if possible
|
||||
if self.trying_int_optimization:
|
||||
if pa.types.is_int64(out.type):
|
||||
out = out.cast(optimized_int_pa_type)
|
||||
elif pa.types.is_list(out.type):
|
||||
if pa.types.is_int64(out.type.value_type):
|
||||
out = array_cast(out, pa.list_(optimized_int_pa_type))
|
||||
elif pa.types.is_list(out.type.value_type) and pa.types.is_int64(out.type.value_type.value_type):
|
||||
out = array_cast(out, pa.list_(pa.list_(optimized_int_pa_type)))
|
||||
# otherwise we can finally use the user's type
|
||||
elif type is not None:
|
||||
# We use cast_array_to_feature to support casting to custom types like Audio and Image
|
||||
# Also, when trying type "string", we don't want to convert integers or floats to "string".
|
||||
# We only do it if trying_type is False - since this is what the user asks for.
|
||||
out = cast_array_to_feature(
|
||||
out, type, allow_primitive_to_str=not self.trying_type, allow_decimal_to_str=not self.trying_type
|
||||
)
|
||||
return out
|
||||
except (
|
||||
TypeError,
|
||||
pa.lib.ArrowTypeError,
|
||||
pa.lib.ArrowInvalid,
|
||||
pa.lib.ArrowNotImplementedError,
|
||||
) as e: # handle type errors and overflows
|
||||
# Ignore ArrowNotImplementedError caused by trying type, otherwise re-raise
|
||||
if not self.trying_type and isinstance(e, pa.lib.ArrowNotImplementedError):
|
||||
raise
|
||||
|
||||
if self.trying_type:
|
||||
try: # second chance
|
||||
if isinstance(data, np.ndarray):
|
||||
return numpy_to_pyarrow_listarray(data)
|
||||
elif isinstance(data, list) and data and any(isinstance(value, np.ndarray) for value in data):
|
||||
return list_of_np_array_to_pyarrow_listarray(data)
|
||||
else:
|
||||
trying_cast_to_python_objects = True
|
||||
return pa.array(cast_to_python_objects(data, only_1d_for_numpy=True))
|
||||
except pa.lib.ArrowInvalid as e:
|
||||
if "overflow" in str(e):
|
||||
raise OverflowError(
|
||||
f"There was an overflow with type {type_(data)}. Try to reduce writer_batch_size to have batches smaller than 2GB.\n({e})"
|
||||
) from None
|
||||
elif self.trying_int_optimization and "not in range" in str(e):
|
||||
optimized_int_pa_type_str = np.dtype(optimized_int_pa_type.to_pandas_dtype()).name
|
||||
logger.info(
|
||||
f"Failed to cast a sequence to {optimized_int_pa_type_str}. Falling back to int64."
|
||||
)
|
||||
return out
|
||||
elif trying_cast_to_python_objects and "Could not convert" in str(e):
|
||||
out = pa.array(
|
||||
cast_to_python_objects(data, only_1d_for_numpy=True, optimize_list_casting=False)
|
||||
)
|
||||
if type is not None:
|
||||
out = cast_array_to_feature(
|
||||
out, type, allow_primitive_to_str=True, allow_decimal_to_str=True
|
||||
)
|
||||
return out
|
||||
else:
|
||||
raise
|
||||
elif "overflow" in str(e):
|
||||
raise OverflowError(
|
||||
f"There was an overflow with type {type_(data)}. Try to reduce writer_batch_size to have batches smaller than 2GB.\n({e})"
|
||||
) from None
|
||||
elif self.trying_int_optimization and "not in range" in str(e):
|
||||
optimized_int_pa_type_str = np.dtype(optimized_int_pa_type.to_pandas_dtype()).name
|
||||
logger.info(f"Failed to cast a sequence to {optimized_int_pa_type_str}. Falling back to int64.")
|
||||
return out
|
||||
elif trying_cast_to_python_objects and (
|
||||
"Could not convert" in str(e) or "cannot mix struct and non-struct" in str(e) or "Expected " in str(e)
|
||||
):
|
||||
try: # third chance
|
||||
out = pa.array(cast_to_python_objects(data, only_1d_for_numpy=True, optimize_list_casting=False))
|
||||
except (pa.ArrowInvalid, pa.ArrowTypeError) as ee:
|
||||
# in case of mixed types, we use the JSON Lines reader in pyarrow to locate them and set them to json fields
|
||||
if self.on_mixed_types == "use_json" and (
|
||||
"Could not convert " in str(ee)
|
||||
or "cannot mix struct and non-struct" in str(ee)
|
||||
or "Expected " in str(ee)
|
||||
):
|
||||
# we use "obj" to have valid JSON Lines since data may contain lists
|
||||
original_batch = "\n".join([ujson_dumps({"obj": example}) for example in data]).encode()
|
||||
json_field_paths = [["obj"] + json_field_path for json_field_path in json_field_paths]
|
||||
batch = json_encode_fields_in_json_lines(original_batch, json_field_paths)
|
||||
pa_table = None
|
||||
while True:
|
||||
try: # fourth chance
|
||||
pa_table = paj.read_json(
|
||||
io.BytesIO(batch), read_options=paj.ReadOptions(use_threads=False)
|
||||
)
|
||||
break
|
||||
except pa.ArrowInvalid as eee:
|
||||
if "JSON parse error: Column(" in str(eee) and ") changed from" in str(eee):
|
||||
json_field_path = get_json_field_path_from_pyarrow_json_error(str(eee))
|
||||
insert_json_field_path(json_field_paths, json_field_path)
|
||||
batch = json_encode_fields_in_json_lines(original_batch, json_field_paths)
|
||||
else:
|
||||
break
|
||||
if pa_table is not None:
|
||||
features = Features.from_arrow_schema(pa_table.schema)
|
||||
features = set_json_types_in_feature(features, json_field_paths)
|
||||
pa_table = table_cast(pa_table, features.arrow_schema)
|
||||
out = pa_table[0] # get the "obj" column
|
||||
return out
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
if type is not None:
|
||||
out = cast_array_to_feature(out, type, allow_primitive_to_str=True, allow_decimal_to_str=True)
|
||||
return out
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class OptimizedTypedSequence(TypedSequence):
|
||||
def __init__(
|
||||
self,
|
||||
data,
|
||||
type: Optional[FeatureType] = None,
|
||||
try_type: Optional[FeatureType] = None,
|
||||
col: Optional[str] = None,
|
||||
optimized_int_type: Optional[FeatureType] = None,
|
||||
on_mixed_types: Optional[Literal["use_json"]] = None,
|
||||
):
|
||||
optimized_int_type_by_col = {
|
||||
"attention_mask": Value("int8"), # binary tensor
|
||||
"special_tokens_mask": Value("int8"),
|
||||
"input_ids": Value("int32"), # typical vocab size: 0-50k (max ~500k, never > 1M)
|
||||
"token_type_ids": Value(
|
||||
"int8"
|
||||
), # binary mask; some (XLNetModel) use an additional token represented by a 2
|
||||
}
|
||||
if type is None and try_type is None:
|
||||
optimized_int_type = optimized_int_type_by_col.get(col, None)
|
||||
super().__init__(
|
||||
data, type=type, try_type=try_type, optimized_int_type=optimized_int_type, on_mixed_types=on_mixed_types
|
||||
)
|
||||
|
||||
|
||||
class ArrowWriter:
|
||||
"""Shuffles and writes Examples to Arrow files."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: Optional[pa.Schema] = None,
|
||||
features: Optional[Features] = None,
|
||||
path: Optional[str] = None,
|
||||
stream: Optional[pa.NativeFile] = None,
|
||||
fingerprint: Optional[str] = None,
|
||||
writer_batch_size: Optional[int] = None,
|
||||
disable_nullable: bool = False,
|
||||
update_features: bool = False,
|
||||
on_mixed_types: Optional[Literal["use_json"]] = "use_json",
|
||||
with_metadata: bool = True,
|
||||
unit: str = "examples",
|
||||
embed_local_files: bool = False,
|
||||
storage_options: Optional[dict] = None,
|
||||
):
|
||||
if path is None and stream is None:
|
||||
raise ValueError("At least one of path and stream must be provided.")
|
||||
if features is not None:
|
||||
self._features = features
|
||||
self._schema = None
|
||||
elif schema is not None:
|
||||
self._schema: pa.Schema = schema
|
||||
self._features = Features.from_arrow_schema(self._schema)
|
||||
else:
|
||||
self._features = None
|
||||
self._schema = None
|
||||
|
||||
self._disable_nullable = disable_nullable
|
||||
|
||||
if stream is None:
|
||||
fs, path = url_to_fs(path, **(storage_options or {}))
|
||||
self._fs: fsspec.AbstractFileSystem = fs
|
||||
self._path = path if not is_remote_filesystem(self._fs) else self._fs.unstrip_protocol(path)
|
||||
self.stream = self._fs.open(path, "wb")
|
||||
self._closable_stream = True
|
||||
else:
|
||||
self._fs = None
|
||||
self._path = None
|
||||
self.stream = stream
|
||||
self._closable_stream = False
|
||||
|
||||
self.fingerprint = fingerprint
|
||||
self.disable_nullable = disable_nullable
|
||||
self.writer_batch_size = (
|
||||
writer_batch_size
|
||||
or get_arrow_writer_batch_size_from_features(self._features)
|
||||
or config.DEFAULT_MAX_BATCH_SIZE
|
||||
)
|
||||
self.update_features = update_features
|
||||
self.on_mixed_types = on_mixed_types
|
||||
self.with_metadata = with_metadata
|
||||
self.unit = unit
|
||||
self.embed_local_files = embed_local_files
|
||||
|
||||
self._num_examples = 0
|
||||
self._num_bytes = 0
|
||||
self.current_examples: list[tuple[dict[str, Any], str]] = []
|
||||
self.current_rows: list[pa.Table] = []
|
||||
self.pa_writer: Optional[pa.RecordBatchStreamWriter] = None
|
||||
self.hkey_record = []
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of writed and staged examples"""
|
||||
return self._num_examples + len(self.current_examples) + len(self.current_rows)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
# Try closing if opened; if closed: pyarrow.lib.ArrowInvalid: Invalid operation on closed file
|
||||
if self.pa_writer: # it might be None
|
||||
try:
|
||||
self.pa_writer.close()
|
||||
except Exception: # pyarrow.lib.ArrowInvalid, OSError
|
||||
pass
|
||||
if self._closable_stream and not self.stream.closed:
|
||||
self.stream.close() # This also closes self.pa_writer if it is opened
|
||||
|
||||
def _build_schema(self, inferred_schema: pa.Schema):
|
||||
schema = self.schema
|
||||
features = self._features
|
||||
inferred_features = Features.from_arrow_schema(inferred_schema)
|
||||
if self._features is not None:
|
||||
if self.update_features: # keep original features it they match, or update them
|
||||
fields = {field.name: field for field in self._features.type}
|
||||
for inferred_field in inferred_features.type:
|
||||
name = inferred_field.name
|
||||
if name in fields:
|
||||
if inferred_field == fields[name]:
|
||||
inferred_features[name] = self._features[name]
|
||||
features = inferred_features
|
||||
schema: pa.Schema = inferred_schema
|
||||
else:
|
||||
features = inferred_features
|
||||
schema: pa.Schema = inferred_features.arrow_schema
|
||||
|
||||
if self.disable_nullable:
|
||||
schema = pa.schema(pa.field(field.name, field.type, nullable=False) for field in schema)
|
||||
if self.with_metadata:
|
||||
schema = schema.with_metadata(self._build_metadata(DatasetInfo(features=features), self.fingerprint))
|
||||
else:
|
||||
schema = schema.with_metadata({})
|
||||
|
||||
return schema, features
|
||||
|
||||
def _build_writer(self, inferred_schema: pa.Schema):
|
||||
self._schema, self._features = self._build_schema(inferred_schema)
|
||||
self.pa_writer = pa.RecordBatchStreamWriter(self.stream, self._schema)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
_schema = (
|
||||
self._schema
|
||||
if self._schema is not None
|
||||
else (pa.schema(self._features.type) if self._features is not None else None)
|
||||
)
|
||||
if self._disable_nullable and _schema is not None:
|
||||
_schema = pa.schema(pa.field(field.name, field.type, nullable=False) for field in _schema)
|
||||
return _schema if _schema is not None else []
|
||||
|
||||
@staticmethod
|
||||
def _build_metadata(info: DatasetInfo, fingerprint: Optional[str] = None) -> dict[str, str]:
|
||||
info_keys = ["features"] # we can add support for more DatasetInfo keys in the future
|
||||
info_as_dict = asdict(info)
|
||||
metadata = {}
|
||||
metadata["info"] = {key: info_as_dict[key] for key in info_keys}
|
||||
if fingerprint is not None:
|
||||
metadata["fingerprint"] = fingerprint
|
||||
return {"huggingface": json.dumps(metadata)}
|
||||
|
||||
def write_examples_on_file(self):
|
||||
"""Write stored examples from the write-pool of examples. It makes a table out of the examples and write it."""
|
||||
if not self.current_examples:
|
||||
return
|
||||
# preserve the order the columns
|
||||
if self.schema:
|
||||
schema_cols = set(self.schema.names)
|
||||
examples_cols = self.current_examples[0][0].keys() # .keys() preserves the order (unlike set)
|
||||
common_cols = [col for col in self.schema.names if col in examples_cols]
|
||||
extra_cols = [col for col in examples_cols if col not in schema_cols]
|
||||
cols = common_cols + extra_cols
|
||||
else:
|
||||
cols = list(self.current_examples[0][0])
|
||||
batch_examples = {}
|
||||
for col in cols:
|
||||
# We use row[0][col] since current_examples contains (example, key) tuples.
|
||||
# Moreover, examples could be Arrow arrays of 1 element.
|
||||
# This can happen in `.map()` when we want to re-write the same Arrow data
|
||||
if all(isinstance(row[0][col], (pa.Array, pa.ChunkedArray)) for row in self.current_examples):
|
||||
arrays = [row[0][col] for row in self.current_examples]
|
||||
arrays = [
|
||||
chunk
|
||||
for array in arrays
|
||||
for chunk in (array.chunks if isinstance(array, pa.ChunkedArray) else [array])
|
||||
]
|
||||
batch_examples[col] = pa.concat_arrays(arrays)
|
||||
else:
|
||||
batch_examples[col] = [
|
||||
row[0][col].to_pylist()[0] if isinstance(row[0][col], (pa.Array, pa.ChunkedArray)) else row[0][col]
|
||||
for row in self.current_examples
|
||||
]
|
||||
self._write_batch(batch_examples=batch_examples)
|
||||
self.current_examples = []
|
||||
|
||||
def write_rows_on_file(self):
|
||||
"""Write stored rows from the write-pool of rows. It concatenates the single-row tables and it writes the resulting table."""
|
||||
if not self.current_rows:
|
||||
return
|
||||
table = pa.concat_tables(self.current_rows)
|
||||
self._write_table(table)
|
||||
self.current_rows = []
|
||||
|
||||
def write(
|
||||
self,
|
||||
example: dict[str, Any],
|
||||
writer_batch_size: Optional[int] = None,
|
||||
):
|
||||
"""Add a given (Example,Key) pair to the write-pool of examples which is written to file.
|
||||
|
||||
Args:
|
||||
example: the Example to add.
|
||||
"""
|
||||
# Store example as a tuple so as to keep the structure of `self.current_examples` uniform
|
||||
self.current_examples.append((example, ""))
|
||||
|
||||
if writer_batch_size is None:
|
||||
writer_batch_size = self.writer_batch_size
|
||||
if writer_batch_size is not None and len(self.current_examples) >= writer_batch_size:
|
||||
self.write_examples_on_file()
|
||||
|
||||
def write_row(self, row: pa.Table, writer_batch_size: Optional[int] = None):
|
||||
"""Add a given single-row Table to the write-pool of rows which is written to file.
|
||||
|
||||
Args:
|
||||
row: the row to add.
|
||||
"""
|
||||
if len(row) != 1:
|
||||
raise ValueError(f"Only single-row pyarrow tables are allowed but got table with {len(row)} rows.")
|
||||
self.current_rows.append(row)
|
||||
if writer_batch_size is None:
|
||||
writer_batch_size = self.writer_batch_size
|
||||
if writer_batch_size is not None and len(self.current_rows) >= writer_batch_size:
|
||||
self.write_rows_on_file()
|
||||
|
||||
def write_batch(
|
||||
self,
|
||||
batch_examples: dict[str, list],
|
||||
writer_batch_size: Optional[int] = None,
|
||||
try_original_type: Optional[bool] = True,
|
||||
):
|
||||
"""Write a batch of Example to file.
|
||||
Ignores the batch if it appears to be empty,
|
||||
preventing a potential schema update of unknown types.
|
||||
|
||||
Args:
|
||||
batch_examples: the batch of examples to add.
|
||||
try_original_type: use `try_type` when instantiating OptimizedTypedSequence if `True`, otherwise `try_type = None`.
|
||||
"""
|
||||
self.write_examples_on_file() # in case there are buffered examples to write first
|
||||
self._write_batch(batch_examples, writer_batch_size=writer_batch_size, try_original_type=try_original_type)
|
||||
|
||||
def _write_batch(
|
||||
self,
|
||||
batch_examples: dict[str, list],
|
||||
writer_batch_size: Optional[int] = None,
|
||||
try_original_type: Optional[bool] = True,
|
||||
):
|
||||
if batch_examples and len(next(iter(batch_examples.values()))) == 0:
|
||||
return
|
||||
features = None if self.pa_writer is None and self.update_features else self._features
|
||||
try_features = self._features if self.pa_writer is None and self.update_features else None
|
||||
arrays = []
|
||||
inferred_features = Features()
|
||||
# preserve the order the columns
|
||||
if self.schema:
|
||||
schema_cols = set(self.schema.names)
|
||||
batch_cols = batch_examples.keys() # .keys() preserves the order (unlike set)
|
||||
common_cols = [col for col in self.schema.names if col in batch_cols]
|
||||
extra_cols = [col for col in batch_cols if col not in schema_cols]
|
||||
cols = common_cols + extra_cols
|
||||
else:
|
||||
cols = list(batch_examples)
|
||||
for col in cols:
|
||||
col_values = batch_examples[col]
|
||||
col_type = features[col] if features else None
|
||||
if isinstance(col_values, (pa.Array, pa.ChunkedArray)):
|
||||
array = cast_array_to_feature(col_values, col_type) if col_type is not None else col_values
|
||||
arrays.append(array)
|
||||
inferred_features[col] = generate_from_arrow_type(col_values.type)
|
||||
else:
|
||||
col_try_type = (
|
||||
try_features[col]
|
||||
if try_features is not None and col in try_features and try_original_type
|
||||
else None
|
||||
)
|
||||
typed_sequence = OptimizedTypedSequence(
|
||||
col_values, type=col_type, try_type=col_try_type, col=col, on_mixed_types=self.on_mixed_types
|
||||
)
|
||||
arrays.append(pa.array(typed_sequence))
|
||||
inferred_features[col] = typed_sequence.get_inferred_type()
|
||||
schema = inferred_features.arrow_schema if self.pa_writer is None else self.schema
|
||||
pa_table = pa.Table.from_arrays(arrays, schema=schema)
|
||||
self.write_table(pa_table, writer_batch_size)
|
||||
|
||||
def write_table(self, pa_table: pa.Table, writer_batch_size: Optional[int] = None):
|
||||
"""Write a Table to file.
|
||||
|
||||
Args:
|
||||
example: the Table to add.
|
||||
"""
|
||||
self.write_rows_on_file() # in case there are buffered rows to write first
|
||||
self._write_table(pa_table, writer_batch_size=writer_batch_size)
|
||||
|
||||
def _write_table(self, pa_table: pa.Table, writer_batch_size: Optional[int] = None):
|
||||
if writer_batch_size is None:
|
||||
writer_batch_size = self.writer_batch_size
|
||||
if self.pa_writer is None:
|
||||
self._build_writer(inferred_schema=pa_table.schema)
|
||||
pa_table = pa_table.combine_chunks()
|
||||
pa_table = table_cast(pa_table, self._schema)
|
||||
if self.embed_local_files:
|
||||
pa_table = embed_table_storage(pa_table, local_files=True, remote_files=False)
|
||||
self._num_bytes += pa_table.nbytes
|
||||
self._num_examples += pa_table.num_rows
|
||||
self.pa_writer.write_table(pa_table, writer_batch_size)
|
||||
|
||||
def finalize(self, close_stream=True):
|
||||
self.write_rows_on_file()
|
||||
# In case current_examples < writer_batch_size, but user uses finalize()
|
||||
self.write_examples_on_file()
|
||||
# If schema is known, infer features even if no examples were written
|
||||
if self.pa_writer is None and self.schema:
|
||||
self._build_writer(self.schema)
|
||||
if self.pa_writer is not None:
|
||||
self.pa_writer.close()
|
||||
self.pa_writer = None
|
||||
if close_stream:
|
||||
self.stream.close()
|
||||
else:
|
||||
if close_stream:
|
||||
self.stream.close()
|
||||
raise SchemaInferenceError("Please pass `features` or at least one example when writing data")
|
||||
logger.debug(
|
||||
f"Done writing {self._num_examples} {self.unit} in {self._num_bytes} bytes {self._path if self._path else ''}."
|
||||
)
|
||||
return self._num_examples, self._num_bytes
|
||||
|
||||
|
||||
class ParquetWriter(ArrowWriter):
|
||||
def __init__(self, *args, use_content_defined_chunking=True, write_page_index=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if use_content_defined_chunking is True:
|
||||
use_content_defined_chunking = config.DEFAULT_CDC_OPTIONS
|
||||
self.use_content_defined_chunking = use_content_defined_chunking
|
||||
self.write_page_index = write_page_index
|
||||
|
||||
def _build_writer(self, inferred_schema: pa.Schema):
|
||||
self._schema, self._features = self._build_schema(inferred_schema)
|
||||
self.pa_writer = pq.ParquetWriter(
|
||||
self.stream,
|
||||
self._schema,
|
||||
use_content_defined_chunking=self.use_content_defined_chunking,
|
||||
write_page_index=self.write_page_index,
|
||||
compression={
|
||||
col: "none" if require_storage_embed(feature) else "snappy" for col, feature in self._features.items()
|
||||
},
|
||||
use_dictionary=[col for col, feature in self._features.items() if not require_storage_embed(feature)],
|
||||
column_encoding={
|
||||
col: "PLAIN" for col, feature in self._features.items() if require_storage_embed(feature)
|
||||
},
|
||||
)
|
||||
if self.use_content_defined_chunking is not False:
|
||||
self.pa_writer.add_key_value_metadata(
|
||||
{"content_defined_chunking": json.dumps(self.use_content_defined_chunking)}
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from .arrow_dataset import Dataset, _concatenate_map_style_datasets, _interleave_map_style_datasets
|
||||
from .dataset_dict import DatasetDict, IterableDatasetDict
|
||||
from .info import DatasetInfo
|
||||
from .iterable_dataset import IterableDataset, _concatenate_iterable_datasets, _interleave_iterable_datasets
|
||||
from .splits import NamedSplit
|
||||
from .utils import logging
|
||||
from .utils.py_utils import Literal
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
DatasetType = TypeVar("DatasetType", Dataset, IterableDataset)
|
||||
|
||||
|
||||
def interleave_datasets(
|
||||
datasets: list[DatasetType],
|
||||
probabilities: Optional[list[float]] = None,
|
||||
seed: Optional[int] = None,
|
||||
info: Optional[DatasetInfo] = None,
|
||||
split: Optional[NamedSplit] = None,
|
||||
stopping_strategy: Literal[
|
||||
"first_exhausted", "all_exhausted", "all_exhausted_without_replacement"
|
||||
] = "first_exhausted",
|
||||
) -> DatasetType:
|
||||
"""
|
||||
Interleave several datasets (sources) into a single dataset.
|
||||
The new dataset is constructed by alternating between the sources to get the examples.
|
||||
|
||||
You can use this function on a list of [`Dataset`] objects, or on a list of [`IterableDataset`] objects.
|
||||
|
||||
- If `probabilities` is `None` (default) the new dataset is constructed by cycling between each source to get the examples.
|
||||
- If `probabilities` is not `None`, the new dataset is constructed by getting examples from a random source at a time according to the provided probabilities.
|
||||
|
||||
The resulting dataset ends when one of the source datasets runs out of examples except when `oversampling` is `True`,
|
||||
in which case, the resulting dataset ends when all datasets have ran out of examples at least one time.
|
||||
|
||||
Note for iterable datasets:
|
||||
|
||||
* The resulting dataset's `num_shards` is the minimum of each dataset's `num_shards` to ensure good parallelism.
|
||||
If some of your datasets have a very low number of shards, you may use [`IterableDataset.reshard`].
|
||||
* In a distributed setup or in PyTorch DataLoader workers, the stopping strategy is applied per process.
|
||||
Therefore the "first_exhausted" strategy on an sharded iterable dataset can generate less samples in total (up to 1 missing sample per subdataset per worker).
|
||||
|
||||
Args:
|
||||
datasets (`List[Dataset]` or `List[IterableDataset]`):
|
||||
List of datasets to interleave.
|
||||
probabilities (`List[float]`, *optional*, defaults to `None`):
|
||||
If specified, the new dataset is constructed by sampling
|
||||
examples from one source at a time according to these probabilities.
|
||||
seed (`int`, *optional*, defaults to `None`):
|
||||
The random seed used to choose a source for each example.
|
||||
info ([`DatasetInfo`], *optional*):
|
||||
Dataset information, like description, citation, etc.
|
||||
<Added version="2.4.0"/>
|
||||
split ([`NamedSplit`], *optional*):
|
||||
Name of the dataset split.
|
||||
<Added version="2.4.0"/>
|
||||
stopping_strategy (`str`, defaults to `first_exhausted`):
|
||||
Three strategies are proposed right now, `first_exhausted`, `all_exhausted` and `all_exhausted_without_replacement`.
|
||||
By default, `first_exhausted` is an undersampling strategy, i.e the dataset construction is stopped as soon as one dataset has ran out of samples.
|
||||
If the strategy is `all_exhausted`, we use an oversampling strategy, i.e the dataset construction is stopped as soon as every samples of every dataset has been added at least once.
|
||||
When strategy is `all_exhausted_without_replacement` we make sure that each sample in each dataset is sampled only once.
|
||||
Note that if the strategy is `all_exhausted`, the interleaved dataset size can get enormous:
|
||||
- with no probabilities, the resulting dataset will have `max_length_datasets*nb_dataset` samples.
|
||||
- with given probabilities, the resulting dataset will have more samples if some datasets have really low probability of visiting.
|
||||
Returns:
|
||||
[`Dataset`] or [`IterableDataset`]: Return type depends on the input `datasets`
|
||||
parameter. `Dataset` if the input is a list of `Dataset`, `IterableDataset` if the input is a list of
|
||||
`IterableDataset`.
|
||||
|
||||
Example:
|
||||
|
||||
For regular datasets (map-style):
|
||||
|
||||
```python
|
||||
>>> from datasets import Dataset, interleave_datasets
|
||||
>>> d1 = Dataset.from_dict({"a": [0, 1, 2]})
|
||||
>>> d2 = Dataset.from_dict({"a": [10, 11, 12]})
|
||||
>>> d3 = Dataset.from_dict({"a": [20, 21, 22]})
|
||||
>>> dataset = interleave_datasets([d1, d2, d3], probabilities=[0.7, 0.2, 0.1], seed=42, stopping_strategy="all_exhausted")
|
||||
>>> dataset["a"]
|
||||
[10, 0, 11, 1, 2, 20, 12, 10, 0, 1, 2, 21, 0, 11, 1, 2, 0, 1, 12, 2, 10, 0, 22]
|
||||
>>> dataset = interleave_datasets([d1, d2, d3], probabilities=[0.7, 0.2, 0.1], seed=42)
|
||||
>>> dataset["a"]
|
||||
[10, 0, 11, 1, 2]
|
||||
>>> dataset = interleave_datasets([d1, d2, d3])
|
||||
>>> dataset["a"]
|
||||
[0, 10, 20, 1, 11, 21, 2, 12, 22]
|
||||
>>> dataset = interleave_datasets([d1, d2, d3], stopping_strategy="all_exhausted")
|
||||
>>> dataset["a"]
|
||||
[0, 10, 20, 1, 11, 21, 2, 12, 22]
|
||||
>>> d1 = Dataset.from_dict({"a": [0, 1, 2]})
|
||||
>>> d2 = Dataset.from_dict({"a": [10, 11, 12, 13]})
|
||||
>>> d3 = Dataset.from_dict({"a": [20, 21, 22, 23, 24]})
|
||||
>>> dataset = interleave_datasets([d1, d2, d3])
|
||||
>>> dataset["a"]
|
||||
[0, 10, 20, 1, 11, 21, 2, 12, 22]
|
||||
>>> dataset = interleave_datasets([d1, d2, d3], stopping_strategy="all_exhausted")
|
||||
>>> dataset["a"]
|
||||
[0, 10, 20, 1, 11, 21, 2, 12, 22, 0, 13, 23, 1, 10, 24]
|
||||
>>> dataset = interleave_datasets([d1, d2, d3], probabilities=[0.7, 0.2, 0.1], seed=42)
|
||||
>>> dataset["a"]
|
||||
[10, 0, 11, 1, 2]
|
||||
>>> dataset = interleave_datasets([d1, d2, d3], probabilities=[0.7, 0.2, 0.1], seed=42, stopping_strategy="all_exhausted")
|
||||
>>> dataset["a"]
|
||||
[10, 0, 11, 1, 2, 20, 12, 13, ..., 0, 1, 2, 0, 24]
|
||||
For datasets in streaming mode (iterable):
|
||||
|
||||
>>> from datasets import interleave_datasets
|
||||
>>> d1 = load_dataset('allenai/c4', 'es', split='train', streaming=True)
|
||||
>>> d2 = load_dataset('allenai/c4', 'fr', split='train', streaming=True)
|
||||
>>> dataset = interleave_datasets([d1, d2])
|
||||
>>> iterator = iter(dataset)
|
||||
>>> next(iterator)
|
||||
{'text': 'Comprar Zapatillas para niña en chancla con goma por...'}
|
||||
>>> next(iterator)
|
||||
{'text': 'Le sacre de philippe ier, 23 mai 1059 - Compte Rendu...'
|
||||
```
|
||||
"""
|
||||
from .arrow_dataset import Dataset
|
||||
from .iterable_dataset import IterableDataset
|
||||
|
||||
if not datasets:
|
||||
raise ValueError("Unable to interleave an empty list of datasets.")
|
||||
for i, dataset in enumerate(datasets):
|
||||
if not isinstance(dataset, (Dataset, IterableDataset)):
|
||||
if isinstance(dataset, (DatasetDict, IterableDatasetDict)):
|
||||
if not dataset:
|
||||
raise ValueError(
|
||||
f"Expected a list of Dataset objects or a list of IterableDataset objects, but element at position {i} "
|
||||
"is an empty dataset dictionary."
|
||||
)
|
||||
raise ValueError(
|
||||
f"Dataset at position {i} has at least one split: {list(dataset)}\n"
|
||||
f"Please pick one to interleave with the other datasets, for example: dataset['{next(iter(dataset))}']"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Expected a list of Dataset objects or a list of IterableDataset objects, but element at position {i} is a {type(dataset).__name__}."
|
||||
)
|
||||
if i == 0:
|
||||
dataset_type, other_type = (
|
||||
(Dataset, IterableDataset) if isinstance(dataset, Dataset) else (IterableDataset, Dataset)
|
||||
)
|
||||
elif not isinstance(dataset, dataset_type):
|
||||
raise ValueError(
|
||||
f"Unable to interleave a {dataset_type.__name__} (at position 0) with a {other_type.__name__} (at position {i}). Expected a list of Dataset objects or a list of IterableDataset objects."
|
||||
)
|
||||
if stopping_strategy not in ["first_exhausted", "all_exhausted", "all_exhausted_without_replacement"]:
|
||||
raise ValueError(f"{stopping_strategy} is not supported. Please enter a valid stopping_strategy.")
|
||||
if dataset_type is Dataset:
|
||||
return _interleave_map_style_datasets(
|
||||
datasets, probabilities, seed, info=info, split=split, stopping_strategy=stopping_strategy
|
||||
)
|
||||
else:
|
||||
return _interleave_iterable_datasets(
|
||||
datasets,
|
||||
probabilities,
|
||||
seed,
|
||||
info=info,
|
||||
split=split,
|
||||
stopping_strategy=stopping_strategy,
|
||||
)
|
||||
|
||||
|
||||
def concatenate_datasets(
|
||||
dsets: list[DatasetType],
|
||||
info: Optional[DatasetInfo] = None,
|
||||
split: Optional[NamedSplit] = None,
|
||||
axis: int = 0,
|
||||
) -> DatasetType:
|
||||
"""
|
||||
Concatenate several datasets (sources) into a single dataset.
|
||||
|
||||
Use axis=0 to concatenate vertically (default), or axis=1 to concatenate horizontally.
|
||||
|
||||
Note for iterable datasets:
|
||||
|
||||
* if axis=0, the resulting dataset's `num_shards` is the sum of each dataset's `num_shards`.
|
||||
* if axis=1, the resulting dataset has one (1) shard to not misalign data.
|
||||
|
||||
Args:
|
||||
dsets (`List[datasets.Dataset]` or `List[datasets.IterableDataset]`):
|
||||
List of Datasets to concatenate.
|
||||
info (`DatasetInfo`, *optional*):
|
||||
Dataset information, like description, citation, etc.
|
||||
split (`NamedSplit`, *optional*):
|
||||
Name of the dataset split.
|
||||
axis (`{0, 1}`, defaults to `0`):
|
||||
Axis to concatenate over, where `0` means over rows (vertically) and `1` means over columns
|
||||
(horizontally).
|
||||
|
||||
<Added version="1.6.0"/>
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> ds3 = concatenate_datasets([ds1, ds2])
|
||||
```
|
||||
"""
|
||||
|
||||
if not dsets:
|
||||
raise ValueError("Unable to concatenate an empty list of datasets.")
|
||||
for i, dataset in enumerate(dsets):
|
||||
if not isinstance(dataset, (Dataset, IterableDataset)):
|
||||
if isinstance(dataset, (DatasetDict, IterableDatasetDict)):
|
||||
if not dataset:
|
||||
raise ValueError(
|
||||
f"Expected a list of Dataset objects or a list of IterableDataset objects, but element at position {i} "
|
||||
"is an empty dataset dictionary."
|
||||
)
|
||||
raise ValueError(
|
||||
f"Dataset at position {i} has at least one split: {list(dataset)}\n"
|
||||
f"Please pick one to interleave with the other datasets, for example: dataset['{next(iter(dataset))}']"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Expected a list of Dataset objects or a list of IterableDataset objects, but element at position {i} is a {type(dataset).__name__}."
|
||||
)
|
||||
if i == 0:
|
||||
dataset_type, other_type = (
|
||||
(Dataset, IterableDataset) if isinstance(dataset, Dataset) else (IterableDataset, Dataset)
|
||||
)
|
||||
elif not isinstance(dataset, dataset_type):
|
||||
raise ValueError(
|
||||
f"Unable to interleave a {dataset_type.__name__} (at position 0) with a {other_type.__name__} (at position {i}). Expected a list of Dataset objects or a list of IterableDataset objects."
|
||||
)
|
||||
if dataset_type is Dataset:
|
||||
return _concatenate_map_style_datasets(dsets, info=info, split=split, axis=axis)
|
||||
else:
|
||||
return _concatenate_iterable_datasets(dsets, info=info, split=split, axis=axis)
|
||||
@@ -0,0 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
class BaseDatasetsCLICommand(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def register_subcommand(parser: ArgumentParser):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from datasets.commands.delete_from_hub import DeleteFromHubCommand
|
||||
from datasets.commands.env import EnvironmentCommand
|
||||
from datasets.commands.test import TestCommand
|
||||
from datasets.utils.logging import set_verbosity_info
|
||||
|
||||
|
||||
def parse_unknown_args(unknown_args):
|
||||
return {key.lstrip("-"): value for key, value in zip(unknown_args[::2], unknown_args[1::2])}
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(
|
||||
"HuggingFace Datasets CLI tool", usage="datasets-cli <command> [<args>]", allow_abbrev=False
|
||||
)
|
||||
commands_parser = parser.add_subparsers(help="datasets-cli command helpers")
|
||||
set_verbosity_info()
|
||||
|
||||
# Register commands
|
||||
EnvironmentCommand.register_subcommand(commands_parser)
|
||||
TestCommand.register_subcommand(commands_parser)
|
||||
DeleteFromHubCommand.register_subcommand(commands_parser)
|
||||
|
||||
# Parse args
|
||||
args, unknown_args = parser.parse_known_args()
|
||||
if not hasattr(args, "func"):
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
kwargs = parse_unknown_args(unknown_args)
|
||||
|
||||
# Run
|
||||
service = args.func(args, **kwargs)
|
||||
service.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
from argparse import ArgumentParser
|
||||
from typing import Optional
|
||||
|
||||
from datasets.commands import BaseDatasetsCLICommand
|
||||
from datasets.hub import delete_from_hub
|
||||
|
||||
|
||||
def _command_factory(args):
|
||||
return DeleteFromHubCommand(
|
||||
args.dataset_id,
|
||||
args.config_name,
|
||||
args.token,
|
||||
args.revision,
|
||||
)
|
||||
|
||||
|
||||
class DeleteFromHubCommand(BaseDatasetsCLICommand):
|
||||
@staticmethod
|
||||
def register_subcommand(parser):
|
||||
parser: ArgumentParser = parser.add_parser("delete_from_hub", help="Delete dataset config from the Hub")
|
||||
parser.add_argument(
|
||||
"dataset_id", help="source dataset ID, e.g. USERNAME/DATASET_NAME or ORGANIZATION/DATASET_NAME"
|
||||
)
|
||||
parser.add_argument("config_name", help="config name to delete")
|
||||
parser.add_argument("--token", help="access token to the Hugging Face Hub")
|
||||
parser.add_argument("--revision", help="source revision")
|
||||
parser.set_defaults(func=_command_factory)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dataset_id: str,
|
||||
config_name: str,
|
||||
token: Optional[str],
|
||||
revision: Optional[str],
|
||||
):
|
||||
self._dataset_id = dataset_id
|
||||
self._config_name = config_name
|
||||
self._token = token
|
||||
self._revision = revision
|
||||
|
||||
def run(self) -> None:
|
||||
_ = delete_from_hub(self._dataset_id, self._config_name, revision=self._revision, token=self._token)
|
||||
@@ -0,0 +1,41 @@
|
||||
import platform
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import fsspec
|
||||
import huggingface_hub
|
||||
import pandas
|
||||
import pyarrow
|
||||
|
||||
from datasets import __version__ as version
|
||||
from datasets.commands import BaseDatasetsCLICommand
|
||||
|
||||
|
||||
def info_command_factory(_):
|
||||
return EnvironmentCommand()
|
||||
|
||||
|
||||
class EnvironmentCommand(BaseDatasetsCLICommand):
|
||||
@staticmethod
|
||||
def register_subcommand(parser: ArgumentParser):
|
||||
download_parser = parser.add_parser("env", help="Print relevant system environment info.")
|
||||
download_parser.set_defaults(func=info_command_factory)
|
||||
|
||||
def run(self):
|
||||
info = {
|
||||
"`datasets` version": version,
|
||||
"Platform": platform.platform(),
|
||||
"Python version": platform.python_version(),
|
||||
"`huggingface_hub` version": huggingface_hub.__version__,
|
||||
"PyArrow version": pyarrow.__version__,
|
||||
"Pandas version": pandas.__version__,
|
||||
"`fsspec` version": fsspec.__version__,
|
||||
}
|
||||
|
||||
print("\nCopy-and-paste the text below in your GitHub issue.\n")
|
||||
print(self.format_dict(info))
|
||||
|
||||
return info
|
||||
|
||||
@staticmethod
|
||||
def format_dict(d):
|
||||
return "\n".join([f"- {prop}: {val}" for prop, val in d.items()]) + "\n"
|
||||
@@ -0,0 +1,180 @@
|
||||
import logging
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from collections.abc import Generator
|
||||
from shutil import rmtree
|
||||
|
||||
import datasets.config
|
||||
from datasets.builder import DatasetBuilder
|
||||
from datasets.commands import BaseDatasetsCLICommand
|
||||
from datasets.download.download_manager import DownloadMode
|
||||
from datasets.info import DatasetInfosDict
|
||||
from datasets.load import dataset_module_factory, get_dataset_builder_class
|
||||
from datasets.utils.info_utils import VerificationMode
|
||||
from datasets.utils.logging import ERROR, get_logger
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _test_command_factory(args):
|
||||
return TestCommand(
|
||||
args.dataset,
|
||||
args.name,
|
||||
args.cache_dir,
|
||||
args.data_dir,
|
||||
args.all_configs,
|
||||
args.save_info or args.save_infos,
|
||||
args.ignore_verifications,
|
||||
args.force_redownload,
|
||||
args.clear_cache,
|
||||
args.num_proc,
|
||||
)
|
||||
|
||||
|
||||
class TestCommand(BaseDatasetsCLICommand):
|
||||
__test__ = False # to tell pytest it's not a test class
|
||||
|
||||
@staticmethod
|
||||
def register_subcommand(parser: ArgumentParser):
|
||||
test_parser = parser.add_parser("test", help="Test dataset loading.")
|
||||
test_parser.add_argument("--name", type=str, default=None, help="Dataset processing name")
|
||||
test_parser.add_argument(
|
||||
"--cache_dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Cache directory where the datasets are stored.",
|
||||
)
|
||||
test_parser.add_argument(
|
||||
"--data_dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Can be used to specify a manual directory to get the files from.",
|
||||
)
|
||||
test_parser.add_argument("--all_configs", action="store_true", help="Test all dataset configurations")
|
||||
test_parser.add_argument(
|
||||
"--save_info", action="store_true", help="Save the dataset infos in the dataset card (README.md)"
|
||||
)
|
||||
test_parser.add_argument(
|
||||
"--ignore_verifications",
|
||||
action="store_true",
|
||||
help="Run the test without checksums and splits checks.",
|
||||
)
|
||||
test_parser.add_argument("--force_redownload", action="store_true", help="Force dataset redownload")
|
||||
test_parser.add_argument(
|
||||
"--clear_cache",
|
||||
action="store_true",
|
||||
help="Remove downloaded files and cached datasets after each config test",
|
||||
)
|
||||
test_parser.add_argument("--num_proc", type=int, default=None, help="Number of processes")
|
||||
# aliases
|
||||
test_parser.add_argument("--save_infos", action="store_true", help="alias to save_info")
|
||||
test_parser.add_argument("dataset", type=str, help="Name of the dataset to download")
|
||||
test_parser.set_defaults(func=_test_command_factory)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dataset: str,
|
||||
name: str,
|
||||
cache_dir: str,
|
||||
data_dir: str,
|
||||
all_configs: bool,
|
||||
save_infos: bool,
|
||||
ignore_verifications: bool,
|
||||
force_redownload: bool,
|
||||
clear_cache: bool,
|
||||
num_proc: int,
|
||||
):
|
||||
self._dataset = dataset
|
||||
self._name = name
|
||||
self._cache_dir = cache_dir
|
||||
self._data_dir = data_dir
|
||||
self._all_configs = all_configs
|
||||
self._save_infos = save_infos
|
||||
self._ignore_verifications = ignore_verifications
|
||||
self._force_redownload = force_redownload
|
||||
self._clear_cache = clear_cache
|
||||
self._num_proc = num_proc
|
||||
if clear_cache and not cache_dir:
|
||||
print(
|
||||
"When --clear_cache is used, specifying a cache directory is mandatory.\n"
|
||||
"The 'download' folder of the cache directory and the dataset builder cache will be deleted after each configuration test.\n"
|
||||
"Please provide a --cache_dir that will be used to test the dataset."
|
||||
)
|
||||
exit(1)
|
||||
if save_infos:
|
||||
self._ignore_verifications = True
|
||||
|
||||
def run(self):
|
||||
logging.getLogger("filelock").setLevel(ERROR)
|
||||
if self._name is not None and self._all_configs:
|
||||
print("Both parameters `config` and `all_configs` can't be used at once.")
|
||||
exit(1)
|
||||
path, config_name = self._dataset, self._name
|
||||
module = dataset_module_factory(path)
|
||||
builder_cls = get_dataset_builder_class(module)
|
||||
n_builders = len(builder_cls.BUILDER_CONFIGS) if self._all_configs and builder_cls.BUILDER_CONFIGS else 1
|
||||
|
||||
def get_builders() -> Generator[DatasetBuilder, None, None]:
|
||||
if self._all_configs and builder_cls.BUILDER_CONFIGS:
|
||||
for i, config in enumerate(builder_cls.BUILDER_CONFIGS):
|
||||
if "config_name" in module.builder_kwargs:
|
||||
yield builder_cls(
|
||||
cache_dir=self._cache_dir,
|
||||
data_dir=self._data_dir,
|
||||
**module.builder_kwargs,
|
||||
)
|
||||
else:
|
||||
yield builder_cls(
|
||||
config_name=config.name,
|
||||
cache_dir=self._cache_dir,
|
||||
data_dir=self._data_dir,
|
||||
**module.builder_kwargs,
|
||||
)
|
||||
else:
|
||||
if "config_name" in module.builder_kwargs:
|
||||
yield builder_cls(cache_dir=self._cache_dir, data_dir=self._data_dir, **module.builder_kwargs)
|
||||
else:
|
||||
yield builder_cls(
|
||||
config_name=config_name,
|
||||
cache_dir=self._cache_dir,
|
||||
data_dir=self._data_dir,
|
||||
**module.builder_kwargs,
|
||||
)
|
||||
|
||||
for j, builder in enumerate(get_builders()):
|
||||
print(f"Testing builder '{builder.config.name}' ({j + 1}/{n_builders})")
|
||||
builder._record_checksums = os.path.exists(
|
||||
os.path.join(builder.get_imported_module_dir(), datasets.config.DATASETDICT_INFOS_FILENAME)
|
||||
) # record checksums only if we need to update a (deprecated) dataset_infos.json
|
||||
builder.download_and_prepare(
|
||||
download_mode=DownloadMode.REUSE_CACHE_IF_EXISTS
|
||||
if not self._force_redownload
|
||||
else DownloadMode.FORCE_REDOWNLOAD,
|
||||
verification_mode=VerificationMode.NO_CHECKS
|
||||
if self._ignore_verifications
|
||||
else VerificationMode.ALL_CHECKS,
|
||||
num_proc=self._num_proc,
|
||||
)
|
||||
builder.as_dataset()
|
||||
|
||||
# If save_infos=True, we create the dataset card (README.md)
|
||||
# The dataset_infos are saved in the YAML part of the README.md
|
||||
# This is to allow the user to upload them on HF afterwards.
|
||||
if self._save_infos:
|
||||
save_infos_dir = os.path.basename(path) if not os.path.isdir(path) else path
|
||||
os.makedirs(save_infos_dir, exist_ok=True)
|
||||
DatasetInfosDict(**{builder.config.name: builder.info}).write_to_directory(save_infos_dir)
|
||||
print(f"Dataset card saved at {os.path.join(save_infos_dir, datasets.config.REPOCARD_FILENAME)}")
|
||||
|
||||
# If clear_cache=True, the download folder and the dataset builder cache directory are deleted
|
||||
if self._clear_cache:
|
||||
if os.path.isdir(builder._cache_dir):
|
||||
logger.warning(f"Clearing cache at {builder._cache_dir}")
|
||||
rmtree(builder._cache_dir)
|
||||
download_dir = os.path.join(self._cache_dir, datasets.config.DOWNLOADED_DATASETS_DIR)
|
||||
if os.path.isdir(download_dir):
|
||||
logger.warning(f"Clearing cache at {download_dir}")
|
||||
rmtree(download_dir)
|
||||
|
||||
print("Test successful.")
|
||||
@@ -0,0 +1,277 @@
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from huggingface_hub import constants
|
||||
from packaging import version
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__.split(".", 1)[0]) # to avoid circular import from .utils.logging
|
||||
|
||||
# Datasets
|
||||
S3_DATASETS_BUCKET_PREFIX = "https://s3.amazonaws.com/datasets.huggingface.co/datasets/datasets"
|
||||
CLOUDFRONT_DATASETS_DISTRIB_PREFIX = "https://cdn-datasets.huggingface.co/datasets/datasets"
|
||||
REPO_DATASETS_URL = "https://raw.githubusercontent.com/huggingface/datasets/{revision}/datasets/{path}/{name}"
|
||||
|
||||
# Hub
|
||||
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
|
||||
HUB_DATASETS_URL = HF_ENDPOINT + "/datasets/{repo_id}/resolve/{revision}/{path}"
|
||||
HUB_DATASETS_HFFS_URL = "hf://datasets/{repo_id}@{revision}/{path}"
|
||||
HUB_DEFAULT_VERSION = "main"
|
||||
|
||||
PY_VERSION = version.parse(platform.python_version())
|
||||
|
||||
# General environment variables accepted values for booleans
|
||||
ENV_VARS_TRUE_VALUES = {"1", "ON", "YES", "TRUE"}
|
||||
ENV_VARS_FALSE_VALUES = {"0", "OFF", "NO", "FALSE"}
|
||||
ENV_VARS_TRUE_AND_AUTO_VALUES = ENV_VARS_TRUE_VALUES.union({"AUTO"})
|
||||
ENV_VARS_FALSE_AND_AUTO_VALUES = ENV_VARS_FALSE_VALUES.union({"AUTO"})
|
||||
|
||||
|
||||
# Imports
|
||||
DILL_VERSION = version.parse(importlib.metadata.version("dill"))
|
||||
FSSPEC_VERSION = version.parse(importlib.metadata.version("fsspec"))
|
||||
PANDAS_VERSION = version.parse(importlib.metadata.version("pandas"))
|
||||
PYARROW_VERSION = version.parse(importlib.metadata.version("pyarrow"))
|
||||
HF_HUB_VERSION = version.parse(importlib.metadata.version("huggingface_hub"))
|
||||
|
||||
USE_TF = os.environ.get("USE_TF", "AUTO").upper()
|
||||
USE_TORCH = os.environ.get("USE_TORCH", "AUTO").upper()
|
||||
USE_JAX = os.environ.get("USE_JAX", "AUTO").upper()
|
||||
|
||||
TORCH_VERSION = "N/A"
|
||||
TORCH_AVAILABLE = False
|
||||
|
||||
if USE_TORCH in ENV_VARS_TRUE_AND_AUTO_VALUES and USE_TF not in ENV_VARS_TRUE_VALUES:
|
||||
TORCH_AVAILABLE = importlib.util.find_spec("torch") is not None
|
||||
if TORCH_AVAILABLE:
|
||||
try:
|
||||
TORCH_VERSION = version.parse(importlib.metadata.version("torch"))
|
||||
logger.debug(f"PyTorch version {TORCH_VERSION} available.")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
else:
|
||||
logger.info("Disabling PyTorch because USE_TF is set")
|
||||
|
||||
POLARS_VERSION = "N/A"
|
||||
POLARS_AVAILABLE = importlib.util.find_spec("polars") is not None
|
||||
|
||||
if POLARS_AVAILABLE:
|
||||
try:
|
||||
POLARS_VERSION = version.parse(importlib.metadata.version("polars"))
|
||||
logger.debug(f"Polars version {POLARS_VERSION} available.")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
DUCKDB_VERSION = "N/A"
|
||||
DUCKDB_AVAILABLE = importlib.util.find_spec("duckdb") is not None
|
||||
|
||||
if DUCKDB_AVAILABLE:
|
||||
try:
|
||||
DUCKDB_VERSION = version.parse(importlib.metadata.version("duckdb"))
|
||||
logger.debug(f"Duckdb version {DUCKDB_VERSION} available.")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
|
||||
TF_VERSION = "N/A"
|
||||
TF_AVAILABLE = False
|
||||
|
||||
if USE_TF in ENV_VARS_TRUE_AND_AUTO_VALUES and USE_TORCH not in ENV_VARS_TRUE_VALUES:
|
||||
TF_AVAILABLE = importlib.util.find_spec("tensorflow") is not None
|
||||
if TF_AVAILABLE:
|
||||
# For the metadata, we have to look for both tensorflow and tensorflow-cpu
|
||||
for package in [
|
||||
"tensorflow",
|
||||
"tensorflow-cpu",
|
||||
"tensorflow-gpu",
|
||||
"tf-nightly",
|
||||
"tf-nightly-cpu",
|
||||
"tf-nightly-gpu",
|
||||
"intel-tensorflow",
|
||||
"tensorflow-rocm",
|
||||
"tensorflow-macos",
|
||||
]:
|
||||
try:
|
||||
TF_VERSION = version.parse(importlib.metadata.version(package))
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
TF_AVAILABLE = False
|
||||
if TF_AVAILABLE:
|
||||
if TF_VERSION.major < 2:
|
||||
logger.info(f"TensorFlow found but with version {TF_VERSION}. `datasets` requires version 2 minimum.")
|
||||
TF_AVAILABLE = False
|
||||
else:
|
||||
logger.info(f"TensorFlow version {TF_VERSION} available.")
|
||||
else:
|
||||
logger.info("Disabling Tensorflow because USE_TORCH is set")
|
||||
|
||||
|
||||
JAX_VERSION = "N/A"
|
||||
JAX_AVAILABLE = False
|
||||
|
||||
if USE_JAX in ENV_VARS_TRUE_AND_AUTO_VALUES:
|
||||
JAX_AVAILABLE = importlib.util.find_spec("jax") is not None and importlib.util.find_spec("jaxlib") is not None
|
||||
if JAX_AVAILABLE:
|
||||
try:
|
||||
JAX_VERSION = version.parse(importlib.metadata.version("jax"))
|
||||
logger.info(f"JAX version {JAX_VERSION} available.")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
else:
|
||||
logger.info("Disabling JAX because USE_JAX is set to False")
|
||||
|
||||
|
||||
# Optional tools for data loading
|
||||
SQLALCHEMY_AVAILABLE = importlib.util.find_spec("sqlalchemy") is not None
|
||||
|
||||
# Optional tools for file parsing and feature decoding
|
||||
PIL_AVAILABLE = importlib.util.find_spec("PIL") is not None
|
||||
IS_OPUS_SUPPORTED = True
|
||||
IS_MP3_SUPPORTED = True
|
||||
TORCHCODEC_AVAILABLE = importlib.util.find_spec("torchcodec") is not None
|
||||
TORCHVISION_AVAILABLE = importlib.util.find_spec("torchvision") is not None
|
||||
PDFPLUMBER_AVAILABLE = importlib.util.find_spec("pdfplumber") is not None
|
||||
NIBABEL_AVAILABLE = importlib.util.find_spec("nibabel") is not None
|
||||
TRIMESH_AVAILABLE = importlib.util.find_spec("trimesh") is not None
|
||||
TEICH_AVAILABLE = importlib.util.find_spec("teich") is not None
|
||||
|
||||
# Optional compression tools
|
||||
RARFILE_AVAILABLE = importlib.util.find_spec("rarfile") is not None
|
||||
ZSTANDARD_AVAILABLE = importlib.util.find_spec("zstandard") is not None
|
||||
LZ4_AVAILABLE = importlib.util.find_spec("lz4") is not None
|
||||
PY7ZR_AVAILABLE = importlib.util.find_spec("py7zr") is not None
|
||||
|
||||
# Cache location
|
||||
DEFAULT_XDG_CACHE_HOME = "~/.cache"
|
||||
XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", DEFAULT_XDG_CACHE_HOME)
|
||||
DEFAULT_HF_CACHE_HOME = os.path.join(XDG_CACHE_HOME, "huggingface")
|
||||
HF_CACHE_HOME = os.path.expanduser(os.getenv("HF_HOME", DEFAULT_HF_CACHE_HOME))
|
||||
|
||||
DEFAULT_HF_DATASETS_CACHE = os.path.join(HF_CACHE_HOME, "datasets")
|
||||
HF_DATASETS_CACHE = Path(os.getenv("HF_DATASETS_CACHE", DEFAULT_HF_DATASETS_CACHE))
|
||||
|
||||
DEFAULT_HF_MODULES_CACHE = os.path.join(HF_CACHE_HOME, "modules")
|
||||
HF_MODULES_CACHE = Path(os.getenv("HF_MODULES_CACHE", DEFAULT_HF_MODULES_CACHE))
|
||||
|
||||
DOWNLOADED_DATASETS_DIR = "downloads"
|
||||
DEFAULT_DOWNLOADED_DATASETS_PATH = os.path.join(HF_DATASETS_CACHE, DOWNLOADED_DATASETS_DIR)
|
||||
DOWNLOADED_DATASETS_PATH = Path(os.getenv("HF_DATASETS_DOWNLOADED_DATASETS_PATH", DEFAULT_DOWNLOADED_DATASETS_PATH))
|
||||
|
||||
EXTRACTED_DATASETS_DIR = "extracted"
|
||||
DEFAULT_EXTRACTED_DATASETS_PATH = os.path.join(DEFAULT_DOWNLOADED_DATASETS_PATH, EXTRACTED_DATASETS_DIR)
|
||||
EXTRACTED_DATASETS_PATH = Path(os.getenv("HF_DATASETS_EXTRACTED_DATASETS_PATH", DEFAULT_EXTRACTED_DATASETS_PATH))
|
||||
|
||||
# Cached dataset info options
|
||||
SAVE_ORIGINAL_SHARD_LENGTHS = False
|
||||
|
||||
# Download count for the website
|
||||
HF_UPDATE_DOWNLOAD_COUNTS = (
|
||||
os.environ.get("HF_UPDATE_DOWNLOAD_COUNTS", "AUTO").upper() in ENV_VARS_TRUE_AND_AUTO_VALUES
|
||||
)
|
||||
|
||||
# For downloads and to check remote files metadata
|
||||
HF_DATASETS_MULTITHREADING_MAX_WORKERS = 16
|
||||
|
||||
# Dataset viewer API
|
||||
USE_PARQUET_EXPORT = True
|
||||
|
||||
# Batch size constants. For more info, see:
|
||||
# https://github.com/apache/arrow/blob/master/docs/source/cpp/arrays.rst#size-limitations-and-recommendations)
|
||||
DEFAULT_MAX_BATCH_SIZE = 1000
|
||||
|
||||
DEFAULT_CDC_OPTIONS = {"min_chunk_size": 256 * 1024, "max_chunk_size": 1024 * 1024, "norm_level": 0}
|
||||
|
||||
# Size of the preloaded record batch in `Dataset.__iter__`
|
||||
ARROW_READER_BATCH_SIZE_IN_DATASET_ITER = 10
|
||||
|
||||
# Max uncompressed shard size in bytes (e.g. to shard parquet datasets in push_to_hub or download_and_prepare)
|
||||
MAX_SHARD_SIZE = "500MB"
|
||||
|
||||
# Max uncompressed row group size in bytes (e.g. for parquet files in push_to_hub or download_and_prepare)
|
||||
MAX_ROW_GROUP_SIZE = "100MB"
|
||||
|
||||
# Parquet configuration
|
||||
PARQUET_ROW_GROUP_SIZE_FOR_AUDIO_DATASETS = None
|
||||
PARQUET_ROW_GROUP_SIZE_FOR_IMAGE_DATASETS = None
|
||||
PARQUET_ROW_GROUP_SIZE_FOR_BINARY_DATASETS = None
|
||||
PARQUET_ROW_GROUP_SIZE_FOR_VIDEO_DATASETS = None
|
||||
|
||||
# Arrow configuration
|
||||
ARROW_RECORD_BATCH_SIZE_FOR_AUDIO_DATASETS = 100
|
||||
ARROW_RECORD_BATCH_SIZE_FOR_IMAGE_DATASETS = 100
|
||||
ARROW_RECORD_BATCH_SIZE_FOR_BINARY_DATASETS = 100
|
||||
ARROW_RECORD_BATCH_SIZE_FOR_VIDEO_DATASETS = 10
|
||||
|
||||
# Offline mode
|
||||
_offline = os.environ.get("HF_DATASETS_OFFLINE")
|
||||
HF_HUB_OFFLINE = constants.HF_HUB_OFFLINE if _offline is None else _offline.upper() in ENV_VARS_TRUE_VALUES
|
||||
HF_DATASETS_OFFLINE = HF_HUB_OFFLINE # kept for backward-compatibility
|
||||
|
||||
# Here, `True` will disable progress bars globally without possibility of enabling it
|
||||
# programmatically. `False` will enable them without possibility of disabling them.
|
||||
# If environment variable is not set (None), then the user is free to enable/disable
|
||||
# them programmatically.
|
||||
# TL;DR: env variable has priority over code
|
||||
__HF_DATASETS_DISABLE_PROGRESS_BARS = os.environ.get("HF_DATASETS_DISABLE_PROGRESS_BARS")
|
||||
HF_DATASETS_DISABLE_PROGRESS_BARS: Optional[bool] = (
|
||||
__HF_DATASETS_DISABLE_PROGRESS_BARS.upper() in ENV_VARS_TRUE_VALUES
|
||||
if __HF_DATASETS_DISABLE_PROGRESS_BARS is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# In-memory
|
||||
DEFAULT_IN_MEMORY_MAX_SIZE = 0 # Disabled
|
||||
IN_MEMORY_MAX_SIZE = float(os.environ.get("HF_DATASETS_IN_MEMORY_MAX_SIZE", DEFAULT_IN_MEMORY_MAX_SIZE))
|
||||
|
||||
# File names
|
||||
DATASET_ARROW_FILENAME = "dataset.arrow"
|
||||
DATASET_INDICES_FILENAME = "indices.arrow"
|
||||
DATASET_STATE_JSON_FILENAME = "state.json"
|
||||
DATASET_INFO_FILENAME = "dataset_info.json"
|
||||
DATASETDICT_INFOS_FILENAME = "dataset_infos.json"
|
||||
LICENSE_FILENAME = "LICENSE"
|
||||
DATASETDICT_JSON_FILENAME = "dataset_dict.json"
|
||||
METADATA_CONFIGS_FIELD = "configs"
|
||||
REPOCARD_FILENAME = "README.md"
|
||||
REPOYAML_FILENAME = ".huggingface.yaml"
|
||||
|
||||
MODULE_NAME_FOR_DYNAMIC_MODULES = "datasets_modules"
|
||||
|
||||
MAX_DATASET_CONFIG_ID_READABLE_LENGTH = 255
|
||||
|
||||
# Temporary cache directory prefix
|
||||
TEMP_CACHE_DIR_PREFIX = "hf_datasets-"
|
||||
|
||||
# Streaming
|
||||
STREAMING_READ_MAX_RETRIES = 20
|
||||
STREAMING_READ_RETRY_INTERVAL = 5
|
||||
STREAMING_READ_SERVER_UNAVAILABLE_RETRY_INTERVAL = 20
|
||||
STREAMING_READ_RATE_LIMIT_RETRY_INTERVAL = 60
|
||||
STREAMING_OPEN_MAX_RETRIES = 20
|
||||
STREAMING_OPEN_RETRY_INTERVAL = 5
|
||||
|
||||
# Datasets repositories exploration
|
||||
ARCHIVES_MAX_NUMBER_FOR_MODULE_INFERENCE = 10
|
||||
|
||||
# Async map functions
|
||||
MAX_NUM_RUNNING_ASYNC_MAP_FUNCTIONS_IN_PARALLEL = 1000
|
||||
|
||||
# Progress bars
|
||||
PBAR_REFRESH_TIME_INTERVAL = 0.05 # 20 progress updates per sec
|
||||
|
||||
# Maximum number of uploaded files per commit
|
||||
UPLOADS_MAX_NUMBER_PER_COMMIT = 50
|
||||
|
||||
# Backward compatibility
|
||||
MAX_TABLE_NBYTES_FOR_PICKLING = 4 << 30
|
||||
|
||||
# Time to let PyArrow close threads gracefully
|
||||
SLEEP_TIME_ON_THREADS_SHUTDOWN = 5
|
||||
@@ -0,0 +1,829 @@
|
||||
import os
|
||||
import re
|
||||
from functools import partial
|
||||
from glob import has_magic
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import huggingface_hub
|
||||
from fsspec.core import url_to_fs
|
||||
from huggingface_hub import HfFileSystem
|
||||
from packaging import version
|
||||
from tqdm.contrib.concurrent import thread_map
|
||||
|
||||
from . import config
|
||||
from .download import DownloadConfig
|
||||
from .naming import _split_re
|
||||
from .splits import Split
|
||||
from .utils import logging
|
||||
from .utils import tqdm as hf_tqdm
|
||||
from .utils.file_utils import _prepare_path_and_storage_options, is_local_path, is_relative_path, xbasename, xjoin
|
||||
from .utils.py_utils import string_to_dict
|
||||
|
||||
|
||||
SingleOriginMetadata = Union[tuple[str, str], tuple[str], tuple[()]]
|
||||
|
||||
|
||||
SANITIZED_DEFAULT_SPLIT = str(Split.TRAIN)
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
class Url(str):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyDatasetError(FileNotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
SPLIT_PATTERN_SHARDED = "data/{split}-[0-9][0-9][0-9][0-9][0-9]-of-[0-9][0-9][0-9][0-9][0-9]*.*"
|
||||
|
||||
SPLIT_KEYWORDS = {
|
||||
Split.TRAIN: ["train", "training"],
|
||||
Split.VALIDATION: ["validation", "valid", "dev", "val"],
|
||||
Split.TEST: ["test", "testing", "eval", "evaluation"],
|
||||
}
|
||||
NON_WORDS_CHARS = "-._ 0-9"
|
||||
if config.FSSPEC_VERSION < version.parse("2023.9.0"):
|
||||
KEYWORDS_IN_FILENAME_BASE_PATTERNS = ["**[{sep}/]{keyword}[{sep}]*", "{keyword}[{sep}]*"]
|
||||
KEYWORDS_IN_DIR_NAME_BASE_PATTERNS = [
|
||||
"{keyword}/**",
|
||||
"{keyword}[{sep}]*/**",
|
||||
"**[{sep}/]{keyword}/**",
|
||||
"**[{sep}/]{keyword}[{sep}]*/**",
|
||||
]
|
||||
elif config.FSSPEC_VERSION < version.parse("2023.12.0"):
|
||||
KEYWORDS_IN_FILENAME_BASE_PATTERNS = ["**/*[{sep}/]{keyword}[{sep}]*", "{keyword}[{sep}]*"]
|
||||
KEYWORDS_IN_DIR_NAME_BASE_PATTERNS = [
|
||||
"{keyword}/**/*",
|
||||
"{keyword}[{sep}]*/**/*",
|
||||
"**/*[{sep}/]{keyword}/**/*",
|
||||
"**/*[{sep}/]{keyword}[{sep}]*/**/*",
|
||||
]
|
||||
else:
|
||||
KEYWORDS_IN_FILENAME_BASE_PATTERNS = ["**/{keyword}[{sep}]*", "**/*[{sep}]{keyword}[{sep}]*"]
|
||||
KEYWORDS_IN_DIR_NAME_BASE_PATTERNS = [
|
||||
"**/{keyword}/**",
|
||||
"**/{keyword}[{sep}]*/**",
|
||||
"**/*[{sep}]{keyword}/**",
|
||||
"**/*[{sep}]{keyword}[{sep}]*/**",
|
||||
]
|
||||
|
||||
DEFAULT_SPLITS = [Split.TRAIN, Split.VALIDATION, Split.TEST]
|
||||
DEFAULT_PATTERNS_SPLIT_IN_FILENAME = {
|
||||
split: [
|
||||
pattern.format(keyword=keyword, sep=NON_WORDS_CHARS)
|
||||
for keyword in SPLIT_KEYWORDS[split]
|
||||
for pattern in KEYWORDS_IN_FILENAME_BASE_PATTERNS
|
||||
]
|
||||
for split in DEFAULT_SPLITS
|
||||
}
|
||||
DEFAULT_PATTERNS_SPLIT_IN_DIR_NAME = {
|
||||
split: [
|
||||
pattern.format(keyword=keyword, sep=NON_WORDS_CHARS)
|
||||
for keyword in SPLIT_KEYWORDS[split]
|
||||
for pattern in KEYWORDS_IN_DIR_NAME_BASE_PATTERNS
|
||||
]
|
||||
for split in DEFAULT_SPLITS
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_PATTERNS_ALL = {
|
||||
Split.TRAIN: ["**"],
|
||||
}
|
||||
|
||||
DEFAULT_PATTERNS_LOGS = {"logs": ["**/*.eval"]}
|
||||
|
||||
ALL_SPLIT_PATTERNS = [SPLIT_PATTERN_SHARDED]
|
||||
ALL_DEFAULT_PATTERNS = [
|
||||
DEFAULT_PATTERNS_LOGS,
|
||||
DEFAULT_PATTERNS_SPLIT_IN_DIR_NAME,
|
||||
DEFAULT_PATTERNS_SPLIT_IN_FILENAME,
|
||||
DEFAULT_PATTERNS_ALL,
|
||||
]
|
||||
WILDCARD_CHARACTERS = "*[]"
|
||||
FILES_TO_IGNORE = [
|
||||
"README.md",
|
||||
"config.json",
|
||||
"dataset_info.json",
|
||||
"dataset_infos.json",
|
||||
"dummy_data.zip",
|
||||
"dataset_dict.json",
|
||||
]
|
||||
|
||||
|
||||
def contains_wildcards(pattern: str) -> bool:
|
||||
return any(wildcard_character in pattern for wildcard_character in WILDCARD_CHARACTERS)
|
||||
|
||||
|
||||
def sanitize_patterns(patterns: Union[dict, list, str]) -> dict[str, Union[list[str], "DataFilesList"]]:
|
||||
"""
|
||||
Take the data_files patterns from the user, and format them into a dictionary.
|
||||
Each key is the name of the split, and each value is a list of data files patterns (paths or urls).
|
||||
The default split is "train".
|
||||
|
||||
Returns:
|
||||
patterns: dictionary of split_name -> list of patterns
|
||||
"""
|
||||
if isinstance(patterns, dict):
|
||||
return {str(key): value if isinstance(value, list) else [value] for key, value in patterns.items()}
|
||||
elif isinstance(patterns, str):
|
||||
return {SANITIZED_DEFAULT_SPLIT: [patterns]}
|
||||
elif isinstance(patterns, list):
|
||||
if any(isinstance(pattern, dict) for pattern in patterns):
|
||||
for pattern in patterns:
|
||||
if not (
|
||||
isinstance(pattern, dict)
|
||||
and len(pattern) == 2
|
||||
and "split" in pattern
|
||||
and isinstance(pattern.get("path"), (str, list))
|
||||
):
|
||||
raise ValueError(
|
||||
"Invalid format for data_files entry. "
|
||||
"Each item must be a dictionary with the structure "
|
||||
"{'split': <split_name>, 'path': <path_or_list_of_paths>}.\n"
|
||||
f"Received: {pattern}"
|
||||
)
|
||||
splits = [pattern["split"] for pattern in patterns]
|
||||
if len(set(splits)) != len(splits):
|
||||
raise ValueError(f"Some splits are duplicated in data_files: {splits}")
|
||||
return {
|
||||
str(pattern["split"]): pattern["path"] if isinstance(pattern["path"], list) else [pattern["path"]]
|
||||
for pattern in patterns
|
||||
}
|
||||
else:
|
||||
return {SANITIZED_DEFAULT_SPLIT: patterns}
|
||||
else:
|
||||
return sanitize_patterns(list(patterns))
|
||||
|
||||
|
||||
def _is_inside_unrequested_special_dir(matched_rel_path: str, pattern: str) -> bool:
|
||||
"""
|
||||
When a path matches a pattern, we additionally check if it's inside a special directory
|
||||
we ignore by default (if it starts with a double underscore).
|
||||
|
||||
Users can still explicitly request a filepath inside such a directory if "__pycache__" is
|
||||
mentioned explicitly in the requested pattern.
|
||||
|
||||
Some examples:
|
||||
|
||||
base directory:
|
||||
|
||||
./
|
||||
└── __pycache__
|
||||
└── b.txt
|
||||
|
||||
>>> _is_inside_unrequested_special_dir("__pycache__/b.txt", "**")
|
||||
True
|
||||
>>> _is_inside_unrequested_special_dir("__pycache__/b.txt", "*/b.txt")
|
||||
True
|
||||
>>> _is_inside_unrequested_special_dir("__pycache__/b.txt", "__pycache__/*")
|
||||
False
|
||||
>>> _is_inside_unrequested_special_dir("__pycache__/b.txt", "__*/*")
|
||||
False
|
||||
"""
|
||||
# We just need to check if every special directories from the path is present explicitly in the pattern.
|
||||
# Since we assume that the path matches the pattern, it's equivalent to counting that both
|
||||
# the parent path and the parent pattern have the same number of special directories.
|
||||
data_dirs_to_ignore_in_path = [part for part in PurePath(matched_rel_path).parent.parts if part.startswith("__")]
|
||||
data_dirs_to_ignore_in_pattern = [part for part in PurePath(pattern).parent.parts if part.startswith("__")]
|
||||
return len(data_dirs_to_ignore_in_path) != len(data_dirs_to_ignore_in_pattern)
|
||||
|
||||
|
||||
def _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(matched_rel_path: str, pattern: str) -> bool:
|
||||
"""
|
||||
When a path matches a pattern, we additionally check if it's a hidden file or if it's inside
|
||||
a hidden directory we ignore by default, i.e. if the file name or a parent directory name starts with a dot.
|
||||
|
||||
Users can still explicitly request a filepath that is hidden or is inside a hidden directory
|
||||
if the hidden part is mentioned explicitly in the requested pattern.
|
||||
|
||||
Some examples:
|
||||
|
||||
base directory:
|
||||
|
||||
./
|
||||
└── .hidden_file.txt
|
||||
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_file.txt", "**")
|
||||
True
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_file.txt", ".*")
|
||||
False
|
||||
|
||||
base directory:
|
||||
|
||||
./
|
||||
└── .hidden_dir
|
||||
└── a.txt
|
||||
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/a.txt", "**")
|
||||
True
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/a.txt", ".*/*")
|
||||
False
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/a.txt", ".hidden_dir/*")
|
||||
False
|
||||
|
||||
base directory:
|
||||
|
||||
./
|
||||
└── .hidden_dir
|
||||
└── .hidden_file.txt
|
||||
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/.hidden_file.txt", "**")
|
||||
True
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/.hidden_file.txt", ".*/*")
|
||||
True
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/.hidden_file.txt", ".*/.*")
|
||||
False
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/.hidden_file.txt", ".hidden_dir/*")
|
||||
True
|
||||
>>> _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(".hidden_dir/.hidden_file.txt", ".hidden_dir/.*")
|
||||
False
|
||||
"""
|
||||
# We just need to check if every hidden part from the path is present explicitly in the pattern.
|
||||
# Since we assume that the path matches the pattern, it's equivalent to counting that both
|
||||
# the path and the pattern have the same number of hidden parts.
|
||||
hidden_directories_in_path = [
|
||||
part for part in PurePath(matched_rel_path).parts if part.startswith(".") and not set(part) == {"."}
|
||||
]
|
||||
hidden_directories_in_pattern = [
|
||||
part for part in PurePath(pattern).parts if part.startswith(".") and not set(part) == {"."}
|
||||
]
|
||||
return len(hidden_directories_in_path) != len(hidden_directories_in_pattern)
|
||||
|
||||
|
||||
def _get_data_files_patterns(pattern_resolver: Callable[[str], list[str]]) -> dict[str, list[str]]:
|
||||
"""
|
||||
Get the default pattern from a directory or repository by testing all the supported patterns.
|
||||
The first patterns to return a non-empty list of data files is returned.
|
||||
|
||||
In order, it first tests if SPLIT_PATTERN_SHARDED works, otherwise it tests the patterns in ALL_DEFAULT_PATTERNS.
|
||||
"""
|
||||
# first check the split patterns like data/{split}-00000-of-00001.parquet
|
||||
for split_pattern in ALL_SPLIT_PATTERNS:
|
||||
pattern = split_pattern.replace("{split}", "*")
|
||||
try:
|
||||
data_files = pattern_resolver(pattern)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if len(data_files) > 0:
|
||||
splits: set[str] = set()
|
||||
for p in data_files:
|
||||
p_parts = string_to_dict(xbasename(p), xbasename(split_pattern))
|
||||
assert p_parts is not None
|
||||
splits.add(p_parts["split"])
|
||||
|
||||
if any(not re.match(_split_re, split) for split in splits):
|
||||
raise ValueError(f"Split name should match '{_split_re}'' but got '{splits}'.")
|
||||
sorted_splits = [str(split) for split in DEFAULT_SPLITS if split in splits] + sorted(
|
||||
splits - {str(split) for split in DEFAULT_SPLITS}
|
||||
)
|
||||
return {split: [split_pattern.format(split=split)] for split in sorted_splits}
|
||||
# then check the default patterns based on train/valid/test splits
|
||||
for patterns_dict in ALL_DEFAULT_PATTERNS:
|
||||
non_empty_splits = []
|
||||
for split, patterns in patterns_dict.items():
|
||||
for pattern in patterns:
|
||||
try:
|
||||
data_files = pattern_resolver(pattern)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if len(data_files) > 0:
|
||||
non_empty_splits.append(split)
|
||||
break
|
||||
if non_empty_splits:
|
||||
return {split: patterns_dict[split] for split in non_empty_splits}
|
||||
raise FileNotFoundError(f"Couldn't resolve pattern {pattern} with resolver {pattern_resolver}")
|
||||
|
||||
|
||||
def resolve_pattern(
|
||||
pattern: str,
|
||||
base_path: str,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Resolve the paths and URLs of the data files from the pattern passed by the user.
|
||||
|
||||
You can use patterns to resolve multiple local files. Here are a few examples:
|
||||
- *.csv to match all the CSV files at the first level
|
||||
- **.csv to match all the CSV files at any level
|
||||
- data/* to match all the files inside "data"
|
||||
- data/** to match all the files inside "data" and its subdirectories
|
||||
|
||||
The patterns are resolved using the fsspec glob. In fsspec>=2023.12.0 this is equivalent to
|
||||
Python's glob.glob, Path.glob, Path.match and fnmatch where ** is unsupported with a prefix/suffix
|
||||
other than a forward slash /.
|
||||
|
||||
More generally:
|
||||
- '*' matches any character except a forward-slash (to match just the file or directory name)
|
||||
- '**' matches any character including a forward-slash /
|
||||
|
||||
Hidden files and directories (i.e. whose names start with a dot) are ignored, unless they are explicitly requested.
|
||||
The same applies to special directories that start with a double underscore like "__pycache__".
|
||||
You can still include one if the pattern explicitly mentions it:
|
||||
- to include a hidden file: "*/.hidden.txt" or "*/.*"
|
||||
- to include a hidden directory: ".hidden/*" or ".*/*"
|
||||
- to include a special directory: "__special__/*" or "__*/*"
|
||||
|
||||
Example::
|
||||
|
||||
>>> from datasets.data_files import resolve_pattern
|
||||
>>> base_path = "."
|
||||
>>> resolve_pattern("docs/**/*.py", base_path)
|
||||
[/Users/mariosasko/Desktop/projects/datasets/docs/source/_config.py']
|
||||
|
||||
Args:
|
||||
pattern (str): Unix pattern or paths or URLs of the data files to resolve.
|
||||
The paths can be absolute or relative to base_path.
|
||||
Remote filesystems using fsspec are supported, e.g. with the hf:// protocol.
|
||||
base_path (str): Base path to use when resolving relative paths.
|
||||
allowed_extensions (Optional[list], optional): White-list of file extensions to use. Defaults to None (all extensions).
|
||||
For example: allowed_extensions=[".csv", ".json", ".txt", ".parquet"]
|
||||
download_config ([`DownloadConfig`], *optional*): Specific download configuration parameters.
|
||||
Returns:
|
||||
List[str]: List of paths or URLs to the local or remote files that match the patterns.
|
||||
"""
|
||||
if is_relative_path(pattern):
|
||||
pattern = xjoin(base_path, pattern)
|
||||
elif is_local_path(pattern):
|
||||
base_path = os.path.splitdrive(pattern)[0] + os.sep
|
||||
else:
|
||||
base_path = ""
|
||||
pattern, storage_options = _prepare_path_and_storage_options(pattern, download_config=download_config)
|
||||
fs, fs_pattern = url_to_fs(pattern, **storage_options)
|
||||
files_to_ignore = set(FILES_TO_IGNORE) - {xbasename(pattern)}
|
||||
protocol = (
|
||||
pattern.split("://")[0]
|
||||
if "://" in pattern
|
||||
else (fs.protocol if isinstance(fs.protocol, str) else fs.protocol[0])
|
||||
)
|
||||
protocol_prefix = protocol + "://" if protocol != "file" else ""
|
||||
glob_kwargs = {}
|
||||
if protocol == "hf":
|
||||
# 10 times faster glob with detail=True (ignores costly info like lastCommit)
|
||||
glob_kwargs["expand_info"] = False
|
||||
|
||||
# if the pattern contains hops like "zip://csv/*.csv::data.zip", we need to keep them after globbing
|
||||
_, *rest_hops = pattern.split("::")
|
||||
matched_paths = []
|
||||
for filepath, info in fs.glob(fs_pattern, detail=True, **glob_kwargs).items():
|
||||
if not (info["type"] == "file" or (info.get("islink") and os.path.isfile(os.path.realpath(filepath)))) or (
|
||||
xbasename(filepath) in files_to_ignore
|
||||
):
|
||||
continue
|
||||
if _is_inside_unrequested_special_dir(filepath, fs_pattern):
|
||||
continue
|
||||
if _is_unrequested_hidden_file_or_is_inside_unrequested_hidden_dir(filepath, fs_pattern):
|
||||
continue
|
||||
filepath = filepath if "://" in filepath else protocol_prefix + filepath
|
||||
if rest_hops:
|
||||
filepath = "::".join([filepath] + rest_hops)
|
||||
matched_paths.append(filepath)
|
||||
# ignore .ipynb and __pycache__, but keep /../
|
||||
if allowed_extensions is not None:
|
||||
out = [
|
||||
filepath
|
||||
for filepath in matched_paths
|
||||
if any("." + suffix in allowed_extensions for suffix in xbasename(filepath).split(".")[1:])
|
||||
]
|
||||
if len(out) < len(matched_paths):
|
||||
invalid_matched_files = list(set(matched_paths) - set(out))
|
||||
logger.info(
|
||||
f"Some files matched the pattern '{pattern}' but don't have valid data file extensions: {invalid_matched_files}"
|
||||
)
|
||||
else:
|
||||
out = matched_paths
|
||||
if not out:
|
||||
error_msg = f"Unable to find '{pattern}'"
|
||||
if allowed_extensions is not None:
|
||||
error_msg += f" with any supported extension {list(allowed_extensions)}"
|
||||
raise FileNotFoundError(error_msg)
|
||||
return out
|
||||
|
||||
|
||||
def get_data_patterns(base_path: str, download_config: Optional[DownloadConfig] = None) -> dict[str, list[str]]:
|
||||
"""
|
||||
Get the default pattern from a directory testing all the supported patterns.
|
||||
The first patterns to return a non-empty list of data files is returned.
|
||||
|
||||
Some examples of supported patterns:
|
||||
|
||||
Input:
|
||||
|
||||
my_dataset_repository/
|
||||
├── README.md
|
||||
└── dataset.csv
|
||||
|
||||
Output:
|
||||
|
||||
{'train': ['**']}
|
||||
|
||||
Input:
|
||||
|
||||
my_dataset_repository/
|
||||
├── README.md
|
||||
├── train.csv
|
||||
└── test.csv
|
||||
|
||||
my_dataset_repository/
|
||||
├── README.md
|
||||
└── data/
|
||||
├── train.csv
|
||||
└── test.csv
|
||||
|
||||
my_dataset_repository/
|
||||
├── README.md
|
||||
├── train_0.csv
|
||||
├── train_1.csv
|
||||
├── train_2.csv
|
||||
├── train_3.csv
|
||||
├── test_0.csv
|
||||
└── test_1.csv
|
||||
|
||||
Output:
|
||||
|
||||
{'train': ['**/train[-._ 0-9]*', '**/*[-._ 0-9]train[-._ 0-9]*', '**/training[-._ 0-9]*', '**/*[-._ 0-9]training[-._ 0-9]*'],
|
||||
'test': ['**/test[-._ 0-9]*', '**/*[-._ 0-9]test[-._ 0-9]*', '**/testing[-._ 0-9]*', '**/*[-._ 0-9]testing[-._ 0-9]*', ...]}
|
||||
|
||||
Input:
|
||||
|
||||
my_dataset_repository/
|
||||
├── README.md
|
||||
└── data/
|
||||
├── train/
|
||||
│ ├── shard_0.csv
|
||||
│ ├── shard_1.csv
|
||||
│ ├── shard_2.csv
|
||||
│ └── shard_3.csv
|
||||
└── test/
|
||||
├── shard_0.csv
|
||||
└── shard_1.csv
|
||||
|
||||
Output:
|
||||
|
||||
{'train': ['**/train/**', '**/train[-._ 0-9]*/**', '**/*[-._ 0-9]train/**', '**/*[-._ 0-9]train[-._ 0-9]*/**', ...],
|
||||
'test': ['**/test/**', '**/test[-._ 0-9]*/**', '**/*[-._ 0-9]test/**', '**/*[-._ 0-9]test[-._ 0-9]*/**', ...]}
|
||||
|
||||
Input:
|
||||
|
||||
my_dataset_repository/
|
||||
├── README.md
|
||||
└── data/
|
||||
├── train-00000-of-00003.csv
|
||||
├── train-00001-of-00003.csv
|
||||
├── train-00002-of-00003.csv
|
||||
├── test-00000-of-00001.csv
|
||||
├── random-00000-of-00003.csv
|
||||
├── random-00001-of-00003.csv
|
||||
└── random-00002-of-00003.csv
|
||||
|
||||
Output:
|
||||
|
||||
{'train': ['data/train-[0-9][0-9][0-9][0-9][0-9]-of-[0-9][0-9][0-9][0-9][0-9]*.*'],
|
||||
'test': ['data/test-[0-9][0-9][0-9][0-9][0-9]-of-[0-9][0-9][0-9][0-9][0-9]*.*'],
|
||||
'random': ['data/random-[0-9][0-9][0-9][0-9][0-9]-of-[0-9][0-9][0-9][0-9][0-9]*.*']}
|
||||
|
||||
In order, it first tests if SPLIT_PATTERN_SHARDED works, otherwise it tests the patterns in ALL_DEFAULT_PATTERNS.
|
||||
"""
|
||||
resolver = partial(resolve_pattern, base_path=base_path, download_config=download_config)
|
||||
try:
|
||||
return _get_data_files_patterns(resolver)
|
||||
except FileNotFoundError:
|
||||
raise EmptyDatasetError(f"The directory at {base_path} doesn't contain any data files") from None
|
||||
|
||||
|
||||
def _get_single_origin_metadata(
|
||||
data_file: str,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> SingleOriginMetadata:
|
||||
if data_file.startswith(config.HF_ENDPOINT):
|
||||
fs = HfFileSystem(endpoint=config.HF_ENDPOINT, token=download_config.token)
|
||||
data_file = "hf://" + data_file[len(config.HF_ENDPOINT) + 1 :]
|
||||
data_file = data_file.replace("/resolve/", "/" if data_file.startswith("hf://buckets/") else "@", 1)
|
||||
fs_path = data_file
|
||||
else:
|
||||
data_file, storage_options = _prepare_path_and_storage_options(data_file, download_config=download_config)
|
||||
fs, fs_path = url_to_fs(data_file, **storage_options)
|
||||
if isinstance(fs, HfFileSystem):
|
||||
resolved_path = fs.resolve_path(fs_path)
|
||||
if hasattr(resolved_path, "revision"): # no revision for buckets
|
||||
return resolved_path.repo_id, resolved_path.revision
|
||||
info = fs.info(fs_path)
|
||||
# s3fs uses "ETag", gcsfs uses "etag", and for local we simply check mtime
|
||||
for key in ["ETag", "etag", "mtime"]:
|
||||
if key in info:
|
||||
return (str(info[key]),)
|
||||
return ()
|
||||
|
||||
|
||||
def _get_origin_metadata(
|
||||
data_files: list[str],
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
max_workers: Optional[int] = None,
|
||||
) -> list[SingleOriginMetadata]:
|
||||
max_workers = max_workers if max_workers is not None else config.HF_DATASETS_MULTITHREADING_MAX_WORKERS
|
||||
if all("hf://" in data_file for data_file in data_files):
|
||||
# No need for multithreading here since the origin metadata of HF files
|
||||
# is (repo_id, revision) and is cached after first .info() call.
|
||||
return [
|
||||
_get_single_origin_metadata(data_file, download_config=download_config)
|
||||
for data_file in hf_tqdm(
|
||||
data_files,
|
||||
desc="Resolving data files",
|
||||
# set `disable=None` rather than `disable=False` by default to disable progress bar when no TTY attached
|
||||
disable=len(data_files) <= 16 or None,
|
||||
)
|
||||
]
|
||||
return thread_map(
|
||||
partial(_get_single_origin_metadata, download_config=download_config),
|
||||
data_files,
|
||||
max_workers=max_workers,
|
||||
tqdm_class=hf_tqdm,
|
||||
desc="Resolving data files",
|
||||
# set `disable=None` rather than `disable=False` by default to disable progress bar when no TTY attached
|
||||
disable=len(data_files) <= 16 or None,
|
||||
)
|
||||
|
||||
|
||||
class DataFilesList(list[str]):
|
||||
"""
|
||||
List of data files (absolute local paths or URLs).
|
||||
It has two construction methods given the user's data files patterns:
|
||||
- ``from_hf_repo``: resolve patterns inside a dataset repository
|
||||
- ``from_local_or_remote``: resolve patterns from a local path
|
||||
|
||||
Moreover, DataFilesList has an additional attribute ``origin_metadata``.
|
||||
It can store:
|
||||
- the last modified time of local files
|
||||
- ETag of remote files
|
||||
- commit sha of a dataset repository
|
||||
|
||||
Thanks to this additional attribute, it is possible to hash the list
|
||||
and get a different hash if and only if at least one file changed.
|
||||
This is useful for caching Dataset objects that are obtained from a list of data files.
|
||||
"""
|
||||
|
||||
def __init__(self, data_files: list[str], origin_metadata: list[SingleOriginMetadata]) -> None:
|
||||
super().__init__(data_files)
|
||||
self.origin_metadata = origin_metadata
|
||||
|
||||
def __add__(self, other: "DataFilesList") -> "DataFilesList":
|
||||
return DataFilesList([*self, *other], self.origin_metadata + other.origin_metadata)
|
||||
|
||||
@classmethod
|
||||
def from_hf_repo(
|
||||
cls,
|
||||
patterns: list[str],
|
||||
dataset_info: huggingface_hub.hf_api.DatasetInfo,
|
||||
base_path: Optional[str] = None,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesList":
|
||||
base_path = f"hf://datasets/{dataset_info.id}@{dataset_info.sha}/{base_path or ''}".rstrip("/")
|
||||
return cls.from_patterns(
|
||||
patterns, base_path=base_path, allowed_extensions=allowed_extensions, download_config=download_config
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_local_or_remote(
|
||||
cls,
|
||||
patterns: list[str],
|
||||
base_path: Optional[str] = None,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesList":
|
||||
base_path = base_path if base_path is not None else Path().resolve().as_posix()
|
||||
return cls.from_patterns(
|
||||
patterns, base_path=base_path, allowed_extensions=allowed_extensions, download_config=download_config
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_patterns(
|
||||
cls,
|
||||
patterns: list[str],
|
||||
base_path: Optional[str] = None,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesList":
|
||||
base_path = base_path if base_path is not None else Path().resolve().as_posix()
|
||||
data_files = []
|
||||
for pattern in patterns:
|
||||
try:
|
||||
data_files.extend(
|
||||
resolve_pattern(
|
||||
pattern,
|
||||
base_path=base_path,
|
||||
allowed_extensions=allowed_extensions,
|
||||
download_config=download_config,
|
||||
)
|
||||
)
|
||||
except FileNotFoundError:
|
||||
if not has_magic(pattern):
|
||||
raise
|
||||
origin_metadata = _get_origin_metadata(data_files, download_config=download_config)
|
||||
return cls(data_files, origin_metadata)
|
||||
|
||||
def filter(
|
||||
self, *, extensions: Optional[list[str]] = None, file_names: Optional[list[str]] = None
|
||||
) -> "DataFilesList":
|
||||
patterns = []
|
||||
if extensions:
|
||||
ext_pattern = "|".join(re.escape(ext) for ext in extensions)
|
||||
patterns.append(re.compile(f".*({ext_pattern})(\\..+)?$"))
|
||||
if file_names:
|
||||
fn_pattern = "|".join(re.escape(fn) for fn in file_names)
|
||||
patterns.append(re.compile(rf".*[\/]?({fn_pattern})$"))
|
||||
if patterns:
|
||||
return DataFilesList(
|
||||
[data_file for data_file in self if any(pattern.match(data_file) for pattern in patterns)],
|
||||
origin_metadata=self.origin_metadata,
|
||||
)
|
||||
else:
|
||||
return DataFilesList(list(self), origin_metadata=self.origin_metadata)
|
||||
|
||||
|
||||
class DataFilesDict(dict[str, DataFilesList]):
|
||||
"""
|
||||
Dict of split_name -> list of data files (absolute local paths or URLs).
|
||||
It has two construction methods given the user's data files patterns :
|
||||
- ``from_hf_repo``: resolve patterns inside a dataset repository
|
||||
- ``from_local_or_remote``: resolve patterns from a local path
|
||||
|
||||
Moreover, each list is a DataFilesList. It is possible to hash the dictionary
|
||||
and get a different hash if and only if at least one file changed.
|
||||
For more info, see [`DataFilesList`].
|
||||
|
||||
This is useful for caching Dataset objects that are obtained from a list of data files.
|
||||
|
||||
Changing the order of the keys of this dictionary also doesn't change its hash.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_local_or_remote(
|
||||
cls,
|
||||
patterns: dict[str, Union[list[str], DataFilesList]],
|
||||
base_path: Optional[str] = None,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesDict":
|
||||
out = cls()
|
||||
for key, patterns_for_key in patterns.items():
|
||||
out[key] = (
|
||||
patterns_for_key
|
||||
if isinstance(patterns_for_key, DataFilesList)
|
||||
else DataFilesList.from_local_or_remote(
|
||||
patterns_for_key,
|
||||
base_path=base_path,
|
||||
allowed_extensions=allowed_extensions,
|
||||
download_config=download_config,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def from_hf_repo(
|
||||
cls,
|
||||
patterns: dict[str, Union[list[str], DataFilesList]],
|
||||
dataset_info: huggingface_hub.hf_api.DatasetInfo,
|
||||
base_path: Optional[str] = None,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesDict":
|
||||
out = cls()
|
||||
for key, patterns_for_key in patterns.items():
|
||||
out[key] = (
|
||||
patterns_for_key
|
||||
if isinstance(patterns_for_key, DataFilesList)
|
||||
else DataFilesList.from_hf_repo(
|
||||
patterns_for_key,
|
||||
dataset_info=dataset_info,
|
||||
base_path=base_path,
|
||||
allowed_extensions=allowed_extensions,
|
||||
download_config=download_config,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def from_patterns(
|
||||
cls,
|
||||
patterns: dict[str, Union[list[str], DataFilesList]],
|
||||
base_path: Optional[str] = None,
|
||||
allowed_extensions: Optional[list[str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesDict":
|
||||
out = cls()
|
||||
for key, patterns_for_key in patterns.items():
|
||||
out[key] = (
|
||||
patterns_for_key
|
||||
if isinstance(patterns_for_key, DataFilesList)
|
||||
else DataFilesList.from_patterns(
|
||||
patterns_for_key,
|
||||
base_path=base_path,
|
||||
allowed_extensions=allowed_extensions,
|
||||
download_config=download_config,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
def filter(
|
||||
self, *, extensions: Optional[list[str]] = None, file_names: Optional[list[str]] = None
|
||||
) -> "DataFilesDict":
|
||||
out = type(self)()
|
||||
for key, data_files_list in self.items():
|
||||
out[key] = data_files_list.filter(extensions=extensions, file_names=file_names)
|
||||
return out
|
||||
|
||||
|
||||
class DataFilesPatternsList(list[str]):
|
||||
"""
|
||||
List of data files patterns (absolute local paths or URLs).
|
||||
For each pattern there should also be a list of allowed extensions
|
||||
to keep, or a None ot keep all the files for the pattern.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: list[str],
|
||||
allowed_extensions: list[Optional[list[str]]],
|
||||
):
|
||||
super().__init__(patterns)
|
||||
self.allowed_extensions = allowed_extensions
|
||||
|
||||
def __add__(self, other):
|
||||
return DataFilesList([*self, *other], self.allowed_extensions + other.allowed_extensions)
|
||||
|
||||
@classmethod
|
||||
def from_patterns(
|
||||
cls, patterns: list[str], allowed_extensions: Optional[list[str]] = None
|
||||
) -> "DataFilesPatternsList":
|
||||
return cls(patterns, [allowed_extensions] * len(patterns))
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
base_path: str,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesList":
|
||||
base_path = base_path if base_path is not None else Path().resolve().as_posix()
|
||||
data_files = []
|
||||
for pattern, allowed_extensions in zip(self, self.allowed_extensions):
|
||||
try:
|
||||
data_files.extend(
|
||||
resolve_pattern(
|
||||
pattern,
|
||||
base_path=base_path,
|
||||
allowed_extensions=allowed_extensions,
|
||||
download_config=download_config,
|
||||
)
|
||||
)
|
||||
except FileNotFoundError:
|
||||
if not has_magic(pattern):
|
||||
raise
|
||||
origin_metadata = _get_origin_metadata(data_files, download_config=download_config)
|
||||
return DataFilesList(data_files, origin_metadata)
|
||||
|
||||
def filter_extensions(self, extensions: list[str]) -> "DataFilesPatternsList":
|
||||
return DataFilesPatternsList(
|
||||
self, [allowed_extensions + extensions for allowed_extensions in self.allowed_extensions]
|
||||
)
|
||||
|
||||
|
||||
class DataFilesPatternsDict(dict[str, DataFilesPatternsList]):
|
||||
"""
|
||||
Dict of split_name -> list of data files patterns (absolute local paths or URLs).
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_patterns(
|
||||
cls, patterns: dict[str, list[str]], allowed_extensions: Optional[list[str]] = None
|
||||
) -> "DataFilesPatternsDict":
|
||||
out = cls()
|
||||
for key, patterns_for_key in patterns.items():
|
||||
out[key] = (
|
||||
patterns_for_key
|
||||
if isinstance(patterns_for_key, DataFilesPatternsList)
|
||||
else DataFilesPatternsList.from_patterns(
|
||||
patterns_for_key,
|
||||
allowed_extensions=allowed_extensions,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
base_path: str,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
) -> "DataFilesDict":
|
||||
out = DataFilesDict()
|
||||
for key, data_files_patterns_list in self.items():
|
||||
out[key] = data_files_patterns_list.resolve(base_path, download_config)
|
||||
return out
|
||||
|
||||
def filter_extensions(self, extensions: list[str]) -> "DataFilesPatternsDict":
|
||||
out = type(self)()
|
||||
for key, data_files_patterns_list in self.items():
|
||||
out[key] = data_files_patterns_list.filter_extensions(extensions)
|
||||
return out
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
from typing import TypeVar
|
||||
|
||||
from .arrow_dataset import Dataset, _split_by_node_map_style_dataset
|
||||
from .iterable_dataset import IterableDataset, _split_by_node_iterable_dataset
|
||||
|
||||
|
||||
DatasetType = TypeVar("DatasetType", Dataset, IterableDataset)
|
||||
|
||||
|
||||
def split_dataset_by_node(dataset: DatasetType, rank: int, world_size: int) -> DatasetType:
|
||||
"""
|
||||
Split a dataset for the node at rank `rank` in a pool of nodes of size `world_size`.
|
||||
|
||||
For map-style datasets:
|
||||
|
||||
Each node is assigned a chunk of data, e.g. rank 0 is given the first chunk of the dataset.
|
||||
To maximize data loading throughput, chunks are made of contiguous data on disk if possible.
|
||||
|
||||
For iterable datasets:
|
||||
|
||||
If the dataset has a number of shards that is a factor of `world_size` (i.e. if `dataset.num_shards % world_size == 0`),
|
||||
then the shards are evenly assigned across the nodes, which is the most optimized.
|
||||
Otherwise, each node keeps 1 example out of `world_size`, skipping the other examples.
|
||||
|
||||
> [!WARNING]
|
||||
> If you shuffle your iterable dataset in a distributed setup, make sure to set a fixed `seed` in [`IterableDataset.shuffle`]
|
||||
so the same shuffled list of shards is used on every node to know which shards the node should skip.
|
||||
|
||||
Args:
|
||||
dataset ([`Dataset`] or [`IterableDataset`]):
|
||||
The dataset to split by node.
|
||||
rank (`int`):
|
||||
Rank of the current node.
|
||||
world_size (`int`):
|
||||
Total number of nodes.
|
||||
|
||||
Returns:
|
||||
[`Dataset`] or [`IterableDataset`]: The dataset to be used on the node at rank `rank`.
|
||||
"""
|
||||
if isinstance(dataset, Dataset):
|
||||
return _split_by_node_map_style_dataset(dataset, rank=rank, world_size=world_size)
|
||||
else:
|
||||
return _split_by_node_iterable_dataset(dataset, rank=rank, world_size=world_size)
|
||||
@@ -0,0 +1,10 @@
|
||||
__all__ = [
|
||||
"DownloadConfig",
|
||||
"DownloadManager",
|
||||
"DownloadMode",
|
||||
"StreamingDownloadManager",
|
||||
]
|
||||
|
||||
from .download_config import DownloadConfig
|
||||
from .download_manager import DownloadManager, DownloadMode
|
||||
from .streaming_download_manager import StreamingDownloadManager
|
||||
@@ -0,0 +1,85 @@
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .. import config
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadConfig:
|
||||
"""Configuration for our cached path manager.
|
||||
|
||||
Attributes:
|
||||
cache_dir (`str` or `Path`, *optional*):
|
||||
Specify a cache directory to save the file to (overwrite the
|
||||
default cache dir).
|
||||
force_download (`bool`, defaults to `False`):
|
||||
If `True`, re-download the file even if it's already cached in
|
||||
the cache dir.
|
||||
resume_download (`bool`, defaults to `False`):
|
||||
If `True`, resume the download if an incompletely received file is
|
||||
found.
|
||||
proxies (`dict`, *optional*):
|
||||
user_agent (`str`, *optional*):
|
||||
Optional string or dict that will be appended to the user-agent on remote
|
||||
requests.
|
||||
extract_compressed_file (`bool`, defaults to `False`):
|
||||
If `True` and the path point to a zip or tar file,
|
||||
extract the compressed file in a folder along the archive.
|
||||
force_extract (`bool`, defaults to `False`):
|
||||
If `True` when `extract_compressed_file` is `True` and the archive
|
||||
was already extracted, re-extract the archive and override the folder where it was extracted.
|
||||
delete_extracted (`bool`, defaults to `False`):
|
||||
Whether to delete (or keep) the extracted files.
|
||||
extract_on_the_fly (`bool`, defaults to `False`):
|
||||
If `True`, extract compressed files while they are being read.
|
||||
use_etag (`bool`, defaults to `True`):
|
||||
Whether to use the ETag HTTP response header to validate the cached files.
|
||||
num_proc (`int`, *optional*):
|
||||
The number of processes to launch to download the files in parallel.
|
||||
max_retries (`int`, default to `1`):
|
||||
The number of times to retry an HTTP request if it fails.
|
||||
token (`str` or `bool`, *optional*):
|
||||
Optional string or boolean to use as Bearer token
|
||||
for remote files on the Datasets Hub. If `True`, or not specified, will get token from `~/.huggingface`.
|
||||
storage_options (`dict`, *optional*):
|
||||
Key/value pairs to be passed on to the dataset file-system backend, if any.
|
||||
download_desc (`str`, *optional*):
|
||||
A description to be displayed alongside with the progress bar while downloading the files.
|
||||
disable_tqdm (`bool`, defaults to `False`):
|
||||
Whether to disable the individual files download progress bar
|
||||
"""
|
||||
|
||||
cache_dir: Optional[Union[str, Path]] = None
|
||||
force_download: bool = False
|
||||
resume_download: bool = False
|
||||
local_files_only: bool = False
|
||||
proxies: Optional[dict] = None
|
||||
user_agent: Optional[str] = None
|
||||
extract_compressed_file: bool = False
|
||||
force_extract: bool = False
|
||||
delete_extracted: bool = False
|
||||
extract_on_the_fly: bool = False
|
||||
use_etag: bool = True
|
||||
num_proc: Optional[int] = None
|
||||
max_retries: int = 1
|
||||
token: Optional[Union[str, bool]] = None
|
||||
storage_options: dict[str, Any] = field(default_factory=dict)
|
||||
download_desc: Optional[str] = None
|
||||
disable_tqdm: bool = False
|
||||
|
||||
def copy(self) -> "DownloadConfig":
|
||||
return self.__class__(**{k: copy.deepcopy(v) for k, v in self.__dict__.items()})
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name == "token" and getattr(self, "storage_options", None) is not None:
|
||||
if "hf" not in self.storage_options:
|
||||
self.storage_options["hf"] = {"endpoint": config.HF_ENDPOINT, "token": value}
|
||||
else:
|
||||
self.storage_options["hf"]["token"] = value
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __post_init__(self):
|
||||
# update storage_options
|
||||
self.token = self.token
|
||||
@@ -0,0 +1,340 @@
|
||||
# Copyright 2020 The TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
"""Download manager interface."""
|
||||
|
||||
import enum
|
||||
import io
|
||||
import multiprocessing
|
||||
import os
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Optional, Union
|
||||
|
||||
import fsspec
|
||||
from fsspec.core import url_to_fs
|
||||
from tqdm.contrib.concurrent import thread_map
|
||||
|
||||
from .. import config
|
||||
from ..utils import tqdm as hf_tqdm
|
||||
from ..utils.file_utils import (
|
||||
ArchiveIterable,
|
||||
FilesIterable,
|
||||
cached_path,
|
||||
is_relative_path,
|
||||
stack_multiprocessing_download_progress_bars,
|
||||
url_or_path_join,
|
||||
)
|
||||
from ..utils.info_utils import get_size_checksum_dict
|
||||
from ..utils.logging import get_logger, tqdm
|
||||
from ..utils.py_utils import NestedDataStructure, map_nested
|
||||
from ..utils.track import tracked_str
|
||||
from .download_config import DownloadConfig
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DownloadMode(enum.Enum):
|
||||
"""`Enum` for how to treat pre-existing downloads and data.
|
||||
|
||||
The default mode is `REUSE_DATASET_IF_EXISTS`, which will reuse both
|
||||
raw downloads and the prepared dataset if they exist.
|
||||
|
||||
The generations modes:
|
||||
|
||||
| | Downloads | Dataset |
|
||||
|-------------------------------------|-----------|---------|
|
||||
| `REUSE_DATASET_IF_EXISTS` (default) | Reuse | Reuse |
|
||||
| `REUSE_CACHE_IF_EXISTS` | Reuse | Fresh |
|
||||
| `FORCE_REDOWNLOAD` | Fresh | Fresh |
|
||||
|
||||
"""
|
||||
|
||||
REUSE_DATASET_IF_EXISTS = "reuse_dataset_if_exists"
|
||||
REUSE_CACHE_IF_EXISTS = "reuse_cache_if_exists"
|
||||
FORCE_REDOWNLOAD = "force_redownload"
|
||||
|
||||
|
||||
class DownloadManager:
|
||||
is_streaming = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dataset_name: Optional[str] = None,
|
||||
data_dir: Optional[str] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
base_path: Optional[str] = None,
|
||||
record_checksums=False,
|
||||
):
|
||||
"""Download manager constructor.
|
||||
|
||||
Args:
|
||||
data_dir:
|
||||
can be used to specify a manual directory to get the files from.
|
||||
dataset_name (`str`):
|
||||
name of dataset this instance will be used for. If
|
||||
provided, downloads will contain which datasets they were used for.
|
||||
download_config (`DownloadConfig`):
|
||||
to specify the cache directory and other
|
||||
download options
|
||||
base_path (`str`):
|
||||
base path that is used when relative paths are used to
|
||||
download files. This can be a remote url.
|
||||
record_checksums (`bool`, defaults to `False`):
|
||||
Whether to record the checksums of the downloaded files. If None, the value is inferred from the builder.
|
||||
"""
|
||||
self._dataset_name = dataset_name
|
||||
self._data_dir = data_dir
|
||||
self._base_path = base_path or os.path.abspath(".")
|
||||
# To record what is being used: {url: {num_bytes: int, checksum: str}}
|
||||
self._recorded_sizes_checksums: dict[str, dict[str, Optional[Union[int, str]]]] = {}
|
||||
self.record_checksums = record_checksums
|
||||
self.download_config = download_config or DownloadConfig()
|
||||
self.downloaded_paths = {}
|
||||
self.extracted_paths = {}
|
||||
|
||||
@property
|
||||
def manual_dir(self):
|
||||
return self._data_dir
|
||||
|
||||
@property
|
||||
def downloaded_size(self):
|
||||
"""Returns the total size of downloaded files."""
|
||||
return sum(checksums_dict["num_bytes"] for checksums_dict in self._recorded_sizes_checksums.values())
|
||||
|
||||
def _record_sizes_checksums(self, url_or_urls: NestedDataStructure, downloaded_path_or_paths: NestedDataStructure):
|
||||
"""Record size/checksum of downloaded files."""
|
||||
delay = 5
|
||||
for url, path in hf_tqdm(
|
||||
list(zip(url_or_urls.flatten(), downloaded_path_or_paths.flatten())),
|
||||
delay=delay,
|
||||
desc="Computing checksums",
|
||||
):
|
||||
# call str to support PathLike objects
|
||||
self._recorded_sizes_checksums[str(url)] = get_size_checksum_dict(
|
||||
path, record_checksum=self.record_checksums
|
||||
)
|
||||
|
||||
def download(self, url_or_urls):
|
||||
"""Download given URL(s).
|
||||
|
||||
By default, only one process is used for download. Pass customized `download_config.num_proc` to change this behavior.
|
||||
|
||||
Args:
|
||||
url_or_urls (`str` or `list` or `dict`):
|
||||
URL or `list` or `dict` of URLs to download. Each URL is a `str`.
|
||||
|
||||
Returns:
|
||||
`str` or `list` or `dict`:
|
||||
The downloaded paths matching the given input `url_or_urls`.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> downloaded_files = dl_manager.download('https://storage.googleapis.com/seldon-datasets/sentence_polarity_v1/rt-polaritydata.tar.gz')
|
||||
```
|
||||
"""
|
||||
download_config = self.download_config.copy()
|
||||
download_config.extract_compressed_file = False
|
||||
if download_config.download_desc is None:
|
||||
download_config.download_desc = "Downloading data"
|
||||
|
||||
download_func = partial(self._download_batched, download_config=download_config)
|
||||
|
||||
start_time = datetime.now()
|
||||
with stack_multiprocessing_download_progress_bars():
|
||||
downloaded_path_or_paths = map_nested(
|
||||
download_func,
|
||||
url_or_urls,
|
||||
map_tuple=True,
|
||||
num_proc=download_config.num_proc,
|
||||
desc="Downloading data files",
|
||||
batched=True,
|
||||
batch_size=-1,
|
||||
)
|
||||
duration = datetime.now() - start_time
|
||||
logger.info(f"Downloading took {duration.total_seconds() // 60} min")
|
||||
url_or_urls = NestedDataStructure(url_or_urls)
|
||||
downloaded_path_or_paths = NestedDataStructure(downloaded_path_or_paths)
|
||||
self.downloaded_paths.update(dict(zip(url_or_urls.flatten(), downloaded_path_or_paths.flatten())))
|
||||
|
||||
start_time = datetime.now()
|
||||
self._record_sizes_checksums(url_or_urls, downloaded_path_or_paths)
|
||||
duration = datetime.now() - start_time
|
||||
logger.info(f"Checksum Computation took {duration.total_seconds() // 60} min")
|
||||
|
||||
return downloaded_path_or_paths.data
|
||||
|
||||
def _download_batched(
|
||||
self,
|
||||
url_or_filenames: list[str],
|
||||
download_config: DownloadConfig,
|
||||
) -> list[str]:
|
||||
if len(url_or_filenames) >= 16:
|
||||
download_config = download_config.copy()
|
||||
download_config.disable_tqdm = True
|
||||
download_func = partial(self._download_single, download_config=download_config)
|
||||
|
||||
fs: fsspec.AbstractFileSystem
|
||||
path = str(url_or_filenames[0])
|
||||
if is_relative_path(path):
|
||||
# append the relative path to the base_path
|
||||
path = url_or_path_join(self._base_path, path)
|
||||
fs, path = url_to_fs(path, **download_config.storage_options)
|
||||
size = 0
|
||||
try:
|
||||
size = fs.info(path).get("size", 0)
|
||||
except Exception:
|
||||
pass
|
||||
max_workers = (
|
||||
config.HF_DATASETS_MULTITHREADING_MAX_WORKERS if size < (20 << 20) else 1
|
||||
) # enable multithreading if files are small
|
||||
|
||||
return thread_map(
|
||||
download_func,
|
||||
url_or_filenames,
|
||||
desc=download_config.download_desc or "Downloading",
|
||||
unit="files",
|
||||
position=multiprocessing.current_process()._identity[-1] # contains the ranks of subprocesses
|
||||
if os.environ.get("HF_DATASETS_STACK_MULTIPROCESSING_DOWNLOAD_PROGRESS_BARS") == "1"
|
||||
and multiprocessing.current_process()._identity
|
||||
else None,
|
||||
max_workers=max_workers,
|
||||
tqdm_class=tqdm,
|
||||
)
|
||||
else:
|
||||
return [
|
||||
self._download_single(url_or_filename, download_config=download_config)
|
||||
for url_or_filename in url_or_filenames
|
||||
]
|
||||
|
||||
def _download_single(self, url_or_filename: str, download_config: DownloadConfig) -> str:
|
||||
url_or_filename = str(url_or_filename)
|
||||
if is_relative_path(url_or_filename):
|
||||
# append the relative path to the base_path
|
||||
url_or_filename = url_or_path_join(self._base_path, url_or_filename)
|
||||
out = cached_path(url_or_filename, download_config=download_config)
|
||||
out = tracked_str(out)
|
||||
out.set_origin(url_or_filename)
|
||||
return out
|
||||
|
||||
def iter_archive(self, path_or_buf: Union[str, io.BufferedReader]):
|
||||
"""Iterate over files within an archive.
|
||||
|
||||
Args:
|
||||
path_or_buf (`str` or `io.BufferedReader`):
|
||||
Archive path or archive binary file object.
|
||||
|
||||
Yields:
|
||||
`tuple[str, io.BufferedReader]`:
|
||||
2-tuple (path_within_archive, file_object).
|
||||
File object is opened in binary mode.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> archive = dl_manager.download('https://storage.googleapis.com/seldon-datasets/sentence_polarity_v1/rt-polaritydata.tar.gz')
|
||||
>>> files = dl_manager.iter_archive(archive)
|
||||
```
|
||||
"""
|
||||
|
||||
if hasattr(path_or_buf, "read"):
|
||||
return ArchiveIterable.from_buf(path_or_buf)
|
||||
else:
|
||||
return ArchiveIterable.from_urlpath(path_or_buf)
|
||||
|
||||
def iter_files(self, paths: Union[str, list[str]]):
|
||||
"""Iterate over file paths.
|
||||
|
||||
Args:
|
||||
paths (`str` or `list` of `str`):
|
||||
Root paths.
|
||||
|
||||
Yields:
|
||||
`str`: File path.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> files = dl_manager.download_and_extract('https://huggingface.co/datasets/AI-Lab-Makerere/beans/resolve/main/data/train.zip')
|
||||
>>> files = dl_manager.iter_files(files)
|
||||
```
|
||||
"""
|
||||
return FilesIterable.from_urlpaths(paths)
|
||||
|
||||
def extract(self, path_or_paths):
|
||||
"""Extract given path(s).
|
||||
|
||||
Args:
|
||||
path_or_paths (path or `list` or `dict`):
|
||||
Path of file to extract. Each path is a `str`.
|
||||
|
||||
Returns:
|
||||
extracted_path(s): `str`, The extracted paths matching the given input
|
||||
path_or_paths.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> downloaded_files = dl_manager.download('https://storage.googleapis.com/seldon-datasets/sentence_polarity_v1/rt-polaritydata.tar.gz')
|
||||
>>> extracted_files = dl_manager.extract(downloaded_files)
|
||||
```
|
||||
"""
|
||||
download_config = self.download_config.copy()
|
||||
download_config.extract_compressed_file = True
|
||||
extract_func = partial(self._download_single, download_config=download_config)
|
||||
extracted_paths = map_nested(
|
||||
extract_func,
|
||||
path_or_paths,
|
||||
num_proc=download_config.num_proc,
|
||||
desc="Extracting data files",
|
||||
)
|
||||
path_or_paths = NestedDataStructure(path_or_paths)
|
||||
extracted_paths = NestedDataStructure(extracted_paths)
|
||||
self.extracted_paths.update(dict(zip(path_or_paths.flatten(), extracted_paths.flatten())))
|
||||
return extracted_paths.data
|
||||
|
||||
def download_and_extract(self, url_or_urls):
|
||||
"""Download and extract given `url_or_urls`.
|
||||
|
||||
Is roughly equivalent to:
|
||||
|
||||
```
|
||||
extracted_paths = dl_manager.extract(dl_manager.download(url_or_urls))
|
||||
```
|
||||
|
||||
Args:
|
||||
url_or_urls (`str` or `list` or `dict`):
|
||||
URL or `list` or `dict` of URLs to download and extract. Each URL is a `str`.
|
||||
|
||||
Returns:
|
||||
extracted_path(s): `str`, extracted paths of given URL(s).
|
||||
"""
|
||||
return self.extract(self.download(url_or_urls))
|
||||
|
||||
def get_recorded_sizes_checksums(self):
|
||||
return self._recorded_sizes_checksums.copy()
|
||||
|
||||
def delete_extracted_files(self):
|
||||
paths_to_delete = set(self.extracted_paths.values()) - set(self.downloaded_paths.values())
|
||||
for key, path in list(self.extracted_paths.items()):
|
||||
if path in paths_to_delete and os.path.isfile(path):
|
||||
os.remove(path)
|
||||
del self.extracted_paths[key]
|
||||
|
||||
def manage_extracted_files(self):
|
||||
if self.download_config.delete_extracted:
|
||||
self.delete_extracted_files()
|
||||
@@ -0,0 +1,219 @@
|
||||
import io
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Union
|
||||
|
||||
from ..utils.file_utils import ( # noqa: F401 # backward compatibility
|
||||
SINGLE_FILE_COMPRESSION_PROTOCOLS,
|
||||
ArchiveIterable,
|
||||
FilesIterable,
|
||||
_get_extraction_protocol,
|
||||
_get_path_extension,
|
||||
_prepare_path_and_storage_options,
|
||||
is_relative_path,
|
||||
url_or_path_join,
|
||||
xbasename,
|
||||
xdirname,
|
||||
xet_parse,
|
||||
xexists,
|
||||
xgetsize,
|
||||
xglob,
|
||||
xgzip_open,
|
||||
xisdir,
|
||||
xisfile,
|
||||
xjoin,
|
||||
xlistdir,
|
||||
xnumpy_load,
|
||||
xopen,
|
||||
xpandas_read_csv,
|
||||
xpandas_read_excel,
|
||||
xPath,
|
||||
xpyarrow_parquet_read_table,
|
||||
xrelpath,
|
||||
xsio_loadmat,
|
||||
xsplit,
|
||||
xsplitext,
|
||||
xwalk,
|
||||
xxml_dom_minidom_parse,
|
||||
)
|
||||
from ..utils.logging import get_logger
|
||||
from ..utils.py_utils import map_nested
|
||||
from .download_config import DownloadConfig
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class StreamingDownloadManager:
|
||||
"""
|
||||
Download manager that uses the "::" separator to navigate through (possibly remote) compressed archives.
|
||||
Contrary to the regular `DownloadManager`, the `download` and `extract` methods don't actually download nor extract
|
||||
data, but they rather return the path or url that could be opened using the `xopen` function which extends the
|
||||
built-in `open` function to stream data from remote files.
|
||||
"""
|
||||
|
||||
is_streaming = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dataset_name: Optional[str] = None,
|
||||
data_dir: Optional[str] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
base_path: Optional[str] = None,
|
||||
):
|
||||
self._dataset_name = dataset_name
|
||||
self._data_dir = data_dir
|
||||
self._base_path = base_path or os.path.abspath(".")
|
||||
self.download_config = download_config or DownloadConfig()
|
||||
self.downloaded_size = None
|
||||
self.record_checksums = False
|
||||
|
||||
@property
|
||||
def manual_dir(self):
|
||||
return self._data_dir
|
||||
|
||||
def download(self, url_or_urls):
|
||||
"""Normalize URL(s) of files to stream data from.
|
||||
This is the lazy version of `DownloadManager.download` for streaming.
|
||||
|
||||
Args:
|
||||
url_or_urls (`str` or `list` or `dict`):
|
||||
URL(s) of files to stream data from. Each url is a `str`.
|
||||
|
||||
Returns:
|
||||
url(s): (`str` or `list` or `dict`), URL(s) to stream data from matching the given input url_or_urls.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> downloaded_files = dl_manager.download('https://storage.googleapis.com/seldon-datasets/sentence_polarity_v1/rt-polaritydata.tar.gz')
|
||||
```
|
||||
"""
|
||||
url_or_urls = map_nested(self._download_single, url_or_urls, map_tuple=True)
|
||||
return url_or_urls
|
||||
|
||||
def _download_single(self, urlpath: str) -> str:
|
||||
urlpath = str(urlpath)
|
||||
if is_relative_path(urlpath):
|
||||
# append the relative path to the base_path
|
||||
urlpath = url_or_path_join(self._base_path, urlpath)
|
||||
return urlpath
|
||||
|
||||
def extract(self, url_or_urls):
|
||||
"""Add extraction protocol for given url(s) for streaming.
|
||||
|
||||
This is the lazy version of `DownloadManager.extract` for streaming.
|
||||
|
||||
Args:
|
||||
url_or_urls (`str` or `list` or `dict`):
|
||||
URL(s) of files to stream data from. Each url is a `str`.
|
||||
|
||||
Returns:
|
||||
url(s): (`str` or `list` or `dict`), URL(s) to stream data from matching the given input `url_or_urls`.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> downloaded_files = dl_manager.download('https://storage.googleapis.com/seldon-datasets/sentence_polarity_v1/rt-polaritydata.tar.gz')
|
||||
>>> extracted_files = dl_manager.extract(downloaded_files)
|
||||
```
|
||||
"""
|
||||
urlpaths = map_nested(self._extract, url_or_urls, map_tuple=True)
|
||||
return urlpaths
|
||||
|
||||
def _extract(self, urlpath: str) -> str:
|
||||
urlpath = str(urlpath)
|
||||
# get inner file: zip://train-00000.json.gz::https://foo.bar/data.zip -> zip://train-00000.json.gz
|
||||
protocol = _get_extraction_protocol(urlpath, download_config=self.download_config)
|
||||
path = urlpath.split("::")[0]
|
||||
extension = _get_path_extension(path)
|
||||
if extension in ["tgz", "tar"] or path.endswith((".tar.gz", ".tar.bz2", ".tar.xz")):
|
||||
raise NotImplementedError(
|
||||
f"Extraction protocol for TAR archives like '{urlpath}' is not implemented in streaming mode. "
|
||||
f"Please use `dl_manager.iter_archive` instead.\n\n"
|
||||
f"Example usage:\n\n"
|
||||
f"\turl = dl_manager.download(url)\n"
|
||||
f"\ttar_archive_iterator = dl_manager.iter_archive(url)\n\n"
|
||||
f"\tfor filename, file in tar_archive_iterator:\n"
|
||||
f"\t\t..."
|
||||
)
|
||||
if protocol is None:
|
||||
# no extraction
|
||||
return urlpath
|
||||
elif protocol in SINGLE_FILE_COMPRESSION_PROTOCOLS:
|
||||
# there is one single file which is the uncompressed file
|
||||
inner_file = os.path.basename(urlpath.split("::")[0])
|
||||
inner_file = inner_file[: inner_file.rindex(".")] if "." in inner_file else inner_file
|
||||
return f"{protocol}://{inner_file}::{urlpath}"
|
||||
else:
|
||||
return f"{protocol}://::{urlpath}"
|
||||
|
||||
def download_and_extract(self, url_or_urls):
|
||||
"""Prepare given `url_or_urls` for streaming (add extraction protocol).
|
||||
|
||||
This is the lazy version of `DownloadManager.download_and_extract` for streaming.
|
||||
|
||||
Is equivalent to:
|
||||
|
||||
```
|
||||
urls = dl_manager.extract(dl_manager.download(url_or_urls))
|
||||
```
|
||||
|
||||
Args:
|
||||
url_or_urls (`str` or `list` or `dict`):
|
||||
URL(s) to stream from data from. Each url is a `str`.
|
||||
|
||||
Returns:
|
||||
url(s): (`str` or `list` or `dict`), URL(s) to stream data from matching the given input `url_or_urls`.
|
||||
"""
|
||||
return self.extract(self.download(url_or_urls))
|
||||
|
||||
def iter_archive(self, urlpath_or_buf: Union[str, io.BufferedReader]) -> Iterable[tuple]:
|
||||
"""Iterate over files within an archive.
|
||||
|
||||
Args:
|
||||
urlpath_or_buf (`str` or `io.BufferedReader`):
|
||||
Archive path or archive binary file object.
|
||||
|
||||
Yields:
|
||||
`tuple[str, io.BufferedReader]`:
|
||||
2-tuple (path_within_archive, file_object).
|
||||
File object is opened in binary mode.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> archive = dl_manager.download('https://storage.googleapis.com/seldon-datasets/sentence_polarity_v1/rt-polaritydata.tar.gz')
|
||||
>>> files = dl_manager.iter_archive(archive)
|
||||
```
|
||||
"""
|
||||
|
||||
if hasattr(urlpath_or_buf, "read"):
|
||||
return ArchiveIterable.from_buf(urlpath_or_buf)
|
||||
else:
|
||||
return ArchiveIterable.from_urlpath(urlpath_or_buf, download_config=self.download_config)
|
||||
|
||||
def iter_files(self, urlpaths: Union[str, list[str]]) -> Iterable[str]:
|
||||
"""Iterate over files.
|
||||
|
||||
Args:
|
||||
urlpaths (`str` or `list` of `str`):
|
||||
Root paths.
|
||||
|
||||
Yields:
|
||||
str: File URL path.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> files = dl_manager.download_and_extract('https://huggingface.co/datasets/AI-Lab-Makerere/beans/resolve/main/data/train.zip')
|
||||
>>> files = dl_manager.iter_files(files)
|
||||
```
|
||||
"""
|
||||
return FilesIterable.from_urlpaths(urlpaths, download_config=self.download_config)
|
||||
|
||||
def manage_extracted_files(self):
|
||||
pass
|
||||
|
||||
def get_recorded_sizes_checksums(self):
|
||||
pass
|
||||
@@ -0,0 +1,119 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2023 The HuggingFace Authors.
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from huggingface_hub import HfFileSystem
|
||||
|
||||
from . import config
|
||||
from .table import CastError
|
||||
from .utils.track import TrackedIterableFromGenerator, tracked_list, tracked_str
|
||||
|
||||
|
||||
class DatasetsError(Exception):
|
||||
"""Base class for exceptions in this library."""
|
||||
|
||||
|
||||
class DefunctDatasetError(DatasetsError):
|
||||
"""The dataset has been defunct."""
|
||||
|
||||
|
||||
class FileNotFoundDatasetsError(DatasetsError, FileNotFoundError):
|
||||
"""FileNotFoundError raised by this library."""
|
||||
|
||||
|
||||
class DataFilesNotFoundError(FileNotFoundDatasetsError):
|
||||
"""No (supported) data files found."""
|
||||
|
||||
|
||||
class DatasetNotFoundError(FileNotFoundDatasetsError):
|
||||
"""Dataset not found.
|
||||
|
||||
Raised when trying to access:
|
||||
- a missing dataset, or
|
||||
- a private/gated dataset and the user is not authenticated.
|
||||
"""
|
||||
|
||||
|
||||
class DatasetBuildError(DatasetsError):
|
||||
pass
|
||||
|
||||
|
||||
class ManualDownloadError(DatasetBuildError):
|
||||
pass
|
||||
|
||||
|
||||
class FileFormatError(DatasetBuildError):
|
||||
pass
|
||||
|
||||
|
||||
class DatasetGenerationError(DatasetBuildError):
|
||||
pass
|
||||
|
||||
|
||||
class DatasetGenerationCastError(DatasetGenerationError):
|
||||
@classmethod
|
||||
def from_cast_error(
|
||||
cls,
|
||||
cast_error: CastError,
|
||||
builder_name: str,
|
||||
gen_kwargs: dict[str, Any],
|
||||
token: Optional[Union[bool, str]],
|
||||
) -> "DatasetGenerationCastError":
|
||||
explanation_message = (
|
||||
f"\n\nAll the data files must have the same columns, but at some point {cast_error.details()}"
|
||||
)
|
||||
formatted_tracked_gen_kwargs: list[str] = []
|
||||
for gen_kwarg in gen_kwargs.values():
|
||||
if not isinstance(gen_kwarg, (tracked_str, tracked_list, TrackedIterableFromGenerator)):
|
||||
continue
|
||||
while (
|
||||
isinstance(gen_kwarg, (tracked_list, TrackedIterableFromGenerator)) and gen_kwarg.last_item is not None
|
||||
):
|
||||
gen_kwarg = gen_kwarg.last_item
|
||||
if isinstance(gen_kwarg, tracked_str):
|
||||
gen_kwarg = gen_kwarg.get_origin()
|
||||
if isinstance(gen_kwarg, str) and gen_kwarg.startswith("hf://"):
|
||||
resolved_path = HfFileSystem(endpoint=config.HF_ENDPOINT, token=token).resolve_path(gen_kwarg)
|
||||
gen_kwarg = "hf://" + resolved_path.unresolve()
|
||||
if "@" + resolved_path.revision in gen_kwarg:
|
||||
gen_kwarg = (
|
||||
gen_kwarg.replace("@" + resolved_path.revision, "", 1)
|
||||
+ f" (at revision {resolved_path.revision})"
|
||||
)
|
||||
formatted_tracked_gen_kwargs.append(str(gen_kwarg))
|
||||
if formatted_tracked_gen_kwargs:
|
||||
explanation_message += f"\n\nThis happened while the {builder_name} dataset builder was generating data using\n\n{', '.join(formatted_tracked_gen_kwargs)}"
|
||||
help_message = "\n\nPlease either edit the data files to have matching columns, or separate them into different configurations (see docs at https://hf.co/docs/hub/datasets-manual-configuration#multiple-configurations)"
|
||||
return cls("An error occurred while generating the dataset" + explanation_message + help_message)
|
||||
|
||||
|
||||
class ChecksumVerificationError(DatasetsError):
|
||||
"""Error raised during checksums verifications of downloaded files."""
|
||||
|
||||
|
||||
class UnexpectedDownloadedFileError(ChecksumVerificationError):
|
||||
"""Some downloaded files were not expected."""
|
||||
|
||||
|
||||
class ExpectedMoreDownloadedFilesError(ChecksumVerificationError):
|
||||
"""Some files were supposed to be downloaded but were not."""
|
||||
|
||||
|
||||
class NonMatchingChecksumError(ChecksumVerificationError):
|
||||
"""The downloaded file checksum don't match the expected checksum."""
|
||||
|
||||
|
||||
class SplitsVerificationError(DatasetsError):
|
||||
"""Error raised during splits verifications."""
|
||||
|
||||
|
||||
class UnexpectedSplitsError(SplitsVerificationError):
|
||||
"""The expected splits of the downloaded file is missing."""
|
||||
|
||||
|
||||
class ExpectedMoreSplitsError(SplitsVerificationError):
|
||||
"""Some recorded splits are missing."""
|
||||
|
||||
|
||||
class NonMatchingSplitsSizesError(SplitsVerificationError):
|
||||
"""The splits sizes don't match the expected splits sizes."""
|
||||
@@ -0,0 +1,29 @@
|
||||
__all__ = [
|
||||
"Audio",
|
||||
"Array2D",
|
||||
"Array3D",
|
||||
"Array4D",
|
||||
"Array5D",
|
||||
"ClassLabel",
|
||||
"Features",
|
||||
"Json",
|
||||
"LargeList",
|
||||
"List",
|
||||
"Sequence",
|
||||
"Value",
|
||||
"Image",
|
||||
"Mesh",
|
||||
"Translation",
|
||||
"TranslationVariableLanguages",
|
||||
"Video",
|
||||
"Pdf",
|
||||
"Nifti",
|
||||
]
|
||||
from .audio import Audio
|
||||
from .features import Array2D, Array3D, Array4D, Array5D, ClassLabel, Features, Json, LargeList, List, Sequence, Value
|
||||
from .image import Image
|
||||
from .mesh import Mesh
|
||||
from .nifti import Nifti
|
||||
from .pdf import Pdf
|
||||
from .translation import Translation, TranslationVariableLanguages
|
||||
from .video import Video
|
||||
@@ -0,0 +1,15 @@
|
||||
import numpy as np
|
||||
from torchcodec.decoders import AudioDecoder as _AudioDecoder
|
||||
|
||||
|
||||
class AudioDecoder(_AudioDecoder):
|
||||
def __getitem__(self, key: str):
|
||||
if key == "array":
|
||||
y = self.get_all_samples().data.cpu().numpy()
|
||||
return np.mean(y, axis=tuple(range(y.ndim - 1))) if y.ndim > 1 else y
|
||||
elif key == "sampling_rate":
|
||||
return self.get_samples_played_in_range(0, 0).sample_rate
|
||||
elif hasattr(super(), "__getitem__"):
|
||||
return super().__getitem__(key)
|
||||
else:
|
||||
raise TypeError("'torchcodec.decoders.AudioDecoder' object is not subscriptable")
|
||||
@@ -0,0 +1,365 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..download.download_config import DownloadConfig
|
||||
from ..table import array_cast
|
||||
from ..utils.file_utils import is_local_path, is_remote_url, xopen
|
||||
from ..utils.py_utils import no_op_if_value_is_null, string_to_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torchcodec.decoders import AudioDecoder
|
||||
|
||||
from .features import FeatureType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Audio:
|
||||
"""Audio [`Feature`] to extract audio data from an audio file.
|
||||
|
||||
Input: The Audio feature accepts as input:
|
||||
- A `str`: Absolute path to the audio file (i.e. random access is allowed).
|
||||
- A `pathlib.Path`: path to the audio file (i.e. random access is allowed).
|
||||
- A `dict` with the keys:
|
||||
|
||||
- `path`: String with relative path of the audio file to the archive file.
|
||||
- `bytes`: Bytes content of the audio file.
|
||||
|
||||
This is useful for parquet or webdataset files which embed audio files.
|
||||
|
||||
- A `dict` with the keys:
|
||||
|
||||
- `array`: Array containing the audio sample
|
||||
- `sampling_rate`: Integer corresponding to the sampling rate of the audio sample.
|
||||
|
||||
- A `torchcodec.decoders.AudioDecoder`: torchcodec audio decoder object.
|
||||
|
||||
Output: The Audio features output data as `torchcodec.decoders.AudioDecoder` objects, with additional keys:
|
||||
|
||||
- `array`: Array containing the audio sample
|
||||
- `sampling_rate`: Integer corresponding to the sampling rate of the audio sample.
|
||||
|
||||
Args:
|
||||
sampling_rate (`int`, *optional*):
|
||||
Target sampling rate. If `None`, the native sampling rate is used.
|
||||
num_channels (`int`, *optional*):
|
||||
The desired number of channels of the samples. By default, the number of channels of the source is used.
|
||||
Audio decoding will return samples with shape (num_channels, num_samples)
|
||||
Currently `None` (number of channels of the source, default), `1` (mono) or `2` (stereo) channels are supported.
|
||||
The `num_channels` argument is passed to `torchcodec.decoders.AudioDecoder`.
|
||||
|
||||
<Added version="4.4.0"/>
|
||||
decode (`bool`, defaults to `True`):
|
||||
Whether to decode the audio data. If `False`,
|
||||
returns the underlying dictionary in the format `{"path": audio_path, "bytes": audio_bytes}`.
|
||||
stream_index (`int`, *optional*):
|
||||
The streaming index to use from the file. If `None` defaults to the "best" index.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import load_dataset, Audio
|
||||
>>> ds = load_dataset("PolyAI/minds14", name="en-US", split="train")
|
||||
>>> ds = ds.cast_column("audio", Audio(sampling_rate=44100, num_channels=2))
|
||||
>>> ds[0]["audio"]
|
||||
<datasets.features._torchcodec.AudioDecoder object at 0x11642b6a0>
|
||||
>>> audio = ds[0]["audio"]
|
||||
>>> audio.get_samples_played_in_range(0, 10)
|
||||
AudioSamples:
|
||||
data (shape): torch.Size([2, 110592])
|
||||
pts_seconds: 0.0
|
||||
duration_seconds: 2.507755102040816
|
||||
sample_rate: 44100
|
||||
```
|
||||
"""
|
||||
|
||||
sampling_rate: Optional[int] = None
|
||||
decode: bool = True
|
||||
num_channels: Optional[int] = None
|
||||
stream_index: Optional[int] = None
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "dict"
|
||||
pa_type: ClassVar[Any] = pa.struct({"bytes": pa.binary(), "path": pa.string()})
|
||||
_type: str = field(default="Audio", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return self.pa_type
|
||||
|
||||
def encode_example(self, value: Union[str, bytes, bytearray, dict, "AudioDecoder"]) -> dict:
|
||||
"""Encode example into a format for Arrow.
|
||||
|
||||
Args:
|
||||
value (`str`, `bytes`,`bytearray`,`dict`, `AudioDecoder`):
|
||||
Data passed as input to Audio feature.
|
||||
|
||||
Returns:
|
||||
`dict`
|
||||
"""
|
||||
try:
|
||||
import torch
|
||||
from torchcodec.encoders import AudioEncoder # needed to write audio files
|
||||
except ImportError as err:
|
||||
raise ImportError("To support encoding audio data, please install 'torchcodec'.") from err
|
||||
|
||||
if value is None:
|
||||
raise ValueError("value must be provided")
|
||||
|
||||
if config.TORCHCODEC_AVAILABLE:
|
||||
from torchcodec.decoders import AudioDecoder
|
||||
|
||||
else:
|
||||
AudioDecoder = None
|
||||
|
||||
if isinstance(value, str):
|
||||
return {"bytes": None, "path": value}
|
||||
elif isinstance(value, Path):
|
||||
return {"bytes": None, "path": str(value.absolute())}
|
||||
elif isinstance(value, (bytes, bytearray)):
|
||||
return {"bytes": value, "path": None}
|
||||
elif AudioDecoder is not None and isinstance(value, AudioDecoder):
|
||||
return encode_torchcodec_audio(value)
|
||||
elif "array" in value:
|
||||
# convert the audio array to wav bytes
|
||||
buffer = BytesIO()
|
||||
AudioEncoder(
|
||||
torch.from_numpy(value["array"].astype(np.float32)), sample_rate=value["sampling_rate"]
|
||||
).to_file_like(buffer, format="wav", num_channels=self.num_channels)
|
||||
return {"bytes": buffer.getvalue(), "path": None}
|
||||
elif value.get("path") is not None and os.path.isfile(value["path"]):
|
||||
# we set "bytes": None to not duplicate the data if they're already available locally
|
||||
if value["path"].endswith("pcm"):
|
||||
# "PCM" only has raw audio bytes
|
||||
if value.get("sampling_rate") is None:
|
||||
# At least, If you want to convert "PCM-byte" to "WAV-byte", you have to know sampling rate
|
||||
raise KeyError("To use PCM files, please specify a 'sampling_rate' in Audio object")
|
||||
if value.get("bytes"):
|
||||
# If we already had PCM-byte, we don`t have to make "read file, make bytes" (just use it!)
|
||||
bytes_value = np.frombuffer(value["bytes"], dtype=np.int16).astype(np.float32) / 32767
|
||||
else:
|
||||
bytes_value = np.memmap(value["path"], dtype="h", mode="r").astype(np.float32) / 32767
|
||||
|
||||
buffer = BytesIO()
|
||||
AudioEncoder(torch.from_numpy(bytes_value), sample_rate=value["sampling_rate"]).to_file_like(
|
||||
buffer, format="wav", num_channels=self.num_channels
|
||||
)
|
||||
return {"bytes": buffer.getvalue(), "path": None}
|
||||
else:
|
||||
return {"bytes": None, "path": value.get("path")}
|
||||
elif value.get("bytes") is not None or value.get("path") is not None:
|
||||
# store the audio bytes, and path is used to infer the audio format using the file extension
|
||||
return {"bytes": value.get("bytes"), "path": value.get("path")}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"An audio sample should have one of 'path' or 'bytes' but they are missing or None in {value}."
|
||||
)
|
||||
|
||||
def decode_example(
|
||||
self, value: dict, token_per_repo_id: Optional[dict[str, Union[str, bool, None]]] = None
|
||||
) -> "AudioDecoder":
|
||||
"""Decode example audio file into audio data.
|
||||
|
||||
Args:
|
||||
value (`dict`):
|
||||
A dictionary with keys:
|
||||
|
||||
- `path`: String with relative audio file path.
|
||||
- `bytes`: Bytes of the audio file.
|
||||
token_per_repo_id (`dict`, *optional*):
|
||||
To access and decode
|
||||
audio files from private repositories on the Hub, you can pass
|
||||
a dictionary repo_id (`str`) -> token (`bool` or `str`)
|
||||
|
||||
Returns:
|
||||
`torchcodec.decoders.AudioDecoder`
|
||||
"""
|
||||
if config.TORCHCODEC_AVAILABLE:
|
||||
from ._torchcodec import AudioDecoder
|
||||
else:
|
||||
raise ImportError("To support decoding audio data, please install 'torchcodec'.")
|
||||
|
||||
if not self.decode:
|
||||
raise RuntimeError("Decoding is disabled for this feature. Please use Audio(decode=True) instead.")
|
||||
|
||||
path, bytes = (value["path"], value["bytes"]) if value["bytes"] is not None else (value["path"], None)
|
||||
if path is None and bytes is None:
|
||||
raise ValueError(f"An audio sample should have one of 'path' or 'bytes' but both are None in {value}.")
|
||||
|
||||
if bytes is None and is_local_path(path):
|
||||
audio = AudioDecoder(
|
||||
path, stream_index=self.stream_index, sample_rate=self.sampling_rate, num_channels=self.num_channels
|
||||
)
|
||||
|
||||
elif bytes is None:
|
||||
token_per_repo_id = token_per_repo_id or {}
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
|
||||
download_config = DownloadConfig(token=token)
|
||||
f = xopen(path, "rb", download_config=download_config)
|
||||
audio = AudioDecoder(
|
||||
f, stream_index=self.stream_index, sample_rate=self.sampling_rate, num_channels=self.num_channels
|
||||
)
|
||||
|
||||
else:
|
||||
audio = AudioDecoder(
|
||||
bytes, stream_index=self.stream_index, sample_rate=self.sampling_rate, num_channels=self.num_channels
|
||||
)
|
||||
audio._hf_encoded = {"path": path, "bytes": bytes}
|
||||
audio.metadata.path = path
|
||||
return audio
|
||||
|
||||
def flatten(self) -> Union["FeatureType", dict[str, "FeatureType"]]:
|
||||
"""If in the decodable state, raise an error, otherwise flatten the feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
if self.decode:
|
||||
raise ValueError("Cannot flatten a decoded Audio feature.")
|
||||
return {
|
||||
"bytes": Value("binary"),
|
||||
"path": Value("string"),
|
||||
}
|
||||
|
||||
def cast_storage(self, storage: Union[pa.StringArray, pa.StructArray]) -> pa.StructArray:
|
||||
"""Cast an Arrow array to the Audio arrow storage type.
|
||||
The Arrow types that can be converted to the Audio pyarrow storage type are:
|
||||
|
||||
- `pa.string()` - it must contain the "path" data
|
||||
- `pa.binary()` - it must contain the audio bytes
|
||||
- `pa.struct({"bytes": pa.binary()})`
|
||||
- `pa.struct({"path": pa.string()})`
|
||||
- `pa.struct({"bytes": pa.binary(), "path": pa.string()})` - order doesn't matter
|
||||
|
||||
Args:
|
||||
storage (`Union[pa.StringArray, pa.StructArray]`):
|
||||
PyArrow array to cast.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Audio arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`
|
||||
"""
|
||||
if pa.types.is_string(storage.type):
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, storage], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_large_binary(storage.type):
|
||||
storage = array_cast(
|
||||
storage, pa.binary()
|
||||
) # this can fail in case of big audios, paths should be used instead
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_binary(storage.type):
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_struct(storage.type) and storage.type.get_all_field_indices("array"):
|
||||
storage = pa.array(
|
||||
[Audio().encode_example(x) if x is not None else None for x in storage.to_numpy(zero_copy_only=False)]
|
||||
)
|
||||
elif pa.types.is_struct(storage.type):
|
||||
if storage.type.get_field_index("bytes") >= 0:
|
||||
bytes_array = storage.field("bytes")
|
||||
else:
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
if storage.type.get_field_index("path") >= 0:
|
||||
path_array = storage.field("path")
|
||||
else:
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
def embed_storage(
|
||||
self, storage: pa.StructArray, token_per_repo_id=None, local_files: bool = True, remote_files: bool = True
|
||||
) -> pa.StructArray:
|
||||
"""Embed audio files into the Arrow array.
|
||||
|
||||
Args:
|
||||
storage (`pa.StructArray`):
|
||||
PyArrow array to embed.
|
||||
token_per_repo_id (`dict`, optional):
|
||||
Dictionary repo_id -> token to fetch the files bytes.
|
||||
local_files (`bool`, defaults to `True`)
|
||||
Whether to embed local files data in the array
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
remote_files (`bool`, defaults to `True`)
|
||||
Whether to embed remote files data in the array.
|
||||
E.g. files with paths that start with hf:// or https://
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Audio arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
@no_op_if_value_is_null
|
||||
def path_to_bytes(path):
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
return f.read()
|
||||
|
||||
bytes_array = pa.array(
|
||||
[
|
||||
(
|
||||
path_to_bytes(x["path"])
|
||||
if x["bytes"] is None
|
||||
and ((local_files and is_local_path(x["path"])) or (remote_files and is_remote_url(x["path"])))
|
||||
else x["bytes"]
|
||||
)
|
||||
if x is not None
|
||||
else None
|
||||
for x in storage.to_pylist()
|
||||
],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array(
|
||||
[
|
||||
(
|
||||
os.path.basename(path)
|
||||
if (local_files and is_local_path(path)) or (remote_files and is_remote_url(path))
|
||||
else path
|
||||
)
|
||||
if path is not None
|
||||
else None
|
||||
for path in storage.field("path").to_pylist()
|
||||
],
|
||||
type=pa.string(),
|
||||
)
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
|
||||
def encode_torchcodec_audio(audio: "AudioDecoder") -> dict:
|
||||
if hasattr(audio, "_hf_encoded"):
|
||||
return audio._hf_encoded
|
||||
else:
|
||||
try:
|
||||
from torchcodec.encoders import AudioEncoder # needed to write audio files
|
||||
except ImportError as err:
|
||||
raise ImportError("To support encoding audio data, please install 'torchcodec'.") from err
|
||||
|
||||
samples = audio.get_all_samples()
|
||||
buffer = BytesIO()
|
||||
num_channels = samples.data.shape[0]
|
||||
AudioEncoder(samples.data.cpu(), sample_rate=samples.sample_rate).to_file_like(
|
||||
buffer, format="wav", num_channels=num_channels
|
||||
)
|
||||
return {"bytes": buffer.getvalue(), "path": None}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,441 @@
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..download.download_config import DownloadConfig
|
||||
from ..table import array_cast
|
||||
from ..utils.file_utils import is_local_path, is_remote_url, xopen
|
||||
from ..utils.py_utils import first_non_null_value, no_op_if_value_is_null, string_to_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import PIL.Image
|
||||
|
||||
from .features import FeatureType
|
||||
|
||||
|
||||
_IMAGE_COMPRESSION_FORMATS: Optional[list[str]] = None
|
||||
_NATIVE_BYTEORDER = "<" if sys.byteorder == "little" else ">"
|
||||
# Origin: https://github.com/python-pillow/Pillow/blob/698951e19e19972aeed56df686868f1329981c12/src/PIL/Image.py#L3126 minus "|i1" which values are not preserved correctly when saving and loading an image
|
||||
_VALID_IMAGE_ARRAY_DTPYES = [
|
||||
np.dtype("|b1"),
|
||||
np.dtype("|u1"),
|
||||
np.dtype("<u2"),
|
||||
np.dtype(">u2"),
|
||||
np.dtype("<i2"),
|
||||
np.dtype(">i2"),
|
||||
np.dtype("<u4"),
|
||||
np.dtype(">u4"),
|
||||
np.dtype("<i4"),
|
||||
np.dtype(">i4"),
|
||||
np.dtype("<f4"),
|
||||
np.dtype(">f4"),
|
||||
np.dtype("<f8"),
|
||||
np.dtype(">f8"),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Image:
|
||||
"""Image [`Feature`] to read image data from an image file.
|
||||
|
||||
Input: The Image feature accepts as input:
|
||||
- A `str`: Absolute path to the image file (i.e. random access is allowed).
|
||||
- A `pathlib.Path`: path to the image file (i.e. random access is allowed).
|
||||
- A `dict` with the keys:
|
||||
|
||||
- `path`: String with relative path of the image file to the archive file.
|
||||
- `bytes`: Bytes of the image file.
|
||||
|
||||
This is useful for parquet or webdataset files which embed image files.
|
||||
|
||||
- An `np.ndarray`: NumPy array representing an image.
|
||||
- A `PIL.Image.Image`: PIL image object.
|
||||
|
||||
Output: The Image features output data as `PIL.Image.Image` objects.
|
||||
|
||||
Args:
|
||||
mode (`str`, *optional*):
|
||||
The mode to convert the image to. If `None`, the native mode of the image is used.
|
||||
decode (`bool`, defaults to `True`):
|
||||
Whether to decode the image data. If `False`,
|
||||
returns the underlying dictionary in the format `{"path": image_path, "bytes": image_bytes}`.
|
||||
|
||||
Examples:
|
||||
|
||||
```py
|
||||
>>> from datasets import load_dataset, Image
|
||||
>>> ds = load_dataset("AI-Lab-Makerere/beans", split="train")
|
||||
>>> ds.features["image"]
|
||||
Image(decode=True, id=None)
|
||||
>>> ds[0]["image"]
|
||||
<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=500x500 at 0x15E52E7F0>
|
||||
>>> ds = ds.cast_column('image', Image(decode=False))
|
||||
{'bytes': None,
|
||||
'path': '/root/.cache/huggingface/datasets/downloads/extracted/b0a21163f78769a2cf11f58dfc767fb458fc7cea5c05dccc0144a2c0f0bc1292/train/healthy/healthy_train.85.jpg'}
|
||||
```
|
||||
"""
|
||||
|
||||
mode: Optional[str] = None
|
||||
decode: bool = True
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "PIL.Image.Image"
|
||||
pa_type: ClassVar[Any] = pa.struct({"bytes": pa.binary(), "path": pa.string()})
|
||||
_type: str = field(default="Image", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return self.pa_type
|
||||
|
||||
def encode_example(self, value: Union[str, bytes, bytearray, dict, np.ndarray, "PIL.Image.Image"]) -> dict:
|
||||
"""Encode example into a format for Arrow.
|
||||
|
||||
Args:
|
||||
value (`str`, `np.ndarray`, `PIL.Image.Image` or `dict`):
|
||||
Data passed as input to Image feature.
|
||||
|
||||
Returns:
|
||||
`dict` with "path" and "bytes" fields
|
||||
"""
|
||||
if config.PIL_AVAILABLE:
|
||||
import PIL.Image
|
||||
else:
|
||||
raise ImportError("To support encoding images, please install 'Pillow'.")
|
||||
|
||||
if isinstance(value, list):
|
||||
value = np.array(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return {"path": value, "bytes": None}
|
||||
elif isinstance(value, Path):
|
||||
return {"path": str(value.absolute()), "bytes": None}
|
||||
elif isinstance(value, (bytes, bytearray)):
|
||||
return {"path": None, "bytes": value}
|
||||
elif isinstance(value, np.ndarray):
|
||||
# convert the image array to PNG/TIFF bytes
|
||||
return encode_np_array(value)
|
||||
elif isinstance(value, PIL.Image.Image):
|
||||
# convert the PIL image to bytes (default format is PNG/TIFF)
|
||||
return encode_pil_image(value)
|
||||
elif value.get("path") is not None and os.path.isfile(value["path"]):
|
||||
# we set "bytes": None to not duplicate the data if they're already available locally
|
||||
return {"bytes": None, "path": value.get("path")}
|
||||
elif value.get("bytes") is not None or value.get("path") is not None:
|
||||
# store the image bytes, and path is used to infer the image format using the file extension
|
||||
return {"bytes": value.get("bytes"), "path": value.get("path")}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"An image sample should have one of 'path' or 'bytes' but they are missing or None in {value}."
|
||||
)
|
||||
|
||||
def decode_example(self, value: dict, token_per_repo_id=None) -> "PIL.Image.Image":
|
||||
"""Decode example image file into image data.
|
||||
|
||||
Args:
|
||||
value (`str` or `dict`):
|
||||
A string with the absolute image file path, a dictionary with
|
||||
keys:
|
||||
|
||||
- `path`: String with absolute or relative image file path.
|
||||
- `bytes`: The bytes of the image file.
|
||||
token_per_repo_id (`dict`, *optional*):
|
||||
To access and decode
|
||||
image files from private repositories on the Hub, you can pass
|
||||
a dictionary repo_id (`str`) -> token (`bool` or `str`).
|
||||
|
||||
Returns:
|
||||
`PIL.Image.Image`
|
||||
"""
|
||||
if not self.decode:
|
||||
raise RuntimeError("Decoding is disabled for this feature. Please use Image(decode=True) instead.")
|
||||
|
||||
if config.PIL_AVAILABLE:
|
||||
import PIL.Image
|
||||
import PIL.ImageOps
|
||||
else:
|
||||
raise ImportError("To support decoding images, please install 'Pillow'.")
|
||||
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
path, bytes_ = value["path"], value["bytes"]
|
||||
if bytes_ is None:
|
||||
if path is None:
|
||||
raise ValueError(f"An image should have one of 'path' or 'bytes' but both are None in {value}.")
|
||||
else:
|
||||
if is_local_path(path):
|
||||
image = PIL.Image.open(path)
|
||||
else:
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL
|
||||
if source_url.startswith(config.HF_ENDPOINT)
|
||||
else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = (
|
||||
token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
)
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
bytes_ = BytesIO(f.read())
|
||||
image = PIL.Image.open(bytes_)
|
||||
else:
|
||||
image = PIL.Image.open(BytesIO(bytes_))
|
||||
image.load() # to avoid "Too many open files" errors
|
||||
if image.getexif().get(PIL.Image.ExifTags.Base.Orientation) is not None:
|
||||
image = PIL.ImageOps.exif_transpose(image)
|
||||
if self.mode and self.mode != image.mode:
|
||||
image = image.convert(self.mode)
|
||||
return image
|
||||
|
||||
def flatten(self) -> Union["FeatureType", dict[str, "FeatureType"]]:
|
||||
"""If in the decodable state, return the feature itself, otherwise flatten the feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
return (
|
||||
self
|
||||
if self.decode
|
||||
else {
|
||||
"bytes": Value("binary"),
|
||||
"path": Value("string"),
|
||||
}
|
||||
)
|
||||
|
||||
def cast_storage(self, storage: Union[pa.StringArray, pa.StructArray, pa.ListArray]) -> pa.StructArray:
|
||||
"""Cast an Arrow array to the Image arrow storage type.
|
||||
The Arrow types that can be converted to the Image pyarrow storage type are:
|
||||
|
||||
- `pa.string()` - it must contain the "path" data
|
||||
- `pa.large_string()` - it must contain the "path" data (will be cast to string if possible)
|
||||
- `pa.binary()` - it must contain the image bytes
|
||||
- `pa.struct({"bytes": pa.binary()})`
|
||||
- `pa.struct({"path": pa.string()})`
|
||||
- `pa.struct({"bytes": pa.binary(), "path": pa.string()})` - order doesn't matter
|
||||
- `pa.list(*)` - it must contain the image array data
|
||||
|
||||
Args:
|
||||
storage (`Union[pa.StringArray, pa.StructArray, pa.ListArray]`):
|
||||
PyArrow array to cast.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Image arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if pa.types.is_large_string(storage.type):
|
||||
try:
|
||||
storage = storage.cast(pa.string())
|
||||
except pa.ArrowInvalid as e:
|
||||
raise ValueError(
|
||||
f"Failed to cast large_string to string for Image feature. "
|
||||
f"This can happen if string values exceed 2GB. "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
if pa.types.is_string(storage.type):
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, storage], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_large_binary(storage.type):
|
||||
storage = array_cast(
|
||||
storage, pa.binary()
|
||||
) # this can fail in case of big images, paths should be used instead
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_binary(storage.type):
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_struct(storage.type):
|
||||
if storage.type.get_field_index("bytes") >= 0:
|
||||
bytes_array = storage.field("bytes")
|
||||
else:
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
if storage.type.get_field_index("path") >= 0:
|
||||
path_array = storage.field("path")
|
||||
else:
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_list(storage.type):
|
||||
bytes_array = pa.array(
|
||||
[encode_np_array(np.array(arr))["bytes"] if arr is not None else None for arr in storage.to_pylist()],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays(
|
||||
[bytes_array, path_array], ["bytes", "path"], mask=bytes_array.is_null()
|
||||
)
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
def embed_storage(
|
||||
self, storage: pa.StructArray, token_per_repo_id=None, local_files: bool = True, remote_files: bool = True
|
||||
) -> pa.StructArray:
|
||||
"""Embed image files into the Arrow array.
|
||||
|
||||
Args:
|
||||
storage (`pa.StructArray`):
|
||||
PyArrow array to embed.
|
||||
token_per_repo_id (`dict`, optional):
|
||||
Dictionary repo_id -> token to fetch the files bytes.
|
||||
local_files (`bool`, defaults to `True`)
|
||||
Whether to embed local files data in the array
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
remote_files (`bool`, defaults to `True`)
|
||||
Whether to embed remote files data in the array.
|
||||
E.g. files with paths that start with hf:// or https://
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Image arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
@no_op_if_value_is_null
|
||||
def path_to_bytes(path):
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
return f.read()
|
||||
|
||||
bytes_array = pa.array(
|
||||
[
|
||||
(
|
||||
path_to_bytes(x["path"])
|
||||
if x["bytes"] is None
|
||||
and ((local_files and is_local_path(x["path"])) or (remote_files and is_remote_url(x["path"])))
|
||||
else x["bytes"]
|
||||
)
|
||||
if x is not None
|
||||
else None
|
||||
for x in storage.to_pylist()
|
||||
],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array(
|
||||
[
|
||||
(
|
||||
os.path.basename(path)
|
||||
if (local_files and is_local_path(path)) or (remote_files and is_remote_url(path))
|
||||
else path
|
||||
)
|
||||
if path is not None
|
||||
else None
|
||||
for path in storage.field("path").to_pylist()
|
||||
],
|
||||
type=pa.string(),
|
||||
)
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
|
||||
def list_image_compression_formats() -> list[str]:
|
||||
if config.PIL_AVAILABLE:
|
||||
import PIL.Image
|
||||
else:
|
||||
raise ImportError("To support encoding images, please install 'Pillow'.")
|
||||
|
||||
global _IMAGE_COMPRESSION_FORMATS
|
||||
if _IMAGE_COMPRESSION_FORMATS is None:
|
||||
PIL.Image.init()
|
||||
_IMAGE_COMPRESSION_FORMATS = list(set(PIL.Image.OPEN.keys()) & set(PIL.Image.SAVE.keys()))
|
||||
return _IMAGE_COMPRESSION_FORMATS
|
||||
|
||||
|
||||
def image_to_bytes(image: "PIL.Image.Image") -> bytes:
|
||||
"""Convert a PIL Image object to bytes using native compression if possible, otherwise use PNG/TIFF compression."""
|
||||
buffer = BytesIO()
|
||||
if image.format in list_image_compression_formats():
|
||||
format = image.format
|
||||
else:
|
||||
format = "PNG" if image.mode in ["1", "L", "LA", "RGB", "RGBA"] else "TIFF"
|
||||
image.save(buffer, format=format)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def encode_pil_image(image: "PIL.Image.Image") -> dict:
|
||||
if hasattr(image, "filename") and image.filename != "":
|
||||
return {"path": image.filename, "bytes": None}
|
||||
else:
|
||||
return {"path": None, "bytes": image_to_bytes(image)}
|
||||
|
||||
|
||||
def encode_np_array(array: np.ndarray) -> dict:
|
||||
if config.PIL_AVAILABLE:
|
||||
import PIL.Image
|
||||
else:
|
||||
raise ImportError("To support encoding images, please install 'Pillow'.")
|
||||
|
||||
dtype = array.dtype
|
||||
dtype_byteorder = dtype.byteorder if dtype.byteorder != "=" else _NATIVE_BYTEORDER
|
||||
dtype_kind = dtype.kind
|
||||
dtype_itemsize = dtype.itemsize
|
||||
|
||||
dest_dtype = None
|
||||
|
||||
# Multi-channel array case (only np.dtype("|u1") is allowed)
|
||||
if array.shape[2:]:
|
||||
if dtype_kind not in ["u", "i"]:
|
||||
raise TypeError(
|
||||
f"Unsupported array dtype {dtype} for image encoding. Only {dest_dtype} is supported for multi-channel arrays."
|
||||
)
|
||||
dest_dtype = np.dtype("|u1")
|
||||
if dtype != dest_dtype:
|
||||
warnings.warn(f"Downcasting array dtype {dtype} to {dest_dtype} to be compatible with 'Pillow'")
|
||||
# Exact match
|
||||
elif dtype in _VALID_IMAGE_ARRAY_DTPYES:
|
||||
dest_dtype = dtype
|
||||
else: # Downcast the type within the kind (np.can_cast(from_type, to_type, casting="same_kind") doesn't behave as expected, so do it manually)
|
||||
while dtype_itemsize >= 1:
|
||||
dtype_str = dtype_byteorder + dtype_kind + str(dtype_itemsize)
|
||||
if np.dtype(dtype_str) in _VALID_IMAGE_ARRAY_DTPYES:
|
||||
dest_dtype = np.dtype(dtype_str)
|
||||
warnings.warn(f"Downcasting array dtype {dtype} to {dest_dtype} to be compatible with 'Pillow'")
|
||||
break
|
||||
else:
|
||||
dtype_itemsize //= 2
|
||||
if dest_dtype is None:
|
||||
raise TypeError(
|
||||
f"Cannot downcast dtype {dtype} to a valid image dtype. Valid image dtypes: {_VALID_IMAGE_ARRAY_DTPYES}"
|
||||
)
|
||||
|
||||
image = PIL.Image.fromarray(array.astype(dest_dtype))
|
||||
return {"path": None, "bytes": image_to_bytes(image)}
|
||||
|
||||
|
||||
def objects_to_list_of_image_dicts(
|
||||
objs: Union[list[str], list[dict], list[np.ndarray], list["PIL.Image.Image"]],
|
||||
) -> list[dict]:
|
||||
"""Encode a list of objects into a format suitable for creating an extension array of type `ImageExtensionType`."""
|
||||
if config.PIL_AVAILABLE:
|
||||
import PIL.Image
|
||||
else:
|
||||
raise ImportError("To support encoding images, please install 'Pillow'.")
|
||||
|
||||
if objs:
|
||||
_, obj = first_non_null_value(objs)
|
||||
if isinstance(obj, str):
|
||||
return [{"path": obj, "bytes": None} if obj is not None else None for obj in objs]
|
||||
if isinstance(obj, np.ndarray):
|
||||
obj_to_image_dict_func = no_op_if_value_is_null(encode_np_array)
|
||||
return [obj_to_image_dict_func(obj) for obj in objs]
|
||||
elif isinstance(obj, PIL.Image.Image):
|
||||
obj_to_image_dict_func = no_op_if_value_is_null(encode_pil_image)
|
||||
return [obj_to_image_dict_func(obj) for obj in objs]
|
||||
else:
|
||||
return objs
|
||||
else:
|
||||
return objs
|
||||
@@ -0,0 +1,295 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..download.download_config import DownloadConfig
|
||||
from ..table import array_cast
|
||||
from ..utils.file_utils import is_local_path, is_remote_url, xopen
|
||||
from ..utils.py_utils import string_to_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import trimesh
|
||||
|
||||
from .features import FeatureType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mesh:
|
||||
"""Mesh [`Feature`] to read 3D mesh data from a file.
|
||||
|
||||
Input: The Mesh feature accepts as input:
|
||||
- A `str`: Absolute path to the mesh file (i.e. random access is allowed).
|
||||
- A `pathlib.Path`: path to the mesh file (i.e. random access is allowed).
|
||||
- A `dict` with the keys:
|
||||
|
||||
- `path`: String with relative path of the mesh file to the archive file.
|
||||
- `bytes`: Bytes of the mesh file.
|
||||
|
||||
This is useful for parquet or webdataset files which embed mesh files.
|
||||
|
||||
- A `trimesh.Trimesh` or `trimesh.Scene`: 3D mesh or scene object.
|
||||
|
||||
Output: The Mesh feature outputs data as `trimesh.Trimesh` or `trimesh.Scene` objects.
|
||||
|
||||
Args:
|
||||
decode (`bool`, defaults to `True`):
|
||||
Whether to decode the mesh data. If `False`,
|
||||
returns the underlying dictionary in the format `{"path": mesh_path, "bytes": mesh_bytes}`.
|
||||
Mesh decoding uses `trimesh` and supports `.glb`, `.ply`, and `.stl` files.
|
||||
"""
|
||||
|
||||
decode: bool = True
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "trimesh.Trimesh"
|
||||
pa_type: ClassVar[Any] = pa.struct({"bytes": pa.binary(), "path": pa.string()})
|
||||
_type: str = field(default="Mesh", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return self.pa_type
|
||||
|
||||
def encode_example(self, value: Union[str, bytes, bytearray, dict, "trimesh.Trimesh", "trimesh.Scene"]) -> dict:
|
||||
"""Encode example into a format for Arrow.
|
||||
|
||||
Args:
|
||||
value (`str`, `bytes`, `dict`, `trimesh.Trimesh`, or `trimesh.Scene`):
|
||||
Data passed as input to Mesh feature.
|
||||
|
||||
Returns:
|
||||
`dict` with "path" and "bytes" fields
|
||||
"""
|
||||
if config.TRIMESH_AVAILABLE:
|
||||
import trimesh
|
||||
else:
|
||||
trimesh = None
|
||||
|
||||
if isinstance(value, str):
|
||||
return {"path": value, "bytes": None}
|
||||
elif isinstance(value, Path):
|
||||
return {"path": str(value.absolute()), "bytes": None}
|
||||
elif isinstance(value, (bytes, bytearray)):
|
||||
return {"path": None, "bytes": value}
|
||||
elif trimesh is not None and isinstance(value, (trimesh.Trimesh, trimesh.Scene)):
|
||||
return encode_trimesh_mesh(value)
|
||||
elif isinstance(value, dict) and value.get("path") is not None and os.path.isfile(value["path"]):
|
||||
# we set "bytes": None to not duplicate the data if they're already available locally
|
||||
return {"bytes": None, "path": value.get("path")}
|
||||
elif isinstance(value, dict) and (value.get("bytes") is not None or value.get("path") is not None):
|
||||
# store the mesh bytes, and path is used to infer the mesh format using the file extension
|
||||
return {"bytes": value.get("bytes"), "path": value.get("path")}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"A mesh sample should have one of 'path' or 'bytes' but they are missing or None in {value}."
|
||||
)
|
||||
|
||||
def decode_example(self, value: dict, token_per_repo_id=None) -> Union["trimesh.Trimesh", "trimesh.Scene"]:
|
||||
"""Decode example mesh file.
|
||||
|
||||
Args:
|
||||
value (`dict`):
|
||||
A dictionary with keys:
|
||||
|
||||
- `path`: String with absolute or relative mesh file path.
|
||||
- `bytes`: The bytes of the mesh file.
|
||||
token_per_repo_id (`dict`, *optional*):
|
||||
To access and decode
|
||||
mesh files from private repositories on the Hub, you can pass
|
||||
a dictionary repo_id (`str`) -> token (`bool` or `str`).
|
||||
|
||||
Returns:
|
||||
`trimesh.Trimesh` or `trimesh.Scene`
|
||||
"""
|
||||
if not self.decode:
|
||||
raise RuntimeError("Decoding is disabled for this feature. Please use Mesh(decode=True) instead.")
|
||||
|
||||
if config.TRIMESH_AVAILABLE:
|
||||
import trimesh
|
||||
else:
|
||||
raise ImportError("To support decoding meshes, please install 'trimesh'.")
|
||||
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
path, bytes_ = value["path"], value["bytes"]
|
||||
if bytes_ is None:
|
||||
if path is None:
|
||||
raise ValueError(f"A mesh should have one of 'path' or 'bytes' but both are None in {value}.")
|
||||
if is_local_path(path):
|
||||
file_type = _infer_mesh_file_type(path)
|
||||
if file_type is None:
|
||||
raise ValueError("A mesh path should have a .glb, .ply, or .stl extension.")
|
||||
return trimesh.load(path, file_type=file_type)
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
bytes_ = f.read()
|
||||
|
||||
file_type = _infer_mesh_file_type(path)
|
||||
if file_type is None:
|
||||
raise ValueError(
|
||||
"Decoding mesh bytes requires a 'path' value with a .glb, .ply, or .stl extension "
|
||||
"to infer the mesh file type."
|
||||
)
|
||||
return trimesh.load(BytesIO(bytes_), file_type=file_type)
|
||||
|
||||
def flatten(self) -> Union["FeatureType", dict[str, "FeatureType"]]:
|
||||
"""If in the decodable state, return the feature itself, otherwise flatten the feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
return (
|
||||
self
|
||||
if self.decode
|
||||
else {
|
||||
"bytes": Value("binary"),
|
||||
"path": Value("string"),
|
||||
}
|
||||
)
|
||||
|
||||
def cast_storage(self, storage: Union[pa.StringArray, pa.StructArray]) -> pa.StructArray:
|
||||
"""Cast an Arrow array to the Mesh arrow storage type.
|
||||
The Arrow types that can be converted to the Mesh pyarrow storage type are:
|
||||
|
||||
- `pa.string()` - it must contain the "path" data
|
||||
- `pa.large_string()` - it must contain the "path" data (will be cast to string if possible)
|
||||
- `pa.binary()` - it must contain the mesh bytes
|
||||
- `pa.struct({"bytes": pa.binary()})`
|
||||
- `pa.struct({"path": pa.string()})`
|
||||
- `pa.struct({"bytes": pa.binary(), "path": pa.string()})` - order doesn't matter
|
||||
|
||||
Args:
|
||||
storage (`Union[pa.StringArray, pa.StructArray]`):
|
||||
PyArrow array to cast.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Mesh arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if pa.types.is_large_string(storage.type):
|
||||
try:
|
||||
storage = storage.cast(pa.string())
|
||||
except pa.ArrowInvalid as e:
|
||||
raise ValueError(
|
||||
f"Failed to cast large_string to string for Mesh feature. "
|
||||
f"This can happen if string values exceed 2GB. "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
if pa.types.is_string(storage.type):
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, storage], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_large_binary(storage.type):
|
||||
storage = array_cast(
|
||||
storage, pa.binary()
|
||||
) # this can fail in case of big meshes, paths should be used instead
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_binary(storage.type):
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_struct(storage.type):
|
||||
if storage.type.get_field_index("bytes") >= 0:
|
||||
bytes_array = storage.field("bytes")
|
||||
else:
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
if storage.type.get_field_index("path") >= 0:
|
||||
path_array = storage.field("path")
|
||||
else:
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
def embed_storage(
|
||||
self, storage: pa.StructArray, token_per_repo_id=None, local_files: bool = True, remote_files: bool = True
|
||||
) -> pa.StructArray:
|
||||
"""Embed mesh files into the Arrow array.
|
||||
|
||||
Args:
|
||||
storage (`pa.StructArray`):
|
||||
PyArrow array to embed.
|
||||
token_per_repo_id (`dict`, optional):
|
||||
Dictionary repo_id -> token to fetch the files bytes.
|
||||
local_files (`bool`, defaults to `True`):
|
||||
Whether to embed local files data in the array.
|
||||
remote_files (`bool`, defaults to `True`):
|
||||
Whether to embed remote files data in the array.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Mesh arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
def path_to_bytes(path):
|
||||
if path is None:
|
||||
return None
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
return f.read()
|
||||
|
||||
bytes_array = pa.array(
|
||||
[
|
||||
(
|
||||
path_to_bytes(x["path"])
|
||||
if x["bytes"] is None
|
||||
and ((local_files and is_local_path(x["path"])) or (remote_files and is_remote_url(x["path"])))
|
||||
else x["bytes"]
|
||||
)
|
||||
if x is not None
|
||||
else None
|
||||
for x in storage.to_pylist()
|
||||
],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array(
|
||||
[
|
||||
(
|
||||
os.path.basename(path)
|
||||
if (local_files and is_local_path(path)) or (remote_files and is_remote_url(path))
|
||||
else path
|
||||
)
|
||||
if path is not None
|
||||
else None
|
||||
for path in storage.field("path").to_pylist()
|
||||
],
|
||||
type=pa.string(),
|
||||
)
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
|
||||
def _infer_mesh_file_type(path: Optional[str]) -> Optional[str]:
|
||||
supported_file_types = {"glb", "ply", "stl"}
|
||||
if path is None:
|
||||
return None
|
||||
path_without_archive = path.split("::", 1)[0]
|
||||
path_without_query = path_without_archive.split("?", 1)[0]
|
||||
extension = os.path.splitext(path_without_query)[1].lower().lstrip(".")
|
||||
return extension if extension in supported_file_types else None
|
||||
|
||||
|
||||
def encode_trimesh_mesh(mesh: Union["trimesh.Trimesh", "trimesh.Scene"]) -> dict[str, Optional[bytes | str]]:
|
||||
"""Encode a trimesh mesh or scene object into GLB bytes."""
|
||||
metadata = getattr(mesh, "metadata", None) or {}
|
||||
path = metadata.get("file_path") or metadata.get("file_name") if isinstance(metadata, dict) else None
|
||||
if path is not None and os.path.isfile(path):
|
||||
return {"path": path, "bytes": None}
|
||||
bytes_ = mesh.export(file_type="glb")
|
||||
return {"path": os.path.basename(path) if path else "mesh.glb", "bytes": bytes_}
|
||||
@@ -0,0 +1,351 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..download.download_config import DownloadConfig
|
||||
from ..table import array_cast
|
||||
from ..utils.file_utils import is_local_path, is_remote_url, xopen
|
||||
from ..utils.py_utils import no_op_if_value_is_null, string_to_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import nibabel as nib
|
||||
|
||||
from .features import FeatureType
|
||||
|
||||
if config.NIBABEL_AVAILABLE:
|
||||
import nibabel as nib
|
||||
|
||||
class Nifti1ImageWrapper(nib.nifti1.Nifti1Image):
|
||||
"""
|
||||
A wrapper around nibabel's Nifti1Image to customize its representation.
|
||||
"""
|
||||
|
||||
def __init__(self, nifti_image: nib.nifti1.Nifti1Image):
|
||||
super().__init__(
|
||||
dataobj=nifti_image.dataobj,
|
||||
affine=nifti_image.affine,
|
||||
header=nifti_image.header,
|
||||
extra=nifti_image.extra,
|
||||
file_map=nifti_image.file_map,
|
||||
dtype=nifti_image.get_data_dtype(),
|
||||
)
|
||||
self.nifti_image = nifti_image
|
||||
|
||||
def _repr_html_(self):
|
||||
from ipyniivue import NiiVue, ShowRender, SliceType, Volume
|
||||
from IPython.display import display
|
||||
|
||||
bytes_ = self.nifti_image.to_bytes()
|
||||
nv = NiiVue()
|
||||
nv.set_slice_type(SliceType.MULTIPLANAR)
|
||||
nv.opts.multiplanar_show_render = ShowRender.ALWAYS
|
||||
nv.opts.show_3d_crosshair = True
|
||||
nv.opts.multiplanar_force_render = True
|
||||
name = None
|
||||
if hasattr(self.nifti_image, "file_map"):
|
||||
if (
|
||||
"image" in self.nifti_image.file_map
|
||||
and getattr(self.nifti_image.file_map["image"], "filename", None) is not None
|
||||
):
|
||||
name = self.nifti_image.file_map["image"].filename
|
||||
if name is None:
|
||||
name = "volume.nii.gz"
|
||||
volume = Volume(name=name, data=bytes_)
|
||||
nv.load_volumes([volume])
|
||||
display(nv)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Nifti:
|
||||
"""
|
||||
**Experimental.**
|
||||
Nifti [`Feature`] to read NIfTI neuroimaging files.
|
||||
|
||||
Input: The Nifti feature accepts as input:
|
||||
- A `str`: Absolute path to the NIfTI file (i.e. random access is allowed).
|
||||
- A `pathlib.Path`: path to the NIfTI file (i.e. random access is allowed).
|
||||
- A `dict` with the keys:
|
||||
- `path`: String with relative path of the NIfTI file in a dataset repository.
|
||||
- `bytes`: Bytes of the NIfTI file.
|
||||
This is useful for archived files with sequential access.
|
||||
|
||||
- A `nibabel` image object (e.g., `nibabel.nifti1.Nifti1Image`).
|
||||
|
||||
Args:
|
||||
decode (`bool`, defaults to `True`):
|
||||
Whether to decode the NIfTI data. If `False` a string with the bytes is returned. `decode=False` is not supported when decoding examples.
|
||||
|
||||
Examples:
|
||||
|
||||
```py
|
||||
>>> from datasets import Dataset, Nifti
|
||||
>>> ds = Dataset.from_dict({"nifti": ["path/to/file.nii.gz"]}).cast_column("nifti", Nifti())
|
||||
>>> ds.features["nifti"]
|
||||
Nifti(decode=True, id=None)
|
||||
>>> ds[0]["nifti"]
|
||||
<nibabel.nifti1.Nifti1Image object at 0x7f8a1c2d8f40>
|
||||
>>> ds = ds.cast_column("nifti", Nifti(decode=False))
|
||||
>>> ds[0]["nifti"]
|
||||
{'bytes': None,
|
||||
'path': 'path/to/file.nii.gz'}
|
||||
```
|
||||
"""
|
||||
|
||||
decode: bool = True
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "nibabel.nifti1.Nifti1Image"
|
||||
pa_type: ClassVar[Any] = pa.struct({"bytes": pa.binary(), "path": pa.string()})
|
||||
_type: str = field(default="Nifti", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return self.pa_type
|
||||
|
||||
def encode_example(self, value: Union[str, bytes, bytearray, dict, "nib.Nifti1Image"]) -> dict:
|
||||
"""Encode example into a format for Arrow.
|
||||
|
||||
Args:
|
||||
value (`str`, `bytes`, `nibabel.Nifti1Image` or `dict`):
|
||||
Data passed as input to Nifti feature.
|
||||
|
||||
Returns:
|
||||
`dict` with "path" and "bytes" fields
|
||||
"""
|
||||
if config.NIBABEL_AVAILABLE:
|
||||
import nibabel as nib
|
||||
else:
|
||||
nib = None
|
||||
|
||||
if isinstance(value, str):
|
||||
return {"path": value, "bytes": None}
|
||||
elif isinstance(value, Path):
|
||||
return {"path": str(value.absolute()), "bytes": None}
|
||||
elif isinstance(value, (bytes, bytearray)):
|
||||
return {"path": None, "bytes": value}
|
||||
elif nib is not None and isinstance(value, nib.spatialimages.SpatialImage):
|
||||
# nibabel image object - try to get path or convert to bytes
|
||||
return encode_nibabel_image(value)
|
||||
elif isinstance(value, dict):
|
||||
if value.get("path") is not None and os.path.isfile(value["path"]):
|
||||
# we set "bytes": None to not duplicate the data if they're already available locally
|
||||
return {"bytes": None, "path": value.get("path")}
|
||||
elif value.get("bytes") is not None or value.get("path") is not None:
|
||||
# store the nifti bytes, and path is used to infer the format using the file extension
|
||||
return {"bytes": value.get("bytes"), "path": value.get("path")}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"A nifti sample should have one of 'path' or 'bytes' but they are missing or None in {value}."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"A nifti sample should be a string, bytes, Path, nibabel image, or dict, but got {type(value)}."
|
||||
)
|
||||
|
||||
def decode_example(self, value: dict, token_per_repo_id=None) -> "Nifti1ImageWrapper":
|
||||
"""Decode example NIfTI file into nibabel image object.
|
||||
|
||||
Args:
|
||||
value (`str` or `dict`):
|
||||
A string with the absolute NIfTI file path, a dictionary with
|
||||
keys:
|
||||
|
||||
- `path`: String with absolute or relative NIfTI file path.
|
||||
- `bytes`: The bytes of the NIfTI file.
|
||||
|
||||
token_per_repo_id (`dict`, *optional*):
|
||||
To access and decode NIfTI files from private repositories on
|
||||
the Hub, you can pass a dictionary
|
||||
repo_id (`str`) -> token (`bool` or `str`).
|
||||
|
||||
Returns:
|
||||
`nibabel.Nifti1Image` objects
|
||||
"""
|
||||
if config.NIBABEL_AVAILABLE:
|
||||
import nibabel as nib
|
||||
else:
|
||||
raise ImportError("To support decoding NIfTI files, please install 'nibabel'.")
|
||||
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
path, bytes_ = value["path"], value["bytes"]
|
||||
if bytes_ is None:
|
||||
if path is None:
|
||||
raise ValueError(f"A nifti should have one of 'path' or 'bytes' but both are None in {value}.")
|
||||
else:
|
||||
# gzipped files have the structure: 'gzip://T1.nii::<local_path>'
|
||||
if path.startswith("gzip://") and is_local_path(path.split("::")[-1]):
|
||||
path = path.split("::")[-1]
|
||||
if is_local_path(path):
|
||||
nifti = nib.load(path)
|
||||
else:
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL
|
||||
if source_url.startswith(config.HF_ENDPOINT)
|
||||
else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = (
|
||||
token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
)
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
nifti = nib.load(f)
|
||||
else:
|
||||
import gzip
|
||||
|
||||
if (
|
||||
bytes_[:2] == b"\x1f\x8b"
|
||||
): # gzip magic number, see https://stackoverflow.com/a/76055284/9534390 or "Magic number" on https://en.wikipedia.org/wiki/Gzip
|
||||
bytes_ = gzip.decompress(bytes_)
|
||||
|
||||
nifti = nib.Nifti1Image.from_bytes(bytes_)
|
||||
|
||||
return Nifti1ImageWrapper(nifti)
|
||||
|
||||
def embed_storage(
|
||||
self, storage: pa.StructArray, token_per_repo_id=None, local_files: bool = True, remote_files: bool = True
|
||||
) -> pa.StructArray:
|
||||
"""Embed NifTI files into the Arrow array.
|
||||
|
||||
Args:
|
||||
storage (`pa.StructArray`):
|
||||
PyArrow array to embed.
|
||||
token_per_repo_id (`dict`, optional):
|
||||
Dictionary repo_id -> token to fetch the files bytes.
|
||||
local_files (`bool`, defaults to `True`)
|
||||
Whether to embed local files data in the array
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
remote_files (`bool`, defaults to `True`)
|
||||
Whether to embed remote files data in the array.
|
||||
E.g. files with paths that start with hf:// or https://
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the NifTI arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
@no_op_if_value_is_null
|
||||
def path_to_bytes(path):
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
return f.read()
|
||||
|
||||
bytes_array = pa.array(
|
||||
[
|
||||
(
|
||||
path_to_bytes(x["path"])
|
||||
if x["bytes"] is None
|
||||
and ((local_files and is_local_path(x["path"])) or (remote_files and is_remote_url(x["path"])))
|
||||
else x["bytes"]
|
||||
)
|
||||
if x is not None
|
||||
else None
|
||||
for x in storage.to_pylist()
|
||||
],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array(
|
||||
[
|
||||
(
|
||||
os.path.basename(path)
|
||||
if (local_files and is_local_path(path)) or (remote_files and is_remote_url(path))
|
||||
else path
|
||||
)
|
||||
if path is not None
|
||||
else None
|
||||
for path in storage.field("path").to_pylist()
|
||||
],
|
||||
type=pa.string(),
|
||||
)
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
def flatten(self) -> Union["FeatureType", Dict[str, "FeatureType"]]:
|
||||
"""If in the decodable state, return the feature itself, otherwise flatten the feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
return (
|
||||
self
|
||||
if self.decode
|
||||
else {
|
||||
"bytes": Value("binary"),
|
||||
"path": Value("string"),
|
||||
}
|
||||
)
|
||||
|
||||
def cast_storage(self, storage: Union[pa.StringArray, pa.StructArray, pa.BinaryArray]) -> pa.StructArray:
|
||||
"""Cast an Arrow array to the Nifti arrow storage type.
|
||||
The Arrow types that can be converted to the Nifti pyarrow storage type are:
|
||||
|
||||
- `pa.string()` - it must contain the "path" data
|
||||
- `pa.binary()` - it must contain the NIfTI bytes
|
||||
- `pa.struct({"bytes": pa.binary()})`
|
||||
- `pa.struct({"path": pa.string()})`
|
||||
- `pa.struct({"bytes": pa.binary(), "path": pa.string()})` - order doesn't matter
|
||||
|
||||
Args:
|
||||
storage (`Union[pa.StringArray, pa.StructArray, pa.BinaryArray]`):
|
||||
PyArrow array to cast.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Nifti arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if pa.types.is_string(storage.type):
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, storage], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_binary(storage.type):
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_struct(storage.type):
|
||||
if storage.type.get_field_index("bytes") >= 0:
|
||||
bytes_array = storage.field("bytes")
|
||||
else:
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
if storage.type.get_field_index("path") >= 0:
|
||||
path_array = storage.field("path")
|
||||
else:
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
|
||||
def encode_nibabel_image(img: "nib.Nifti1Image", force_bytes: bool = False) -> dict[str, Optional[Union[str, bytes]]]:
|
||||
"""
|
||||
Encode a nibabel image object into a dictionary.
|
||||
|
||||
If the image has an associated file path, returns the path. Otherwise, serializes
|
||||
the image content into bytes.
|
||||
|
||||
Args:
|
||||
img: A nibabel image object (e.g., Nifti1Image).
|
||||
force_bytes: If `True`, always serialize to bytes even if a file path exists. Needed to upload bytes properly.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with "path" or "bytes" field.
|
||||
"""
|
||||
if hasattr(img, "file_map") and img.file_map is not None and not force_bytes:
|
||||
filename = img.file_map["image"].filename
|
||||
return {"path": filename, "bytes": None}
|
||||
|
||||
bytes_data = img.to_bytes()
|
||||
return {"path": None, "bytes": bytes_data}
|
||||
@@ -0,0 +1,309 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..download.download_config import DownloadConfig
|
||||
from ..table import array_cast
|
||||
from ..utils.file_utils import is_local_path, is_remote_url, xopen
|
||||
from ..utils.py_utils import no_op_if_value_is_null, string_to_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pdfplumber
|
||||
|
||||
from .features import FeatureType
|
||||
|
||||
|
||||
def pdf_to_bytes(pdf: "pdfplumber.pdf.PDF") -> bytes:
|
||||
"""Convert a pdfplumber.pdf.PDF object to bytes."""
|
||||
with BytesIO() as buffer:
|
||||
for page in pdf.pages:
|
||||
buffer.write(page.pdf.stream)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pdf:
|
||||
"""
|
||||
**Experimental.**
|
||||
Pdf [`Feature`] to read pdf documents from a pdf file.
|
||||
|
||||
Input: The Pdf feature accepts as input:
|
||||
- A `str`: Absolute path to the pdf file (i.e. random access is allowed).
|
||||
- A `pathlib.Path`: path to the pdf file (i.e. random access is allowed).
|
||||
- A `dict` with the keys:
|
||||
- `path`: String with relative path of the pdf file in a dataset repository.
|
||||
- `bytes`: Bytes of the pdf file.
|
||||
This is useful for archived files with sequential access.
|
||||
|
||||
- A `pdfplumber.pdf.PDF`: pdfplumber pdf object.
|
||||
|
||||
Args:
|
||||
decode (`bool`, defaults to `True`):
|
||||
Whether to decode the pdf data. If `False`,
|
||||
returns the underlying dictionary in the format `{"path": pdf_path, "bytes": pdf_bytes}`.
|
||||
|
||||
Examples:
|
||||
|
||||
```py
|
||||
>>> from datasets import Dataset, Pdf
|
||||
>>> ds = Dataset.from_dict({"pdf": ["path/to/pdf/file.pdf"]}).cast_column("pdf", Pdf())
|
||||
>>> ds.features["pdf"]
|
||||
Pdf(decode=True, id=None)
|
||||
>>> ds[0]["pdf"]
|
||||
<pdfplumber.pdf.PDF object at 0x7f8a1c2d8f40>
|
||||
>>> ds = ds.cast_column("pdf", Pdf(decode=False))
|
||||
>>> ds[0]["pdf"]
|
||||
{'bytes': None,
|
||||
'path': 'path/to/pdf/file.pdf'}
|
||||
```
|
||||
"""
|
||||
|
||||
decode: bool = True
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "pdfplumber.pdf.PDF"
|
||||
pa_type: ClassVar[Any] = pa.struct({"bytes": pa.binary(), "path": pa.string()})
|
||||
_type: str = field(default="Pdf", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return self.pa_type
|
||||
|
||||
def encode_example(self, value: Union[str, bytes, bytearray, dict, "pdfplumber.pdf.PDF"]) -> dict:
|
||||
"""Encode example into a format for Arrow.
|
||||
|
||||
Args:
|
||||
value (`str`, `bytes`, `pdfplumber.pdf.PDF` or `dict`):
|
||||
Data passed as input to Pdf feature.
|
||||
|
||||
Returns:
|
||||
`dict` with "path" and "bytes" fields
|
||||
"""
|
||||
if config.PDFPLUMBER_AVAILABLE:
|
||||
import pdfplumber
|
||||
else:
|
||||
pdfplumber = None
|
||||
|
||||
if isinstance(value, str):
|
||||
return {"path": value, "bytes": None}
|
||||
elif isinstance(value, Path):
|
||||
return {"path": str(value.absolute()), "bytes": None}
|
||||
elif isinstance(value, (bytes, bytearray)):
|
||||
return {"path": None, "bytes": value}
|
||||
elif pdfplumber is not None and isinstance(value, pdfplumber.pdf.PDF):
|
||||
# convert the pdfplumber.pdf.PDF to bytes
|
||||
return encode_pdfplumber_pdf(value)
|
||||
elif value.get("path") is not None and os.path.isfile(value["path"]):
|
||||
# we set "bytes": None to not duplicate the data if they're already available locally
|
||||
return {"bytes": None, "path": value.get("path")}
|
||||
elif value.get("bytes") is not None or value.get("path") is not None:
|
||||
# store the pdf bytes, and path is used to infer the pdf format using the file extension
|
||||
return {"bytes": value.get("bytes"), "path": value.get("path")}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"A pdf sample should have one of 'path' or 'bytes' but they are missing or None in {value}."
|
||||
)
|
||||
|
||||
def decode_example(self, value: dict, token_per_repo_id=None) -> "pdfplumber.pdf.PDF":
|
||||
"""Decode example pdf file into pdf data.
|
||||
|
||||
Args:
|
||||
value (`str` or `dict`):
|
||||
A string with the absolute pdf file path, a dictionary with
|
||||
keys:
|
||||
|
||||
- `path`: String with absolute or relative pdf file path.
|
||||
- `bytes`: The bytes of the pdf file.
|
||||
|
||||
token_per_repo_id (`dict`, *optional*):
|
||||
To access and decode pdf files from private repositories on
|
||||
the Hub, you can pass a dictionary
|
||||
repo_id (`str`) -> token (`bool` or `str`).
|
||||
|
||||
Returns:
|
||||
`pdfplumber.pdf.PDF`
|
||||
"""
|
||||
if not self.decode:
|
||||
raise RuntimeError("Decoding is disabled for this feature. Please use Pdf(decode=True) instead.")
|
||||
|
||||
if config.PDFPLUMBER_AVAILABLE:
|
||||
import pdfplumber
|
||||
else:
|
||||
raise ImportError("To support decoding pdfs, please install 'pdfplumber'.")
|
||||
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
path, bytes_ = value["path"], value["bytes"]
|
||||
if bytes_ is None:
|
||||
if path is None:
|
||||
raise ValueError(f"A pdf should have one of 'path' or 'bytes' but both are None in {value}.")
|
||||
else:
|
||||
if is_local_path(path):
|
||||
pdf = pdfplumber.open(path)
|
||||
else:
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL
|
||||
if source_url.startswith(config.HF_ENDPOINT)
|
||||
else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
try:
|
||||
repo_id = string_to_dict(source_url, pattern)["repo_id"]
|
||||
token = token_per_repo_id.get(repo_id)
|
||||
except ValueError:
|
||||
token = None
|
||||
download_config = DownloadConfig(token=token)
|
||||
f = xopen(path, "rb", download_config=download_config)
|
||||
return pdfplumber.open(f)
|
||||
else:
|
||||
with pdfplumber.open(BytesIO(bytes_)) as p:
|
||||
pdf = p
|
||||
|
||||
return pdf
|
||||
|
||||
def flatten(self) -> Union["FeatureType", Dict[str, "FeatureType"]]:
|
||||
"""If in the decodable state, return the feature itself, otherwise flatten the feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
return (
|
||||
self
|
||||
if self.decode
|
||||
else {
|
||||
"bytes": Value("binary"),
|
||||
"path": Value("string"),
|
||||
}
|
||||
)
|
||||
|
||||
def cast_storage(self, storage: Union[pa.StringArray, pa.StructArray, pa.ListArray]) -> pa.StructArray:
|
||||
"""Cast an Arrow array to the Pdf arrow storage type.
|
||||
The Arrow types that can be converted to the Pdf pyarrow storage type are:
|
||||
|
||||
- `pa.string()` - it must contain the "path" data
|
||||
- `pa.binary()` - it must contain the image bytes
|
||||
- `pa.struct({"bytes": pa.binary()})`
|
||||
- `pa.struct({"path": pa.string()})`
|
||||
- `pa.struct({"bytes": pa.binary(), "path": pa.string()})` - order doesn't matter
|
||||
- `pa.list(*)` - it must contain the pdf array data
|
||||
|
||||
Args:
|
||||
storage (`Union[pa.StringArray, pa.StructArray, pa.ListArray]`):
|
||||
PyArrow array to cast.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Pdf arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if pa.types.is_string(storage.type):
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, storage], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_binary(storage.type):
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_struct(storage.type):
|
||||
if storage.type.get_field_index("bytes") >= 0:
|
||||
bytes_array = storage.field("bytes")
|
||||
else:
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
if storage.type.get_field_index("path") >= 0:
|
||||
path_array = storage.field("path")
|
||||
else:
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
def embed_storage(
|
||||
self, storage: pa.StructArray, token_per_repo_id=None, local_files: bool = True, remote_files: bool = True
|
||||
) -> pa.StructArray:
|
||||
"""Embed PDF files into the Arrow array.
|
||||
|
||||
Args:
|
||||
storage (`pa.StructArray`):
|
||||
PyArrow array to embed.
|
||||
token_per_repo_id (`dict`, optional):
|
||||
Dictionary repo_id -> token to fetch the files bytes.
|
||||
local_files (`bool`, defaults to `True`)
|
||||
Whether to embed local files data in the array
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
remote_files (`bool`, defaults to `True`)
|
||||
Whether to embed remote files data in the array.
|
||||
E.g. files with paths that start with hf:// or https://
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the PDF arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
@no_op_if_value_is_null
|
||||
def path_to_bytes(path):
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
return f.read()
|
||||
|
||||
bytes_array = pa.array(
|
||||
[
|
||||
(
|
||||
path_to_bytes(x["path"])
|
||||
if x["bytes"] is None
|
||||
and ((local_files and is_local_path(x["path"])) or (remote_files and is_remote_url(x["path"])))
|
||||
else x["bytes"]
|
||||
)
|
||||
if x is not None
|
||||
else None
|
||||
for x in storage.to_pylist()
|
||||
],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array(
|
||||
[
|
||||
(
|
||||
os.path.basename(path)
|
||||
if (local_files and is_local_path(path)) or (remote_files and is_remote_url(path))
|
||||
else path
|
||||
)
|
||||
if path is not None
|
||||
else None
|
||||
for path in storage.field("path").to_pylist()
|
||||
],
|
||||
type=pa.string(),
|
||||
)
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
|
||||
def encode_pdfplumber_pdf(pdf: "pdfplumber.pdf.PDF") -> dict:
|
||||
"""
|
||||
Encode a pdfplumber.pdf.PDF object into a dictionary.
|
||||
|
||||
If the PDF has an associated file path, returns the path. Otherwise, serializes
|
||||
the PDF content into bytes.
|
||||
|
||||
Args:
|
||||
pdf (pdfplumber.pdf.PDF): A pdfplumber PDF object.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with "path" or "bytes" field.
|
||||
"""
|
||||
if hasattr(pdf, "stream") and hasattr(pdf.stream, "name") and pdf.stream.name:
|
||||
# Return the path if the PDF has an associated file path
|
||||
return {"path": pdf.stream.name, "bytes": None}
|
||||
else:
|
||||
# Convert the PDF to bytes if no path is available
|
||||
return {"path": None, "bytes": pdf_to_bytes(pdf)}
|
||||
@@ -0,0 +1,129 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .features import FeatureType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Translation:
|
||||
"""`Feature` for translations with fixed languages per example.
|
||||
Here for compatibility with tfds.
|
||||
|
||||
Args:
|
||||
languages (`dict`):
|
||||
A dictionary for each example mapping string language codes to string translations.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
>>> # At construction time:
|
||||
>>> datasets.features.Translation(languages=['en', 'fr', 'de'])
|
||||
>>> # During data generation:
|
||||
>>> yield {
|
||||
... 'en': 'the cat',
|
||||
... 'fr': 'le chat',
|
||||
... 'de': 'die katze'
|
||||
... }
|
||||
```
|
||||
"""
|
||||
|
||||
languages: list[str]
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "dict"
|
||||
pa_type: ClassVar[Any] = None
|
||||
_type: str = field(default="Translation", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return pa.struct({lang: pa.string() for lang in sorted(self.languages)})
|
||||
|
||||
def flatten(self) -> Union["FeatureType", dict[str, "FeatureType"]]:
|
||||
"""Flatten the Translation feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
return {k: Value("string") for k in sorted(self.languages)}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationVariableLanguages:
|
||||
"""`Feature` for translations with variable languages per example.
|
||||
Here for compatibility with tfds.
|
||||
|
||||
Args:
|
||||
languages (`dict`):
|
||||
A dictionary for each example mapping string language codes to one or more string translations.
|
||||
The languages present may vary from example to example.
|
||||
|
||||
Returns:
|
||||
- `language` or `translation` (variable-length 1D `tf.Tensor` of `tf.string`):
|
||||
Language codes sorted in ascending order or plain text translations, sorted to align with language codes.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
>>> # At construction time:
|
||||
>>> datasets.features.TranslationVariableLanguages(languages=['en', 'fr', 'de'])
|
||||
>>> # During data generation:
|
||||
>>> yield {
|
||||
... 'en': 'the cat',
|
||||
... 'fr': ['le chat', 'la chatte,']
|
||||
... 'de': 'die katze'
|
||||
... }
|
||||
>>> # Tensor returned :
|
||||
>>> {
|
||||
... 'language': ['en', 'de', 'fr', 'fr'],
|
||||
... 'translation': ['the cat', 'die katze', 'la chatte', 'le chat'],
|
||||
... }
|
||||
```
|
||||
"""
|
||||
|
||||
languages: Optional[list] = None
|
||||
num_languages: Optional[int] = None
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "dict"
|
||||
pa_type: ClassVar[Any] = None
|
||||
_type: str = field(default="TranslationVariableLanguages", init=False, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.languages = sorted(set(self.languages)) if self.languages else None
|
||||
self.num_languages = len(self.languages) if self.languages else None
|
||||
|
||||
def __call__(self):
|
||||
return pa.struct({"language": pa.list_(pa.string()), "translation": pa.list_(pa.string())})
|
||||
|
||||
def encode_example(self, translation_dict):
|
||||
lang_set = set(self.languages)
|
||||
if set(translation_dict) == {"language", "translation"}:
|
||||
return translation_dict
|
||||
elif self.languages and set(translation_dict) - lang_set:
|
||||
raise ValueError(
|
||||
f"Some languages in example ({', '.join(sorted(set(translation_dict) - lang_set))}) are not in valid set ({', '.join(lang_set)})."
|
||||
)
|
||||
|
||||
# Convert dictionary into tuples, splitting out cases where there are
|
||||
# multiple translations for a single language.
|
||||
translation_tuples = []
|
||||
for lang, text in translation_dict.items():
|
||||
if isinstance(text, str):
|
||||
translation_tuples.append((lang, text))
|
||||
else:
|
||||
translation_tuples.extend([(lang, el) for el in text])
|
||||
|
||||
# Ensure translations are in ascending order by language code.
|
||||
languages, translations = zip(*sorted(translation_tuples))
|
||||
|
||||
return {"language": languages, "translation": translations}
|
||||
|
||||
def flatten(self) -> Union["FeatureType", dict[str, "FeatureType"]]:
|
||||
"""Flatten the TranslationVariableLanguages feature into a dictionary."""
|
||||
from .features import List, Value
|
||||
|
||||
return {
|
||||
"language": List(Value("string")),
|
||||
"translation": List(Value("string")),
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, TypedDict, Union
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..download.download_config import DownloadConfig
|
||||
from ..table import array_cast
|
||||
from ..utils.file_utils import is_local_path, is_remote_url, xopen
|
||||
from ..utils.py_utils import no_op_if_value_is_null, string_to_dict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch
|
||||
from torchcodec.decoders import VideoDecoder
|
||||
|
||||
from .features import FeatureType
|
||||
|
||||
|
||||
class Example(TypedDict):
|
||||
path: Optional[str]
|
||||
bytes: Optional[bytes]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Video:
|
||||
"""
|
||||
Video [`Feature`] to read video data from a video file.
|
||||
|
||||
Input: The Video feature accepts as input:
|
||||
- A `str`: Absolute path to the video file (i.e. random access is allowed).
|
||||
- A `pathlib.Path`: path to the video file (i.e. random access is allowed).
|
||||
- A `dict` with the keys:
|
||||
|
||||
- `path`: String with relative path of the video file in a dataset repository.
|
||||
- `bytes`: Bytes of the video file.
|
||||
|
||||
This is useful for parquet or webdataset files which embed video files.
|
||||
|
||||
- A `torchcodec.decoders.VideoDecoder`: torchcodec video decoder object.
|
||||
|
||||
Output: The Video features output data as `torchcodec.decoders.VideoDecoder` objects.
|
||||
|
||||
Args:
|
||||
decode (`bool`, defaults to `True`):
|
||||
Whether to decode the video data. If `False`,
|
||||
returns the underlying dictionary in the format `{"path": video_path, "bytes": video_bytes}`.
|
||||
stream_index (`int`, *optional*):
|
||||
The streaming index to use from the file. If `None` defaults to the "best" index.
|
||||
dimension_order (`str`, defaults to `NCHW`):
|
||||
The dimension order of the decoded frames.
|
||||
where N is the batch size, C is the number of channels,
|
||||
H is the height, and W is the width of the frames.
|
||||
num_ffmpeg_threads (`int`, defaults to `1`):
|
||||
The number of threads to use for decoding the video. (Recommended to keep this at 1)
|
||||
device (`str` or `torch.device`, defaults to `cpu`):
|
||||
The device to use for decoding the video.
|
||||
seek_mode (`str`, defaults to `exact`):
|
||||
Determines if frame access will be “exact” or “approximate”.
|
||||
Exact guarantees that requesting frame i will always return frame i, but doing so requires an initial scan of the file.
|
||||
Approximate is faster as it avoids scanning the file, but less accurate as it uses the file's metadata to calculate where i probably is.
|
||||
read more [here](https://docs.pytorch.org/torchcodec/stable/generated_examples/approximate_mode.html#sphx-glr-generated-examples-approximate-mode-py)
|
||||
|
||||
Examples:
|
||||
|
||||
```py
|
||||
>>> from datasets import Dataset, Video
|
||||
>>> ds = Dataset.from_dict({"video":["path/to/Screen Recording.mov"]}).cast_column("video", Video())
|
||||
>>> ds.features["video"]
|
||||
Video(decode=True, id=None)
|
||||
>>> ds[0]["video"]
|
||||
<torchcodec.decoders._video_decoder.VideoDecoder object at 0x14a61e080>
|
||||
>>> video = ds[0]["video"]
|
||||
>>> video.get_frames_in_range(0, 10)
|
||||
FrameBatch:
|
||||
data (shape): torch.Size([10, 3, 50, 66])
|
||||
pts_seconds: tensor([0.4333, 0.4333, 0.4333, 0.4333, 0.4333, 0.4333, 0.4333, 0.4333, 0.4333,
|
||||
0.4333], dtype=torch.float64)
|
||||
duration_seconds: tensor([0.0167, 0.0167, 0.0167, 0.0167, 0.0167, 0.0167, 0.0167, 0.0167, 0.0167,
|
||||
0.0167], dtype=torch.float64)
|
||||
>>> ds.cast_column('video', Video(decode=False))[0]["video]
|
||||
{'bytes': None,
|
||||
'path': 'path/to/Screen Recording.mov'}
|
||||
```
|
||||
"""
|
||||
|
||||
decode: bool = True
|
||||
stream_index: Optional[int] = None
|
||||
dimension_order: Literal["NCHW", "NHWC"] = "NCHW"
|
||||
num_ffmpeg_threads: int = 1
|
||||
device: Optional[Union[str, "torch.device"]] = "cpu"
|
||||
seek_mode: Literal["exact", "approximate"] = "exact"
|
||||
id: Optional[str] = field(default=None, repr=False)
|
||||
# Automatically constructed
|
||||
dtype: ClassVar[str] = "torchcodec.decoders.VideoDecoder"
|
||||
pa_type: ClassVar[Any] = pa.struct({"bytes": pa.binary(), "path": pa.string()})
|
||||
_type: str = field(default="Video", init=False, repr=False)
|
||||
|
||||
def __call__(self):
|
||||
return self.pa_type
|
||||
|
||||
def encode_example(self, value: Union[str, bytes, bytearray, Example, np.ndarray, "VideoDecoder"]) -> Example:
|
||||
"""Encode example into a format for Arrow.
|
||||
|
||||
Args:
|
||||
value (`str`, `np.ndarray`, `bytes`, `bytearray`, `VideoDecoder` or `dict`):
|
||||
Data passed as input to Video feature.
|
||||
|
||||
Returns:
|
||||
`dict` with "path" and "bytes" fields
|
||||
"""
|
||||
if value is None:
|
||||
raise ValueError("value must be provided")
|
||||
|
||||
if config.TORCHCODEC_AVAILABLE:
|
||||
from torchcodec.decoders import VideoDecoder
|
||||
else:
|
||||
VideoDecoder = None
|
||||
|
||||
if isinstance(value, list):
|
||||
value = np.array(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return {"path": value, "bytes": None}
|
||||
elif isinstance(value, Path):
|
||||
return {"path": str(value.absolute()), "bytes": None}
|
||||
elif isinstance(value, (bytes, bytearray)):
|
||||
return {"path": None, "bytes": value}
|
||||
elif isinstance(value, np.ndarray):
|
||||
# convert the video array to bytes
|
||||
return encode_np_array(value)
|
||||
elif VideoDecoder is not None and isinstance(value, VideoDecoder):
|
||||
# convert the torchcodec video decoder to bytes
|
||||
return encode_torchcodec_video(value)
|
||||
elif isinstance(value, dict):
|
||||
path, bytes_ = value.get("path"), value.get("bytes")
|
||||
if path is not None and os.path.isfile(path):
|
||||
# we set "bytes": None to not duplicate the data if they're already available locally
|
||||
return {"bytes": None, "path": path}
|
||||
elif bytes_ is not None or path is not None:
|
||||
# store the video bytes, and path is used to infer the video format using the file extension
|
||||
return {"bytes": bytes_, "path": path}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"A video sample should have one of 'path' or 'bytes' but they are missing or None in {value}."
|
||||
)
|
||||
else:
|
||||
raise TypeError(f"Unsupported encode_example type: {type(value)}")
|
||||
|
||||
def decode_example(
|
||||
self,
|
||||
value: Union[str, Example],
|
||||
token_per_repo_id: Optional[dict[str, Union[bool, str]]] = None,
|
||||
) -> "VideoDecoder":
|
||||
"""Decode example video file into video data.
|
||||
|
||||
Args:
|
||||
value (`str` or `dict`):
|
||||
A string with the absolute video file path, a dictionary with
|
||||
keys:
|
||||
|
||||
- `path`: String with absolute or relative video file path.
|
||||
- `bytes`: The bytes of the video file.
|
||||
token_per_repo_id (`dict`, *optional*):
|
||||
To access and decode
|
||||
video files from private repositories on the Hub, you can pass
|
||||
a dictionary repo_id (`str`) -> token (`bool` or `str`).
|
||||
|
||||
Returns:
|
||||
`torchcodec.decoders.VideoDecoder`
|
||||
"""
|
||||
if not self.decode:
|
||||
raise RuntimeError("Decoding is disabled for this feature. Please use Video(decode=True) instead.")
|
||||
|
||||
if config.TORCHCODEC_AVAILABLE:
|
||||
from torchcodec.decoders import VideoDecoder
|
||||
|
||||
else:
|
||||
raise ImportError("To support decoding videos, please install 'torchcodec'.")
|
||||
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
if isinstance(value, str):
|
||||
path, bytes_ = value, None
|
||||
else:
|
||||
path, bytes_ = value["path"], value["bytes"]
|
||||
|
||||
if bytes_ is None:
|
||||
if path is None:
|
||||
raise ValueError(f"A video should have one of 'path' or 'bytes' but both are None in {value}.")
|
||||
elif is_local_path(path):
|
||||
video = VideoDecoder(
|
||||
path,
|
||||
stream_index=self.stream_index,
|
||||
dimension_order=self.dimension_order,
|
||||
num_ffmpeg_threads=self.num_ffmpeg_threads,
|
||||
device=self.device,
|
||||
seek_mode=self.seek_mode,
|
||||
)
|
||||
else:
|
||||
video = hf_video_reader(
|
||||
path,
|
||||
token_per_repo_id=token_per_repo_id,
|
||||
dimension_order=self.dimension_order,
|
||||
num_ffmpeg_threads=self.num_ffmpeg_threads,
|
||||
device=self.device,
|
||||
seek_mode=self.seek_mode,
|
||||
)
|
||||
else:
|
||||
video = VideoDecoder(
|
||||
bytes_,
|
||||
stream_index=self.stream_index,
|
||||
dimension_order=self.dimension_order,
|
||||
num_ffmpeg_threads=self.num_ffmpeg_threads,
|
||||
device=self.device,
|
||||
seek_mode=self.seek_mode,
|
||||
)
|
||||
video._hf_encoded = {"path": path, "bytes": bytes_}
|
||||
video.metadata.path = path
|
||||
return video
|
||||
|
||||
def flatten(self) -> Union["FeatureType", dict[str, "FeatureType"]]:
|
||||
"""If in the decodable state, return the feature itself, otherwise flatten the feature into a dictionary."""
|
||||
from .features import Value
|
||||
|
||||
return (
|
||||
self
|
||||
if self.decode
|
||||
else {
|
||||
"bytes": Value("binary"),
|
||||
"path": Value("string"),
|
||||
}
|
||||
)
|
||||
|
||||
def cast_storage(self, storage: Union[pa.StringArray, pa.StructArray, pa.ListArray]) -> pa.StructArray:
|
||||
"""Cast an Arrow array to the Video arrow storage type.
|
||||
The Arrow types that can be converted to the Video pyarrow storage type are:
|
||||
|
||||
- `pa.string()` - it must contain the "path" data
|
||||
- `pa.binary()` - it must contain the video bytes
|
||||
- `pa.struct({"bytes": pa.binary()})`
|
||||
- `pa.struct({"path": pa.string()})`
|
||||
- `pa.struct({"bytes": pa.binary(), "path": pa.string()})` - order doesn't matter
|
||||
- `pa.list(*)` - it must contain the video array data
|
||||
|
||||
Args:
|
||||
storage (`Union[pa.StringArray, pa.StructArray, pa.ListArray]`):
|
||||
PyArrow array to cast.
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Video arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if pa.types.is_string(storage.type):
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, storage], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_large_binary(storage.type):
|
||||
storage = array_cast(
|
||||
storage, pa.binary()
|
||||
) # this can fail in case of big videos, paths should be used instead
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_binary(storage.type):
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([storage, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_struct(storage.type):
|
||||
if storage.type.get_field_index("bytes") >= 0:
|
||||
bytes_array = storage.field("bytes")
|
||||
else:
|
||||
bytes_array = pa.array([None] * len(storage), type=pa.binary())
|
||||
if storage.type.get_field_index("path") >= 0:
|
||||
path_array = storage.field("path")
|
||||
else:
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
elif pa.types.is_list(storage.type):
|
||||
bytes_array = pa.array(
|
||||
[encode_np_array(np.array(arr))["bytes"] if arr is not None else None for arr in storage.to_pylist()],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array([None] * len(storage), type=pa.string())
|
||||
storage = pa.StructArray.from_arrays(
|
||||
[bytes_array, path_array], ["bytes", "path"], mask=bytes_array.is_null()
|
||||
)
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
def embed_storage(
|
||||
self, storage: pa.StructArray, token_per_repo_id=None, local_files: bool = True, remote_files: bool = True
|
||||
) -> pa.StructArray:
|
||||
"""Embed image files into the Arrow array.
|
||||
|
||||
Args:
|
||||
storage (`pa.StructArray`):
|
||||
PyArrow array to embed.
|
||||
token_per_repo_id (`dict`, optional):
|
||||
Dictionary repo_id -> token to fetch the files bytes.
|
||||
local_files (`bool`, defaults to `True`)
|
||||
Whether to embed local files data in the array
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
remote_files (`bool`, defaults to `True`)
|
||||
Whether to embed remote files data in the array.
|
||||
E.g. files with paths that start with hf:// or https://
|
||||
|
||||
<Added version="4.8.5"/>
|
||||
|
||||
Returns:
|
||||
`pa.StructArray`: Array in the Video arrow storage type, that is
|
||||
`pa.struct({"bytes": pa.binary(), "path": pa.string()})`.
|
||||
"""
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
|
||||
@no_op_if_value_is_null
|
||||
def path_to_bytes(path):
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = (
|
||||
config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
)
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
with xopen(path, "rb", download_config=download_config) as f:
|
||||
return f.read()
|
||||
|
||||
bytes_array = pa.array(
|
||||
[
|
||||
(
|
||||
path_to_bytes(x["path"])
|
||||
if x["bytes"] is None
|
||||
and ((local_files and is_local_path(x["path"])) or (remote_files and is_remote_url(x["path"])))
|
||||
else x["bytes"]
|
||||
)
|
||||
if x is not None
|
||||
else None
|
||||
for x in storage.to_pylist()
|
||||
],
|
||||
type=pa.binary(),
|
||||
)
|
||||
path_array = pa.array(
|
||||
[
|
||||
(
|
||||
os.path.basename(path)
|
||||
if (local_files and is_local_path(path)) or (remote_files and is_remote_url(path))
|
||||
else path
|
||||
)
|
||||
if path is not None
|
||||
else None
|
||||
for path in storage.field("path").to_pylist()
|
||||
],
|
||||
type=pa.string(),
|
||||
)
|
||||
storage = pa.StructArray.from_arrays([bytes_array, path_array], ["bytes", "path"], mask=storage.is_null())
|
||||
return array_cast(storage, self.pa_type)
|
||||
|
||||
|
||||
def video_to_bytes(video: "VideoDecoder") -> bytes:
|
||||
"""Convert a torchcodec Video object to bytes using native compression if possible"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def encode_torchcodec_video(video: "VideoDecoder") -> Example:
|
||||
if hasattr(video, "_hf_encoded"):
|
||||
return video._hf_encoded
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Encoding a VideoDecoder that doesn't come from datasets.Video.decode() is not implemented"
|
||||
)
|
||||
|
||||
|
||||
def encode_np_array(array: np.ndarray) -> Example:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# No monkey patch needed!
|
||||
# 1. store the encoded video data {"path": ..., "bytes": ...} in `video._hf_encoded``
|
||||
# 2. add support for hf:// files
|
||||
|
||||
|
||||
def hf_video_reader(
|
||||
path: str,
|
||||
token_per_repo_id: Optional[dict[str, Union[bool, str]]] = None,
|
||||
stream: str = "video",
|
||||
dimension_order: Literal["NCHW", "NHWC"] = "NCHW",
|
||||
num_ffmpeg_threads: int = 1,
|
||||
device: Optional[Union[str, "torch.device"]] = "cpu",
|
||||
seek_mode: Literal["exact", "approximate"] = "exact",
|
||||
) -> "VideoDecoder":
|
||||
from torchcodec.decoders import VideoDecoder
|
||||
|
||||
# Load the file from HF
|
||||
if token_per_repo_id is None:
|
||||
token_per_repo_id = {}
|
||||
source_url = path.split("::")[-1]
|
||||
pattern = config.HUB_DATASETS_URL if source_url.startswith(config.HF_ENDPOINT) else config.HUB_DATASETS_HFFS_URL
|
||||
source_url_fields = string_to_dict(source_url, pattern)
|
||||
token = token_per_repo_id.get(source_url_fields["repo_id"]) if source_url_fields is not None else None
|
||||
download_config = DownloadConfig(token=token)
|
||||
f = xopen(path, "rb", download_config=download_config)
|
||||
|
||||
# Instantiate the VideoDecoder
|
||||
stream_id = 0 if len(stream.split(":")) == 1 else int(stream.split(":")[1])
|
||||
vd = VideoDecoder(
|
||||
f,
|
||||
stream_index=stream_id,
|
||||
dimension_order=dimension_order,
|
||||
num_ffmpeg_threads=num_ffmpeg_threads,
|
||||
device=device,
|
||||
seek_mode=seek_mode,
|
||||
)
|
||||
return vd
|
||||
@@ -0,0 +1,47 @@
|
||||
import importlib
|
||||
import shutil
|
||||
import warnings
|
||||
from typing import List
|
||||
|
||||
import fsspec
|
||||
import fsspec.asyn
|
||||
from fsspec.implementations.local import LocalFileSystem
|
||||
|
||||
from . import compression
|
||||
|
||||
|
||||
COMPRESSION_FILESYSTEMS: list[compression.BaseCompressedFileFileSystem] = [
|
||||
compression.Bz2FileSystem,
|
||||
compression.GzipFileSystem,
|
||||
compression.Lz4FileSystem,
|
||||
compression.XzFileSystem,
|
||||
compression.ZstdFileSystem,
|
||||
]
|
||||
|
||||
# Register custom filesystems
|
||||
for fs_class in COMPRESSION_FILESYSTEMS:
|
||||
if fs_class.protocol in fsspec.registry and fsspec.registry[fs_class.protocol] is not fs_class:
|
||||
warnings.warn(f"A filesystem protocol was already set for {fs_class.protocol} and will be overwritten.")
|
||||
fsspec.register_implementation(fs_class.protocol, fs_class, clobber=True)
|
||||
|
||||
|
||||
def is_remote_filesystem(fs: fsspec.AbstractFileSystem) -> bool:
|
||||
"""
|
||||
Checks if `fs` is a remote filesystem.
|
||||
|
||||
Args:
|
||||
fs (`fsspec.spec.AbstractFileSystem`):
|
||||
An abstract super-class for pythonic file-systems, e.g. `fsspec.filesystem(\'file\')` or `s3fs.S3FileSystem`.
|
||||
"""
|
||||
return not isinstance(fs, LocalFileSystem)
|
||||
|
||||
|
||||
def rename(fs: fsspec.AbstractFileSystem, src: str, dst: str):
|
||||
"""
|
||||
Renames the file `src` in `fs` to `dst`.
|
||||
"""
|
||||
if not is_remote_filesystem(fs):
|
||||
# LocalFileSystem.mv does copy + rm, it is more efficient to simply move a local directory
|
||||
shutil.move(fs._strip_protocol(src), fs._strip_protocol(dst))
|
||||
else:
|
||||
fs.mv(src, dst, recursive=True)
|
||||
@@ -0,0 +1,127 @@
|
||||
import os
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
import fsspec
|
||||
from fsspec.archive import AbstractArchiveFileSystem
|
||||
|
||||
|
||||
class BaseCompressedFileFileSystem(AbstractArchiveFileSystem):
|
||||
"""Read contents of compressed file as a filesystem with one file inside."""
|
||||
|
||||
root_marker = ""
|
||||
protocol: str = (
|
||||
None # protocol passed in prefix to the url. ex: "gzip", for gzip://file.txt::http://foo.bar/file.txt.gz
|
||||
)
|
||||
compression: str = None # compression type in fsspec. ex: "gzip"
|
||||
extensions: list[str] = None # extensions of the filename to strip. ex: ".gz" to get file.txt from file.txt.gz
|
||||
|
||||
def __init__(
|
||||
self, fo: str = "", target_protocol: Optional[str] = None, target_options: Optional[dict] = None, **kwargs
|
||||
):
|
||||
"""
|
||||
The compressed file system can be instantiated from any compressed file.
|
||||
It reads the contents of compressed file as a filesystem with one file inside, as if it was an archive.
|
||||
|
||||
The single file inside the filesystem is named after the compresssed file,
|
||||
without the compression extension at the end of the filename.
|
||||
|
||||
Args:
|
||||
fo (:obj:``str``): Path to compressed file. Will fetch file using ``fsspec.open()``
|
||||
mode (:obj:``str``): Currently, only 'rb' accepted
|
||||
target_protocol(:obj:``str``, optional): To override the FS protocol inferred from a URL.
|
||||
target_options (:obj:``dict``, optional): Kwargs passed when instantiating the target FS.
|
||||
"""
|
||||
super().__init__(self, **kwargs)
|
||||
self.fo = fo.__fspath__() if hasattr(fo, "__fspath__") else fo
|
||||
# always open as "rb" since fsspec can then use the TextIOWrapper to make it work for "r" mode
|
||||
self._open_with_fsspec = partial(
|
||||
fsspec.open,
|
||||
self.fo,
|
||||
mode="rb",
|
||||
protocol=target_protocol,
|
||||
compression=self.compression,
|
||||
client_kwargs={
|
||||
"requote_redirect_url": False, # see https://github.com/huggingface/datasets/pull/5459
|
||||
"trust_env": True, # Enable reading proxy env variables.
|
||||
**(target_options or {}).pop("client_kwargs", {}), # To avoid issues if it was already passed.
|
||||
},
|
||||
**(target_options or {}),
|
||||
)
|
||||
self.compressed_name = os.path.basename(self.fo.split("::")[0])
|
||||
self.uncompressed_name = (
|
||||
self.compressed_name[: self.compressed_name.rindex(".")]
|
||||
if "." in self.compressed_name
|
||||
else self.compressed_name
|
||||
)
|
||||
self.dir_cache = None
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path):
|
||||
# compressed file paths are always relative to the archive root
|
||||
return super()._strip_protocol(path).lstrip("/")
|
||||
|
||||
def _get_dirs(self):
|
||||
if self.dir_cache is None:
|
||||
f = {**self._open_with_fsspec().fs.info(self.fo), "name": self.uncompressed_name}
|
||||
self.dir_cache = {f["name"]: f}
|
||||
|
||||
def cat(self, path: str):
|
||||
with self._open_with_fsspec().open() as f:
|
||||
return f.read()
|
||||
|
||||
def _open(
|
||||
self,
|
||||
path: str,
|
||||
mode: str = "rb",
|
||||
block_size=None,
|
||||
autocommit=True,
|
||||
cache_options=None,
|
||||
**kwargs,
|
||||
):
|
||||
path = self._strip_protocol(path)
|
||||
if mode != "rb":
|
||||
raise ValueError(f"Tried to read with mode {mode} on file {self.fo} opened with mode 'rb'")
|
||||
return self._open_with_fsspec().open()
|
||||
|
||||
|
||||
class Bz2FileSystem(BaseCompressedFileFileSystem):
|
||||
"""Read contents of BZ2 file as a filesystem with one file inside."""
|
||||
|
||||
protocol = "bz2"
|
||||
compression = "bz2"
|
||||
extensions = [".bz2"]
|
||||
|
||||
|
||||
class GzipFileSystem(BaseCompressedFileFileSystem):
|
||||
"""Read contents of GZIP file as a filesystem with one file inside."""
|
||||
|
||||
protocol = "gzip"
|
||||
compression = "gzip"
|
||||
extensions = [".gz", ".gzip"]
|
||||
|
||||
|
||||
class Lz4FileSystem(BaseCompressedFileFileSystem):
|
||||
"""Read contents of LZ4 file as a filesystem with one file inside."""
|
||||
|
||||
protocol = "lz4"
|
||||
compression = "lz4"
|
||||
extensions = [".lz4"]
|
||||
|
||||
|
||||
class XzFileSystem(BaseCompressedFileFileSystem):
|
||||
"""Read contents of .xz (LZMA) file as a filesystem with one file inside."""
|
||||
|
||||
protocol = "xz"
|
||||
compression = "xz"
|
||||
extensions = [".xz"]
|
||||
|
||||
|
||||
class ZstdFileSystem(BaseCompressedFileFileSystem):
|
||||
"""
|
||||
Read contents of .zstd file as a filesystem with one file inside.
|
||||
"""
|
||||
|
||||
protocol = "zstd"
|
||||
compression = "zstd"
|
||||
extensions = [".zst", ".zstd"]
|
||||
@@ -0,0 +1,480 @@
|
||||
import inspect
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import tempfile
|
||||
import weakref
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import xxhash
|
||||
|
||||
from . import config
|
||||
from .naming import INVALID_WINDOWS_CHARACTERS_IN_PATH
|
||||
from .utils._dill import dumps
|
||||
from .utils.logging import get_logger
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .arrow_dataset import Dataset
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# Fingerprinting allows to have one deterministic fingerprint per dataset state.
|
||||
# A dataset fingerprint is updated after each transform.
|
||||
# Re-running the same transforms on a dataset in a different session results in the same fingerprint.
|
||||
# This is possible thanks to a custom hashing function that works with most python objects.
|
||||
|
||||
# Fingerprinting is the main mechanism that enables caching.
|
||||
# The caching mechanism allows to reload an existing cache file if it's already been computed.
|
||||
|
||||
|
||||
#################
|
||||
# Caching
|
||||
#################
|
||||
|
||||
_CACHING_ENABLED = True
|
||||
_TEMP_DIR_FOR_TEMP_CACHE_FILES: Optional["_TempCacheDir"] = None
|
||||
_DATASETS_WITH_TABLE_IN_TEMP_DIR: Optional[weakref.WeakSet] = None
|
||||
|
||||
|
||||
class _TempCacheDir:
|
||||
"""
|
||||
A temporary directory for storing cached Arrow files with a cleanup that frees references to the Arrow files
|
||||
before deleting the directory itself to avoid permission errors on Windows.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Check if TMPDIR is set and handle the case where it doesn't exist
|
||||
tmpdir = os.environ.get("TMPDIR") or os.environ.get("TEMP") or os.environ.get("TMP")
|
||||
# Normalize the path to handle any path resolution issues
|
||||
if tmpdir:
|
||||
tmpdir = os.path.normpath(tmpdir)
|
||||
if not os.path.exists(tmpdir):
|
||||
# Auto-create the directory if it doesn't exist
|
||||
# This prevents tempfile from silently falling back to /tmp
|
||||
try:
|
||||
os.makedirs(tmpdir, exist_ok=True)
|
||||
logger.info(f"Created TMPDIR directory: {tmpdir}")
|
||||
except OSError as e:
|
||||
raise OSError(
|
||||
f"TMPDIR is set to '{tmpdir}' but the directory does not exist and could not be created: {e}. "
|
||||
"Please create it manually or unset TMPDIR to fall back to the default temporary directory."
|
||||
) from e
|
||||
# If tmpdir exists, verify it's actually a directory and writable
|
||||
elif not os.path.isdir(tmpdir):
|
||||
raise OSError(
|
||||
f"TMPDIR is set to '{tmpdir}' but it is not a directory. "
|
||||
"Please point TMPDIR to a writable directory or unset it to fall back to the default temporary directory."
|
||||
)
|
||||
|
||||
# Explicitly pass the directory to mkdtemp to ensure TMPDIR is respected
|
||||
# This works even if tempfile.gettempdir() was already called and cached
|
||||
# Pass dir=None if tmpdir is None to use default temp directory
|
||||
self.name = tempfile.mkdtemp(prefix=config.TEMP_CACHE_DIR_PREFIX, dir=tmpdir)
|
||||
self._finalizer = weakref.finalize(self, self._cleanup)
|
||||
|
||||
def _cleanup(self):
|
||||
for dset in get_datasets_with_cache_file_in_temp_dir():
|
||||
dset.__del__()
|
||||
if os.path.exists(self.name):
|
||||
try:
|
||||
shutil.rmtree(self.name)
|
||||
except Exception as e:
|
||||
raise OSError(
|
||||
f"An error occurred while trying to delete temporary cache directory {self.name}. Please delete it manually."
|
||||
) from e
|
||||
|
||||
def cleanup(self):
|
||||
if self._finalizer.detach():
|
||||
self._cleanup()
|
||||
|
||||
|
||||
def maybe_register_dataset_for_temp_dir_deletion(dataset):
|
||||
"""
|
||||
This function registers the datasets that have cache files in _TEMP_DIR_FOR_TEMP_CACHE_FILES in order
|
||||
to properly delete them before deleting the temporary directory.
|
||||
The temporary directory _TEMP_DIR_FOR_TEMP_CACHE_FILES is used when caching is disabled.
|
||||
"""
|
||||
if _TEMP_DIR_FOR_TEMP_CACHE_FILES is None:
|
||||
return
|
||||
|
||||
global _DATASETS_WITH_TABLE_IN_TEMP_DIR
|
||||
if _DATASETS_WITH_TABLE_IN_TEMP_DIR is None:
|
||||
_DATASETS_WITH_TABLE_IN_TEMP_DIR = weakref.WeakSet()
|
||||
if any(
|
||||
Path(_TEMP_DIR_FOR_TEMP_CACHE_FILES.name) in Path(cache_file["filename"]).parents
|
||||
for cache_file in dataset.cache_files
|
||||
):
|
||||
_DATASETS_WITH_TABLE_IN_TEMP_DIR.add(dataset)
|
||||
|
||||
|
||||
def get_datasets_with_cache_file_in_temp_dir():
|
||||
return list(_DATASETS_WITH_TABLE_IN_TEMP_DIR) if _DATASETS_WITH_TABLE_IN_TEMP_DIR is not None else []
|
||||
|
||||
|
||||
def enable_caching():
|
||||
"""
|
||||
When applying transforms on a dataset, the data are stored in cache files.
|
||||
The caching mechanism allows to reload an existing cache file if it's already been computed.
|
||||
|
||||
Reloading a dataset is possible since the cache files are named using the dataset fingerprint, which is updated
|
||||
after each transform.
|
||||
|
||||
If disabled, the library will no longer reload cached datasets files when applying transforms to the datasets.
|
||||
More precisely, if the caching is disabled:
|
||||
- cache files are always recreated
|
||||
- cache files are written to a temporary directory that is deleted when session closes
|
||||
- cache files are named using a random hash instead of the dataset fingerprint
|
||||
- use [`~datasets.Dataset.save_to_disk`] to save a transformed dataset or it will be deleted when session closes
|
||||
- caching doesn't affect [`~datasets.load_dataset`]. If you want to regenerate a dataset from scratch you should use
|
||||
the `download_mode` parameter in [`~datasets.load_dataset`].
|
||||
"""
|
||||
global _CACHING_ENABLED
|
||||
_CACHING_ENABLED = True
|
||||
|
||||
|
||||
def disable_caching():
|
||||
"""
|
||||
When applying transforms on a dataset, the data are stored in cache files.
|
||||
The caching mechanism allows to reload an existing cache file if it's already been computed.
|
||||
|
||||
Reloading a dataset is possible since the cache files are named using the dataset fingerprint, which is updated
|
||||
after each transform.
|
||||
|
||||
If disabled, the library will no longer reload cached datasets files when applying transforms to the datasets.
|
||||
More precisely, if the caching is disabled:
|
||||
- cache files are always recreated
|
||||
- cache files are written to a temporary directory that is deleted when session closes
|
||||
- cache files are named using a random hash instead of the dataset fingerprint
|
||||
- use [`~datasets.Dataset.save_to_disk`] to save a transformed dataset or it will be deleted when session closes
|
||||
- caching doesn't affect [`~datasets.load_dataset`]. If you want to regenerate a dataset from scratch you should use
|
||||
the `download_mode` parameter in [`~datasets.load_dataset`].
|
||||
"""
|
||||
global _CACHING_ENABLED
|
||||
_CACHING_ENABLED = False
|
||||
|
||||
|
||||
def is_caching_enabled() -> bool:
|
||||
"""
|
||||
When applying transforms on a dataset, the data are stored in cache files.
|
||||
The caching mechanism allows to reload an existing cache file if it's already been computed.
|
||||
|
||||
Reloading a dataset is possible since the cache files are named using the dataset fingerprint, which is updated
|
||||
after each transform.
|
||||
|
||||
If disabled, the library will no longer reload cached datasets files when applying transforms to the datasets.
|
||||
More precisely, if the caching is disabled:
|
||||
- cache files are always recreated
|
||||
- cache files are written to a temporary directory that is deleted when session closes
|
||||
- cache files are named using a random hash instead of the dataset fingerprint
|
||||
- use [`~datasets.Dataset.save_to_disk`]] to save a transformed dataset or it will be deleted when session closes
|
||||
- caching doesn't affect [`~datasets.load_dataset`]. If you want to regenerate a dataset from scratch you should use
|
||||
the `download_mode` parameter in [`~datasets.load_dataset`].
|
||||
"""
|
||||
global _CACHING_ENABLED
|
||||
return bool(_CACHING_ENABLED)
|
||||
|
||||
|
||||
def get_temporary_cache_files_directory() -> str:
|
||||
"""Return a directory that is deleted when session closes."""
|
||||
global _TEMP_DIR_FOR_TEMP_CACHE_FILES
|
||||
if _TEMP_DIR_FOR_TEMP_CACHE_FILES is None:
|
||||
_TEMP_DIR_FOR_TEMP_CACHE_FILES = _TempCacheDir()
|
||||
return _TEMP_DIR_FOR_TEMP_CACHE_FILES.name
|
||||
|
||||
|
||||
#################
|
||||
# Hashing
|
||||
#################
|
||||
|
||||
|
||||
class Hasher:
|
||||
"""Hasher that accepts python objects as inputs."""
|
||||
|
||||
dispatch: dict = {}
|
||||
|
||||
def __init__(self):
|
||||
self.m = xxhash.xxh64()
|
||||
|
||||
@classmethod
|
||||
def hash_bytes(cls, value: Union[bytes, list[bytes]]) -> str:
|
||||
value = [value] if isinstance(value, bytes) else value
|
||||
m = xxhash.xxh64()
|
||||
for x in value:
|
||||
m.update(x)
|
||||
return m.hexdigest()
|
||||
|
||||
@classmethod
|
||||
def hash(cls, value: Any) -> str:
|
||||
return cls.hash_bytes(dumps(value))
|
||||
|
||||
def update(self, value: Any) -> None:
|
||||
header_for_update = f"=={type(value)}=="
|
||||
value_for_update = self.hash(value)
|
||||
self.m.update(header_for_update.encode("utf8"))
|
||||
self.m.update(value_for_update.encode("utf-8"))
|
||||
|
||||
def hexdigest(self) -> str:
|
||||
return self.m.hexdigest()
|
||||
|
||||
|
||||
#################
|
||||
# Fingerprinting
|
||||
#################
|
||||
|
||||
fingerprint_rng = random.Random()
|
||||
# we show a warning only once when fingerprinting fails to avoid spam
|
||||
fingerprint_warnings: dict[str, bool] = {}
|
||||
|
||||
|
||||
def generate_fingerprint(dataset: "Dataset") -> str:
|
||||
state = dataset.__dict__
|
||||
hasher = Hasher()
|
||||
for key in sorted(state):
|
||||
if key == "_fingerprint":
|
||||
continue
|
||||
hasher.update(key)
|
||||
hasher.update(state[key])
|
||||
# hash data files last modification timestamps as well
|
||||
for cache_file in dataset.cache_files:
|
||||
hasher.update(os.path.getmtime(cache_file["filename"]))
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def generate_random_fingerprint(nbits: int = 64) -> str:
|
||||
return f"{fingerprint_rng.getrandbits(nbits):0{nbits // 4}x}"
|
||||
|
||||
|
||||
def update_fingerprint(fingerprint, transform, transform_args):
|
||||
global fingerprint_warnings
|
||||
hasher = Hasher()
|
||||
hasher.update(fingerprint)
|
||||
try:
|
||||
hasher.update(transform)
|
||||
except: # noqa various errors might raise here from pickle or dill
|
||||
if _CACHING_ENABLED:
|
||||
if not fingerprint_warnings.get("update_fingerprint_transform_hash_failed", False):
|
||||
logger.warning(
|
||||
f"Transform {transform} couldn't be hashed properly, a random hash was used instead. "
|
||||
"Make sure your transforms and parameters are serializable with pickle or dill for the dataset fingerprinting and caching to work. "
|
||||
"If you reuse this transform, the caching mechanism will consider it to be different from the previous calls and recompute everything. "
|
||||
"This warning is only shown once. Subsequent hashing failures won't be shown."
|
||||
)
|
||||
fingerprint_warnings["update_fingerprint_transform_hash_failed"] = True
|
||||
else:
|
||||
logger.info(f"Transform {transform} couldn't be hashed properly, a random hash was used instead.")
|
||||
else:
|
||||
logger.info(
|
||||
f"Transform {transform} couldn't be hashed properly, a random hash was used instead. This doesn't affect caching since it's disabled."
|
||||
)
|
||||
|
||||
return generate_random_fingerprint()
|
||||
for key in sorted(transform_args):
|
||||
hasher.update(key)
|
||||
try:
|
||||
hasher.update(transform_args[key])
|
||||
except: # noqa various errors might raise here from pickle or dill
|
||||
if _CACHING_ENABLED:
|
||||
if not fingerprint_warnings.get("update_fingerprint_transform_hash_failed", False):
|
||||
logger.warning(
|
||||
f"Parameter '{key}'={transform_args[key]} of the transform {transform} couldn't be hashed properly, a random hash was used instead. "
|
||||
"Make sure your transforms and parameters are serializable with pickle or dill for the dataset fingerprinting and caching to work. "
|
||||
"If you reuse this transform, the caching mechanism will consider it to be different from the previous calls and recompute everything. "
|
||||
"This warning is only shown once. Subsequent hashing failures won't be shown."
|
||||
)
|
||||
fingerprint_warnings["update_fingerprint_transform_hash_failed"] = True
|
||||
else:
|
||||
logger.info(
|
||||
f"Parameter '{key}'={transform_args[key]} of the transform {transform} couldn't be hashed properly, a random hash was used instead."
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Parameter '{key}'={transform_args[key]} of the transform {transform} couldn't be hashed properly, a random hash was used instead. This doesn't affect caching since it's disabled."
|
||||
)
|
||||
return generate_random_fingerprint()
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def validate_fingerprint(fingerprint: str, max_length=64):
|
||||
"""
|
||||
Make sure the fingerprint is a non-empty string that is not longer that max_length=64 by default,
|
||||
so that the fingerprint can be used to name cache files without issues.
|
||||
"""
|
||||
if not isinstance(fingerprint, str) or not fingerprint:
|
||||
raise ValueError(f"Invalid fingerprint '{fingerprint}': it should be a non-empty string.")
|
||||
for invalid_char in INVALID_WINDOWS_CHARACTERS_IN_PATH:
|
||||
if invalid_char in fingerprint:
|
||||
raise ValueError(
|
||||
f"Invalid fingerprint. Bad characters from black list '{INVALID_WINDOWS_CHARACTERS_IN_PATH}' found in '{fingerprint}'. "
|
||||
f"They could create issues when creating cache files."
|
||||
)
|
||||
if len(fingerprint) > max_length:
|
||||
raise ValueError(
|
||||
f"Invalid fingerprint. Maximum lenth is {max_length} but '{fingerprint}' has length {len(fingerprint)}."
|
||||
"It could create issues when creating cache files."
|
||||
)
|
||||
|
||||
|
||||
def format_transform_for_fingerprint(func: Callable, version: Optional[str] = None) -> str:
|
||||
"""
|
||||
Format a transform to the format that will be used to update the fingerprint.
|
||||
"""
|
||||
transform = f"{func.__module__}.{func.__qualname__}"
|
||||
if version is not None:
|
||||
transform += f"@{version}"
|
||||
return transform
|
||||
|
||||
|
||||
def format_kwargs_for_fingerprint(
|
||||
func: Callable,
|
||||
args: tuple,
|
||||
kwargs: dict[str, Any],
|
||||
use_kwargs: Optional[list[str]] = None,
|
||||
ignore_kwargs: Optional[list[str]] = None,
|
||||
randomized_function: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Format the kwargs of a transform to the format that will be used to update the fingerprint.
|
||||
"""
|
||||
kwargs_for_fingerprint = kwargs.copy()
|
||||
if args:
|
||||
params = [p.name for p in inspect.signature(func).parameters.values() if p != p.VAR_KEYWORD]
|
||||
args = args[1:] # assume the first argument is the dataset
|
||||
params = params[1:]
|
||||
kwargs_for_fingerprint.update(zip(params, args))
|
||||
else:
|
||||
del kwargs_for_fingerprint[
|
||||
next(iter(inspect.signature(func).parameters))
|
||||
] # assume the first key is the dataset
|
||||
|
||||
# keep the right kwargs to be hashed to generate the fingerprint
|
||||
|
||||
if use_kwargs:
|
||||
kwargs_for_fingerprint = {k: v for k, v in kwargs_for_fingerprint.items() if k in use_kwargs}
|
||||
if ignore_kwargs:
|
||||
kwargs_for_fingerprint = {k: v for k, v in kwargs_for_fingerprint.items() if k not in ignore_kwargs}
|
||||
if randomized_function: # randomized functions have `seed` and `generator` parameters
|
||||
if kwargs_for_fingerprint.get("seed") is None and kwargs_for_fingerprint.get("generator") is None:
|
||||
_, seed, pos, *_ = np.random.get_state()
|
||||
seed = seed[pos] if pos < 624 else seed[0]
|
||||
kwargs_for_fingerprint["generator"] = np.random.default_rng(seed)
|
||||
|
||||
# remove kwargs that are the default values
|
||||
|
||||
default_values = {
|
||||
p.name: p.default for p in inspect.signature(func).parameters.values() if p.default != inspect._empty
|
||||
}
|
||||
for default_varname, default_value in default_values.items():
|
||||
if default_varname in kwargs_for_fingerprint and kwargs_for_fingerprint[default_varname] == default_value:
|
||||
kwargs_for_fingerprint.pop(default_varname)
|
||||
return kwargs_for_fingerprint
|
||||
|
||||
|
||||
def fingerprint_transform(
|
||||
inplace: bool,
|
||||
use_kwargs: Optional[list[str]] = None,
|
||||
ignore_kwargs: Optional[list[str]] = None,
|
||||
fingerprint_names: Optional[list[str]] = None,
|
||||
randomized_function: bool = False,
|
||||
version: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Wrapper for dataset transforms to update the dataset fingerprint using ``update_fingerprint``
|
||||
Args:
|
||||
inplace (:obj:`bool`): If inplace is True, the fingerprint of the dataset is updated inplace.
|
||||
Otherwise, a parameter "new_fingerprint" is passed to the wrapped method that should take care of
|
||||
setting the fingerprint of the returned Dataset.
|
||||
use_kwargs (:obj:`List[str]`, optional): optional white list of argument names to take into account
|
||||
to update the fingerprint to the wrapped method that should take care of
|
||||
setting the fingerprint of the returned Dataset. By default all the arguments are used.
|
||||
ignore_kwargs (:obj:`List[str]`, optional): optional black list of argument names to take into account
|
||||
to update the fingerprint. Note that ignore_kwargs prevails on use_kwargs.
|
||||
fingerprint_names (:obj:`List[str]`, optional, defaults to ["new_fingerprint"]):
|
||||
If the dataset transforms is not inplace and returns a DatasetDict, then it can require
|
||||
several fingerprints (one per dataset in the DatasetDict). By specifying fingerprint_names,
|
||||
one fingerprint named after each element of fingerprint_names is going to be passed.
|
||||
randomized_function (:obj:`bool`, defaults to False): If the dataset transform is random and has
|
||||
optional parameters "seed" and "generator", then you can set randomized_function to True.
|
||||
This way, even if users set "seed" and "generator" to None, then the fingerprint is
|
||||
going to be randomly generated depending on numpy's current state. In this case, the
|
||||
generator is set to np.random.default_rng(np.random.get_state()[1][0]).
|
||||
version (:obj:`str`, optional): version of the transform. The version is taken into account when
|
||||
computing the fingerprint. If a datase transform changes (or at least if the output data
|
||||
that are cached changes), then one should increase the version. If the version stays the
|
||||
same, then old cached data could be reused that are not compatible with the new transform.
|
||||
It should be in the format "MAJOR.MINOR.PATCH".
|
||||
"""
|
||||
|
||||
if use_kwargs is not None and not isinstance(use_kwargs, list):
|
||||
raise ValueError(f"use_kwargs is supposed to be a list, not {type(use_kwargs)}")
|
||||
|
||||
if ignore_kwargs is not None and not isinstance(ignore_kwargs, list):
|
||||
raise ValueError(f"ignore_kwargs is supposed to be a list, not {type(use_kwargs)}")
|
||||
|
||||
if inplace and fingerprint_names:
|
||||
raise ValueError("fingerprint_names are only used when inplace is False")
|
||||
|
||||
fingerprint_names = fingerprint_names if fingerprint_names is not None else ["new_fingerprint"]
|
||||
|
||||
def _fingerprint(func):
|
||||
if not inplace and not all(name in func.__code__.co_varnames for name in fingerprint_names):
|
||||
raise ValueError(f"function {func} is missing parameters {fingerprint_names} in signature")
|
||||
|
||||
if randomized_function: # randomized function have seed and generator parameters
|
||||
if "seed" not in func.__code__.co_varnames:
|
||||
raise ValueError(f"'seed' must be in {func}'s signature")
|
||||
if "generator" not in func.__code__.co_varnames:
|
||||
raise ValueError(f"'generator' must be in {func}'s signature")
|
||||
# this call has to be outside the wrapper or since __qualname__ changes in multiprocessing
|
||||
transform = format_transform_for_fingerprint(func, version=version)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
kwargs_for_fingerprint = format_kwargs_for_fingerprint(
|
||||
func,
|
||||
args,
|
||||
kwargs,
|
||||
use_kwargs=use_kwargs,
|
||||
ignore_kwargs=ignore_kwargs,
|
||||
randomized_function=randomized_function,
|
||||
)
|
||||
|
||||
if args:
|
||||
dataset: Dataset = args[0]
|
||||
args = args[1:]
|
||||
else:
|
||||
dataset: Dataset = kwargs.pop(next(iter(inspect.signature(func).parameters)))
|
||||
|
||||
# compute new_fingerprint and add it to the args of not in-place transforms
|
||||
if inplace:
|
||||
new_fingerprint = update_fingerprint(dataset._fingerprint, transform, kwargs_for_fingerprint)
|
||||
else:
|
||||
for fingerprint_name in fingerprint_names: # transforms like `train_test_split` have several hashes
|
||||
if kwargs.get(fingerprint_name) is None:
|
||||
kwargs_for_fingerprint["fingerprint_name"] = fingerprint_name
|
||||
kwargs[fingerprint_name] = update_fingerprint(
|
||||
dataset._fingerprint, transform, kwargs_for_fingerprint
|
||||
)
|
||||
else:
|
||||
validate_fingerprint(kwargs[fingerprint_name])
|
||||
|
||||
# Call actual function
|
||||
|
||||
out = func(dataset, *args, **kwargs)
|
||||
|
||||
# Update fingerprint of in-place transforms + update in-place history of transforms
|
||||
|
||||
if inplace: # update after calling func so that the fingerprint doesn't change if the function fails
|
||||
dataset._fingerprint = new_fingerprint
|
||||
|
||||
return out
|
||||
|
||||
wrapper._decorator_name_ = "fingerprint"
|
||||
return wrapper
|
||||
|
||||
return _fingerprint
|
||||
@@ -0,0 +1,136 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors and the TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from .. import config
|
||||
from ..utils import logging
|
||||
from .formatting import (
|
||||
ArrowFormatter,
|
||||
CustomFormatter,
|
||||
Formatter,
|
||||
PandasFormatter,
|
||||
PythonFormatter,
|
||||
TableFormatter,
|
||||
TensorFormatter,
|
||||
format_table,
|
||||
query_table,
|
||||
)
|
||||
from .np_formatter import NumpyFormatter
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
_FORMAT_TYPES: dict[Optional[str], type[Formatter]] = {}
|
||||
_FORMAT_TYPES_ALIASES: dict[Optional[str], str] = {}
|
||||
_FORMAT_TYPES_ALIASES_UNAVAILABLE: dict[Optional[str], Exception] = {}
|
||||
|
||||
|
||||
def _register_formatter(
|
||||
formatter_cls: type,
|
||||
format_type: Optional[str],
|
||||
aliases: Optional[list[str]] = None,
|
||||
):
|
||||
"""
|
||||
Register a Formatter object using a name and optional aliases.
|
||||
This function must be used on a Formatter class.
|
||||
"""
|
||||
aliases = aliases if aliases is not None else []
|
||||
if format_type in _FORMAT_TYPES:
|
||||
logger.warning(
|
||||
f"Overwriting format type '{format_type}' ({_FORMAT_TYPES[format_type].__name__} -> {formatter_cls.__name__})"
|
||||
)
|
||||
_FORMAT_TYPES[format_type] = formatter_cls
|
||||
for alias in set(aliases + [format_type]):
|
||||
if alias in _FORMAT_TYPES_ALIASES:
|
||||
logger.warning(
|
||||
f"Overwriting format type alias '{alias}' ({_FORMAT_TYPES_ALIASES[alias]} -> {format_type})"
|
||||
)
|
||||
_FORMAT_TYPES_ALIASES[alias] = format_type
|
||||
|
||||
|
||||
def _register_unavailable_formatter(
|
||||
unavailable_error: Exception, format_type: Optional[str], aliases: Optional[list[str]] = None
|
||||
):
|
||||
"""
|
||||
Register an unavailable Formatter object using a name and optional aliases.
|
||||
This function must be used on an Exception object that is raised when trying to get the unavailable formatter.
|
||||
"""
|
||||
aliases = aliases if aliases is not None else []
|
||||
for alias in set(aliases + [format_type]):
|
||||
_FORMAT_TYPES_ALIASES_UNAVAILABLE[alias] = unavailable_error
|
||||
|
||||
|
||||
# Here we define all the available formatting functions that can be used by `Dataset.set_format`
|
||||
_register_formatter(PythonFormatter, None, aliases=["python"])
|
||||
_register_formatter(ArrowFormatter, "arrow", aliases=["pa", "pyarrow"])
|
||||
_register_formatter(NumpyFormatter, "numpy", aliases=["np"])
|
||||
_register_formatter(PandasFormatter, "pandas", aliases=["pd"])
|
||||
_register_formatter(CustomFormatter, "custom")
|
||||
|
||||
if config.POLARS_AVAILABLE:
|
||||
from .polars_formatter import PolarsFormatter
|
||||
|
||||
_register_formatter(PolarsFormatter, "polars", aliases=["pl"])
|
||||
else:
|
||||
_polars_error = ValueError("Polars needs to be installed to be able to return Polars dataframes.")
|
||||
_register_unavailable_formatter(_polars_error, "polars", aliases=["pl"])
|
||||
|
||||
if config.TORCH_AVAILABLE:
|
||||
from .torch_formatter import TorchFormatter
|
||||
|
||||
_register_formatter(TorchFormatter, "torch", aliases=["pt", "pytorch"])
|
||||
else:
|
||||
_torch_error = ValueError("PyTorch needs to be installed to be able to return PyTorch tensors.")
|
||||
_register_unavailable_formatter(_torch_error, "torch", aliases=["pt", "pytorch"])
|
||||
|
||||
if config.TF_AVAILABLE:
|
||||
from .tf_formatter import TFFormatter
|
||||
|
||||
_register_formatter(TFFormatter, "tensorflow", aliases=["tf"])
|
||||
else:
|
||||
_tf_error = ValueError("Tensorflow needs to be installed to be able to return Tensorflow tensors.")
|
||||
_register_unavailable_formatter(_tf_error, "tensorflow", aliases=["tf"])
|
||||
|
||||
if config.JAX_AVAILABLE:
|
||||
from .jax_formatter import JaxFormatter
|
||||
|
||||
_register_formatter(JaxFormatter, "jax", aliases=[])
|
||||
else:
|
||||
_jax_error = ValueError("JAX needs to be installed to be able to return JAX arrays.")
|
||||
_register_unavailable_formatter(_jax_error, "jax", aliases=[])
|
||||
|
||||
|
||||
def get_format_type_from_alias(format_type: Optional[str]) -> Optional[str]:
|
||||
"""If the given format type is a known alias, then return its main type name. Otherwise return the type with no change."""
|
||||
if format_type in _FORMAT_TYPES_ALIASES:
|
||||
return _FORMAT_TYPES_ALIASES[format_type]
|
||||
else:
|
||||
return format_type
|
||||
|
||||
|
||||
def get_formatter(format_type: Optional[str], **format_kwargs) -> Formatter:
|
||||
"""
|
||||
Factory function to get a Formatter given its type name and keyword arguments.
|
||||
A formatter is an object that extracts and formats data from pyarrow table.
|
||||
It defines the formatting for rows, columns and batches.
|
||||
If the formatter for a given type name doesn't exist or is not available, an error is raised.
|
||||
"""
|
||||
format_type = get_format_type_from_alias(format_type)
|
||||
if format_type in _FORMAT_TYPES:
|
||||
return _FORMAT_TYPES[format_type](**format_kwargs)
|
||||
if format_type in _FORMAT_TYPES_ALIASES_UNAVAILABLE:
|
||||
raise _FORMAT_TYPES_ALIASES_UNAVAILABLE[format_type]
|
||||
else:
|
||||
raise ValueError(f"Format type should be one of {list(_FORMAT_TYPES.keys())}, but got '{format_type}'")
|
||||
@@ -0,0 +1,678 @@
|
||||
# Copyright 2020 The HuggingFace Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import numbers
|
||||
import operator
|
||||
from collections.abc import Iterable, Mapping, MutableMapping
|
||||
from functools import partial
|
||||
|
||||
# Lint as: python3
|
||||
from typing import Any, Callable, Generic, Optional, TypeVar, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
|
||||
from ..features import Features
|
||||
from ..features.features import _ArrayXDExtensionType, _is_zero_copy_only, decode_nested_example, pandas_types_mapper
|
||||
from ..table import Table
|
||||
from ..utils.py_utils import no_op_if_value_is_null
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
RowFormat = TypeVar("RowFormat")
|
||||
ColumnFormat = TypeVar("ColumnFormat")
|
||||
BatchFormat = TypeVar("BatchFormat")
|
||||
|
||||
|
||||
def _is_range_contiguous(key: range) -> bool:
|
||||
return key.step == 1 and key.stop >= key.start
|
||||
|
||||
|
||||
def _raise_bad_key_type(key: Any):
|
||||
raise TypeError(
|
||||
f"Wrong key type: '{key}' of type '{type(key)}'. Expected one of int, slice, range, str or Iterable."
|
||||
)
|
||||
|
||||
|
||||
def _query_table_with_indices_mapping(
|
||||
table: Table, key: Union[int, slice, range, str, Iterable], indices: Table
|
||||
) -> pa.Table:
|
||||
"""
|
||||
Query a pyarrow Table to extract the subtable that correspond to the given key.
|
||||
The :obj:`indices` parameter corresponds to the indices mapping in case we cant to take into
|
||||
account a shuffling or an indices selection for example.
|
||||
The indices table must contain one column named "indices" of type uint64.
|
||||
"""
|
||||
if isinstance(key, int):
|
||||
key = indices.fast_slice(key % indices.num_rows, 1).column(0)[0].as_py()
|
||||
return _query_table(table, key)
|
||||
if isinstance(key, slice):
|
||||
key = range(*key.indices(indices.num_rows))
|
||||
if isinstance(key, range):
|
||||
if _is_range_contiguous(key) and key.start >= 0:
|
||||
return _query_table(
|
||||
table, [i.as_py() for i in indices.fast_slice(key.start, key.stop - key.start).column(0)]
|
||||
)
|
||||
else:
|
||||
pass # treat as an iterable
|
||||
if isinstance(key, str):
|
||||
table = table.select([key])
|
||||
return _query_table(table, indices.column(0).to_pylist())
|
||||
if isinstance(key, Iterable):
|
||||
return _query_table(table, [indices.fast_slice(i, 1).column(0)[0].as_py() for i in key])
|
||||
|
||||
_raise_bad_key_type(key)
|
||||
|
||||
|
||||
def _query_table(table: Table, key: Union[int, slice, range, str, Iterable]) -> pa.Table:
|
||||
"""
|
||||
Query a pyarrow Table to extract the subtable that correspond to the given key.
|
||||
"""
|
||||
if isinstance(key, int):
|
||||
return table.fast_slice(key % table.num_rows, 1)
|
||||
if isinstance(key, slice):
|
||||
key = range(*key.indices(table.num_rows))
|
||||
if isinstance(key, range):
|
||||
if _is_range_contiguous(key) and key.start >= 0:
|
||||
return table.fast_slice(key.start, key.stop - key.start)
|
||||
else:
|
||||
pass # treat as an iterable
|
||||
if isinstance(key, str):
|
||||
return table.table.drop([column for column in table.column_names if column != key])
|
||||
if isinstance(key, Iterable):
|
||||
key = np.fromiter(key, np.int64)
|
||||
if len(key) == 0:
|
||||
return table.table.slice(0, 0)
|
||||
# don't use pyarrow.Table.take even for pyarrow >=1.0 (see https://issues.apache.org/jira/browse/ARROW-9773)
|
||||
return table.fast_gather(key % table.num_rows)
|
||||
|
||||
_raise_bad_key_type(key)
|
||||
|
||||
|
||||
def _is_array_with_nulls(pa_array: pa.Array) -> bool:
|
||||
return pa_array.null_count > 0
|
||||
|
||||
|
||||
class BaseArrowExtractor(Generic[RowFormat, ColumnFormat, BatchFormat]):
|
||||
"""
|
||||
Arrow extractor are used to extract data from pyarrow tables.
|
||||
It makes it possible to extract rows, columns and batches.
|
||||
These three extractions types have to be implemented.
|
||||
"""
|
||||
|
||||
def extract_row(self, pa_table: pa.Table) -> RowFormat:
|
||||
raise NotImplementedError
|
||||
|
||||
def extract_column(self, pa_table: pa.Table) -> ColumnFormat:
|
||||
raise NotImplementedError
|
||||
|
||||
def extract_batch(self, pa_table: pa.Table) -> BatchFormat:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _unnest(py_dict: dict[str, list[T]]) -> dict[str, T]:
|
||||
"""Return the first element of a batch (dict) as a row (dict)"""
|
||||
return {key: array[0] for key, array in py_dict.items()}
|
||||
|
||||
|
||||
class SimpleArrowExtractor(BaseArrowExtractor[pa.Table, pa.Array, pa.Table]):
|
||||
def extract_row(self, pa_table: pa.Table) -> pa.Table:
|
||||
return pa_table
|
||||
|
||||
def extract_column(self, pa_table: pa.Table) -> pa.Array:
|
||||
return pa_table.column(0)
|
||||
|
||||
def extract_batch(self, pa_table: pa.Table) -> pa.Table:
|
||||
return pa_table
|
||||
|
||||
|
||||
class PythonArrowExtractor(BaseArrowExtractor[dict, list, dict]):
|
||||
def extract_row(self, pa_table: pa.Table) -> dict:
|
||||
return _unnest(pa_table.to_pydict())
|
||||
|
||||
def extract_column(self, pa_table: pa.Table) -> list:
|
||||
return pa_table.column(0).to_pylist()
|
||||
|
||||
def extract_batch(self, pa_table: pa.Table) -> dict:
|
||||
return pa_table.to_pydict()
|
||||
|
||||
|
||||
class NumpyArrowExtractor(BaseArrowExtractor[dict, np.ndarray, dict]):
|
||||
def __init__(self, **np_array_kwargs):
|
||||
self.np_array_kwargs = np_array_kwargs
|
||||
|
||||
def extract_row(self, pa_table: pa.Table) -> dict:
|
||||
return _unnest(self.extract_batch(pa_table))
|
||||
|
||||
def extract_column(self, pa_table: pa.Table) -> np.ndarray:
|
||||
return self._arrow_array_to_numpy(pa_table[pa_table.column_names[0]])
|
||||
|
||||
def extract_batch(self, pa_table: pa.Table) -> dict:
|
||||
return {col: self._arrow_array_to_numpy(pa_table[col]) for col in pa_table.column_names}
|
||||
|
||||
def _arrow_array_to_numpy(self, pa_array: pa.Array) -> np.ndarray:
|
||||
if isinstance(pa_array, pa.ChunkedArray):
|
||||
if isinstance(pa_array.type, _ArrayXDExtensionType):
|
||||
# don't call to_pylist() to preserve dtype of the fixed-size array
|
||||
zero_copy_only = _is_zero_copy_only(pa_array.type.storage_dtype, unnest=True)
|
||||
array: list = [
|
||||
row for chunk in pa_array.chunks for row in chunk.to_numpy(zero_copy_only=zero_copy_only)
|
||||
]
|
||||
else:
|
||||
zero_copy_only = _is_zero_copy_only(pa_array.type) and all(
|
||||
not _is_array_with_nulls(chunk) for chunk in pa_array.chunks
|
||||
)
|
||||
array: list = [
|
||||
row for chunk in pa_array.chunks for row in chunk.to_numpy(zero_copy_only=zero_copy_only)
|
||||
]
|
||||
else:
|
||||
if isinstance(pa_array.type, _ArrayXDExtensionType):
|
||||
# don't call to_pylist() to preserve dtype of the fixed-size array
|
||||
zero_copy_only = _is_zero_copy_only(pa_array.type.storage_dtype, unnest=True)
|
||||
array: list = pa_array.to_numpy(zero_copy_only=zero_copy_only)
|
||||
else:
|
||||
zero_copy_only = _is_zero_copy_only(pa_array.type) and not _is_array_with_nulls(pa_array)
|
||||
array: list = pa_array.to_numpy(zero_copy_only=zero_copy_only).tolist()
|
||||
|
||||
if len(array) > 0:
|
||||
if any(
|
||||
(isinstance(x, np.ndarray) and (x.dtype == object or x.shape != array[0].shape))
|
||||
or (isinstance(x, float) and np.isnan(x))
|
||||
for x in array
|
||||
):
|
||||
if np.lib.NumpyVersion(np.__version__) >= "2.0.0b1":
|
||||
return np.asarray(array, dtype=object)
|
||||
return np.array(array, copy=False, dtype=object)
|
||||
if np.lib.NumpyVersion(np.__version__) >= "2.0.0b1":
|
||||
return np.asarray(array)
|
||||
else:
|
||||
return np.array(array, copy=False)
|
||||
|
||||
|
||||
class PandasArrowExtractor(BaseArrowExtractor[pd.DataFrame, pd.Series, pd.DataFrame]):
|
||||
def extract_row(self, pa_table: pa.Table) -> pd.DataFrame:
|
||||
return pa_table.slice(length=1).to_pandas(types_mapper=pandas_types_mapper)
|
||||
|
||||
def extract_column(self, pa_table: pa.Table) -> pd.Series:
|
||||
return pa_table.select([0]).to_pandas(types_mapper=pandas_types_mapper)[pa_table.column_names[0]]
|
||||
|
||||
def extract_batch(self, pa_table: pa.Table) -> pd.DataFrame:
|
||||
return pa_table.to_pandas(types_mapper=pandas_types_mapper)
|
||||
|
||||
|
||||
class PythonFeaturesDecoder:
|
||||
def __init__(
|
||||
self, features: Optional[Features], token_per_repo_id: Optional[dict[str, Union[str, bool, None]]] = None
|
||||
):
|
||||
self.features = features
|
||||
self.token_per_repo_id = token_per_repo_id
|
||||
|
||||
def decode_row(self, row: dict) -> dict:
|
||||
return self.features.decode_example(row, token_per_repo_id=self.token_per_repo_id) if self.features else row
|
||||
|
||||
def decode_column(self, column: list, column_name: str) -> list:
|
||||
return (
|
||||
self.features.decode_column(column, column_name, token_per_repo_id=self.token_per_repo_id)
|
||||
if self.features
|
||||
else column
|
||||
)
|
||||
|
||||
def decode_batch(self, batch: dict) -> dict:
|
||||
return self.features.decode_batch(batch, token_per_repo_id=self.token_per_repo_id) if self.features else batch
|
||||
|
||||
|
||||
class PandasFeaturesDecoder:
|
||||
def __init__(self, features: Optional[Features]):
|
||||
self.features = features
|
||||
|
||||
def decode_row(self, row: pd.DataFrame) -> pd.DataFrame:
|
||||
decode = (
|
||||
{
|
||||
column_name: no_op_if_value_is_null(partial(decode_nested_example, feature))
|
||||
for column_name, feature in self.features.items()
|
||||
if self.features._column_requires_decoding[column_name]
|
||||
}
|
||||
if self.features
|
||||
else {}
|
||||
)
|
||||
if decode:
|
||||
row[list(decode.keys())] = row.transform(decode)
|
||||
return row
|
||||
|
||||
def decode_column(self, column: pd.Series, column_name: str) -> pd.Series:
|
||||
decode = (
|
||||
no_op_if_value_is_null(partial(decode_nested_example, self.features[column_name]))
|
||||
if self.features and column_name in self.features and self.features._column_requires_decoding[column_name]
|
||||
else None
|
||||
)
|
||||
if decode:
|
||||
column = column.transform(decode)
|
||||
return column
|
||||
|
||||
def decode_batch(self, batch: pd.DataFrame) -> pd.DataFrame:
|
||||
return self.decode_row(batch)
|
||||
|
||||
|
||||
class LazyDict(MutableMapping):
|
||||
"""A dictionary backed by Arrow data. The values are formatted on-the-fly when accessing the dictionary."""
|
||||
|
||||
def __init__(self, pa_table: pa.Table, formatter: "Formatter"):
|
||||
self.pa_table = pa_table
|
||||
self.formatter = formatter
|
||||
|
||||
self.data = dict.fromkeys(pa_table.column_names)
|
||||
self.keys_to_format = set(self.data.keys())
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = self.data[key]
|
||||
if key in self.keys_to_format:
|
||||
value = self.format(key)
|
||||
self.data[key] = value
|
||||
self.keys_to_format.remove(key)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.keys_to_format:
|
||||
self.keys_to_format.remove(key)
|
||||
self.data[key] = value
|
||||
|
||||
def __delitem__(self, key) -> None:
|
||||
if key in self.keys_to_format:
|
||||
self.keys_to_format.remove(key)
|
||||
del self.data[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.data
|
||||
|
||||
def __repr__(self):
|
||||
self._format_all()
|
||||
return repr(self.data)
|
||||
|
||||
def __or__(self, other):
|
||||
if isinstance(other, LazyDict):
|
||||
inst = self.copy()
|
||||
other = other.copy()
|
||||
other._format_all()
|
||||
inst.keys_to_format -= other.data.keys()
|
||||
inst.data = inst.data | other.data
|
||||
return inst
|
||||
if isinstance(other, dict):
|
||||
inst = self.copy()
|
||||
inst.keys_to_format -= other.keys()
|
||||
inst.data = inst.data | other
|
||||
return inst
|
||||
return NotImplemented
|
||||
|
||||
def __ror__(self, other):
|
||||
if isinstance(other, LazyDict):
|
||||
inst = self.copy()
|
||||
other = other.copy()
|
||||
other._format_all()
|
||||
inst.keys_to_format -= other.data.keys()
|
||||
inst.data = other.data | inst.data
|
||||
return inst
|
||||
if isinstance(other, dict):
|
||||
inst = self.copy()
|
||||
inst.keys_to_format -= other.keys()
|
||||
inst.data = other | inst.data
|
||||
return inst
|
||||
return NotImplemented
|
||||
|
||||
def __ior__(self, other):
|
||||
if isinstance(other, LazyDict):
|
||||
other = other.copy()
|
||||
other._format_all()
|
||||
self.keys_to_format -= other.data.keys()
|
||||
self.data |= other.data
|
||||
else:
|
||||
self.keys_to_format -= other.keys()
|
||||
self.data |= other
|
||||
return self
|
||||
|
||||
def __copy__(self):
|
||||
# Identical to `UserDict.__copy__`
|
||||
inst = self.__class__.__new__(self.__class__)
|
||||
inst.__dict__.update(self.__dict__)
|
||||
# Create a copy and avoid triggering descriptors
|
||||
inst.__dict__["data"] = self.__dict__["data"].copy()
|
||||
inst.__dict__["keys_to_format"] = self.__dict__["keys_to_format"].copy()
|
||||
return inst
|
||||
|
||||
def copy(self):
|
||||
import copy
|
||||
|
||||
return copy.copy(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def format(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def _format_all(self):
|
||||
for key in self.keys_to_format:
|
||||
self.data[key] = self.format(key)
|
||||
self.keys_to_format.clear()
|
||||
|
||||
|
||||
class LazyRow(LazyDict):
|
||||
def format(self, key):
|
||||
return self.formatter.format_column(self.pa_table.select([key]))[0]
|
||||
|
||||
|
||||
class LazyBatch(LazyDict):
|
||||
def format(self, key):
|
||||
return self.formatter.format_column(self.pa_table.select([key]))
|
||||
|
||||
|
||||
class Formatter(Generic[RowFormat, ColumnFormat, BatchFormat]):
|
||||
"""
|
||||
A formatter is an object that extracts and formats data from pyarrow tables.
|
||||
It defines the formatting for rows, columns and batches.
|
||||
"""
|
||||
|
||||
simple_arrow_extractor = SimpleArrowExtractor
|
||||
python_arrow_extractor = PythonArrowExtractor
|
||||
numpy_arrow_extractor = NumpyArrowExtractor
|
||||
pandas_arrow_extractor = PandasArrowExtractor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
features: Optional[Features] = None,
|
||||
token_per_repo_id: Optional[dict[str, Union[str, bool, None]]] = None,
|
||||
):
|
||||
self.features = features
|
||||
self.token_per_repo_id = token_per_repo_id
|
||||
self.python_features_decoder = PythonFeaturesDecoder(self.features, self.token_per_repo_id)
|
||||
self.pandas_features_decoder = PandasFeaturesDecoder(self.features)
|
||||
|
||||
def __call__(self, pa_table: pa.Table, query_type: str) -> Union[RowFormat, ColumnFormat, BatchFormat]:
|
||||
if query_type == "row":
|
||||
return self.format_row(pa_table)
|
||||
elif query_type == "column":
|
||||
return self.format_column(pa_table)
|
||||
elif query_type == "batch":
|
||||
return self.format_batch(pa_table)
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> RowFormat:
|
||||
raise NotImplementedError
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> ColumnFormat:
|
||||
raise NotImplementedError
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> BatchFormat:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TensorFormatter(Formatter[RowFormat, ColumnFormat, BatchFormat]):
|
||||
def recursive_tensorize(self, data_struct: dict):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TableFormatter(Formatter[RowFormat, ColumnFormat, BatchFormat]):
|
||||
table_type: str
|
||||
column_type: str
|
||||
|
||||
|
||||
class ArrowFormatter(TableFormatter[pa.Table, pa.Array, pa.Table]):
|
||||
table_type = "arrow table"
|
||||
column_type = "arrow array"
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> pa.Table:
|
||||
return self.simple_arrow_extractor().extract_row(pa_table)
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> pa.Array:
|
||||
return self.simple_arrow_extractor().extract_column(pa_table)
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> pa.Table:
|
||||
return self.simple_arrow_extractor().extract_batch(pa_table)
|
||||
|
||||
|
||||
class PythonFormatter(Formatter[Mapping, list, Mapping]):
|
||||
def __init__(self, features=None, lazy=False, token_per_repo_id=None):
|
||||
super().__init__(features, token_per_repo_id)
|
||||
self.lazy = lazy
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> Mapping:
|
||||
if self.lazy:
|
||||
return LazyRow(pa_table, self)
|
||||
row = self.python_arrow_extractor().extract_row(pa_table)
|
||||
row = self.python_features_decoder.decode_row(row)
|
||||
return row
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> list:
|
||||
column = self.python_arrow_extractor().extract_column(pa_table)
|
||||
column = self.python_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> Mapping:
|
||||
if self.lazy:
|
||||
return LazyBatch(pa_table, self)
|
||||
batch = self.python_arrow_extractor().extract_batch(pa_table)
|
||||
batch = self.python_features_decoder.decode_batch(batch)
|
||||
return batch
|
||||
|
||||
|
||||
class PandasFormatter(TableFormatter[pd.DataFrame, pd.Series, pd.DataFrame]):
|
||||
table_type = "pandas dataframe"
|
||||
column_type = "pandas series"
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> pd.DataFrame:
|
||||
row = self.pandas_arrow_extractor().extract_row(pa_table)
|
||||
row = self.pandas_features_decoder.decode_row(row)
|
||||
return row
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> pd.Series:
|
||||
column = self.pandas_arrow_extractor().extract_column(pa_table)
|
||||
column = self.pandas_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> pd.DataFrame:
|
||||
row = self.pandas_arrow_extractor().extract_batch(pa_table)
|
||||
row = self.pandas_features_decoder.decode_batch(row)
|
||||
return row
|
||||
|
||||
|
||||
class CustomFormatter(Formatter[dict, ColumnFormat, dict]):
|
||||
"""
|
||||
A user-defined custom formatter function defined by a ``transform``.
|
||||
The transform must take as input a batch of data extracted for an arrow table using the python extractor,
|
||||
and return a batch.
|
||||
If the output batch is not a dict, then output_all_columns won't work.
|
||||
If the output batch has several fields, then querying a single column won't work since we don't know which field
|
||||
to return.
|
||||
"""
|
||||
|
||||
def __init__(self, transform: Callable[[dict], dict], features=None, token_per_repo_id=None, **kwargs):
|
||||
super().__init__(features=features, token_per_repo_id=token_per_repo_id)
|
||||
self.transform = transform
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> dict:
|
||||
formatted_batch = self.format_batch(pa_table)
|
||||
try:
|
||||
return _unnest(formatted_batch)
|
||||
except Exception as exc:
|
||||
raise TypeError(
|
||||
f"Custom formatting function must return a dict of sequences to be able to pick a row, but got {formatted_batch}"
|
||||
) from exc
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> ColumnFormat:
|
||||
formatted_batch = self.format_batch(pa_table)
|
||||
if hasattr(formatted_batch, "keys"):
|
||||
if len(formatted_batch.keys()) > 1:
|
||||
raise TypeError(
|
||||
"Tried to query a column but the custom formatting function returns too many columns. "
|
||||
f"Only one column was expected but got columns {list(formatted_batch.keys())}."
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Custom formatting function must return a dict to be able to pick a row, but got {formatted_batch}"
|
||||
)
|
||||
try:
|
||||
return formatted_batch[pa_table.column_names[0]]
|
||||
except Exception as exc:
|
||||
raise TypeError(
|
||||
f"Custom formatting function must return a dict to be able to pick a row, but got {formatted_batch}"
|
||||
) from exc
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> dict:
|
||||
batch = self.python_arrow_extractor().extract_batch(pa_table)
|
||||
batch = self.python_features_decoder.decode_batch(batch)
|
||||
return self.transform(batch)
|
||||
|
||||
|
||||
def _check_valid_column_key(key: str, columns: list[str]) -> None:
|
||||
if key not in columns:
|
||||
raise KeyError(f"Column {key} not in the dataset. Current columns in the dataset: {columns}")
|
||||
|
||||
|
||||
def _check_valid_index_key(key: Union[int, slice, range, Iterable], size: int) -> None:
|
||||
if isinstance(key, int):
|
||||
if (key < 0 and key + size < 0) or (key >= size):
|
||||
raise IndexError(f"Invalid key: {key} is out of bounds for size {size}")
|
||||
return
|
||||
elif isinstance(key, slice):
|
||||
pass
|
||||
elif isinstance(key, range):
|
||||
if len(key) > 0:
|
||||
_check_valid_index_key(max(key), size=size)
|
||||
_check_valid_index_key(min(key), size=size)
|
||||
elif isinstance(key, Iterable):
|
||||
if len(key) > 0:
|
||||
_check_valid_index_key(int(max(key)), size=size)
|
||||
_check_valid_index_key(int(min(key)), size=size)
|
||||
else:
|
||||
_raise_bad_key_type(key)
|
||||
|
||||
|
||||
def key_to_query_type(key: Union[int, slice, range, str, Iterable]) -> str:
|
||||
if isinstance(key, numbers.Integral):
|
||||
return "row"
|
||||
elif isinstance(key, str):
|
||||
return "column"
|
||||
elif isinstance(key, (slice, range, Iterable)):
|
||||
return "batch"
|
||||
_raise_bad_key_type(key)
|
||||
|
||||
|
||||
def query_table(
|
||||
table: Table,
|
||||
key: Union[int, slice, range, str, Iterable],
|
||||
indices: Optional[Table] = None,
|
||||
) -> pa.Table:
|
||||
"""
|
||||
Query a Table to extract the subtable that correspond to the given key.
|
||||
|
||||
Args:
|
||||
table (``datasets.table.Table``): The input Table to query from
|
||||
key (``Union[int, slice, range, str, Iterable]``): The key can be of different types:
|
||||
- an integer i: the subtable containing only the i-th row
|
||||
- a slice [i:j:k]: the subtable containing the rows that correspond to this slice
|
||||
- a range(i, j, k): the subtable containing the rows that correspond to this range
|
||||
- a string c: the subtable containing all the rows but only the column c
|
||||
- an iterable l: the subtable that is the concatenation of all the i-th rows for all i in the iterable
|
||||
indices (Optional ``datasets.table.Table``): If not None, it is used to re-map the given key to the table rows.
|
||||
The indices table must contain one column named "indices" of type uint64.
|
||||
This is used in case of shuffling or rows selection.
|
||||
|
||||
|
||||
Returns:
|
||||
``pyarrow.Table``: the result of the query on the input table
|
||||
"""
|
||||
# Check if key is valid
|
||||
if not isinstance(key, (int, slice, range, str, Iterable)):
|
||||
try:
|
||||
key = operator.index(key)
|
||||
except TypeError:
|
||||
_raise_bad_key_type(key)
|
||||
if isinstance(key, str):
|
||||
_check_valid_column_key(key, table.column_names)
|
||||
else:
|
||||
size = indices.num_rows if indices is not None else table.num_rows
|
||||
_check_valid_index_key(key, size)
|
||||
# Query the main table
|
||||
if indices is None:
|
||||
pa_subtable = _query_table(table, key)
|
||||
else:
|
||||
pa_subtable = _query_table_with_indices_mapping(table, key, indices=indices)
|
||||
return pa_subtable
|
||||
|
||||
|
||||
def format_table(
|
||||
table: Table,
|
||||
key: Union[int, slice, range, str, Iterable],
|
||||
formatter: Formatter,
|
||||
format_columns: Optional[list] = None,
|
||||
output_all_columns=False,
|
||||
):
|
||||
"""
|
||||
Format a Table depending on the key that was used and a Formatter object.
|
||||
|
||||
Args:
|
||||
table (``datasets.table.Table``): The input Table to format
|
||||
key (``Union[int, slice, range, str, Iterable]``): Depending on the key that was used, the formatter formats
|
||||
the table as either a row, a column or a batch.
|
||||
formatter (``datasets.formatting.formatting.Formatter``): Any subclass of a Formatter such as
|
||||
PythonFormatter, NumpyFormatter, etc.
|
||||
format_columns (:obj:`List[str]`, optional): if not None, it defines the columns that will be formatted using the
|
||||
given formatter. Other columns are discarded (unless ``output_all_columns`` is True)
|
||||
output_all_columns (:obj:`bool`, defaults to False). If True, the formatted output is completed using the columns
|
||||
that are not in the ``format_columns`` list. For these columns, the PythonFormatter is used.
|
||||
|
||||
|
||||
Returns:
|
||||
A row, column or batch formatted object defined by the Formatter:
|
||||
- the PythonFormatter returns a dictionary for a row or a batch, and a list for a column.
|
||||
- the NumpyFormatter returns a dictionary for a row or a batch, and a np.array for a column.
|
||||
- the PandasFormatter returns a pd.DataFrame for a row or a batch, and a pd.Series for a column.
|
||||
- the TorchFormatter returns a dictionary for a row or a batch, and a torch.Tensor for a column.
|
||||
- the TFFormatter returns a dictionary for a row or a batch, and a tf.Tensor for a column.
|
||||
"""
|
||||
if isinstance(table, Table):
|
||||
pa_table = table.table
|
||||
else:
|
||||
pa_table = table
|
||||
query_type = key_to_query_type(key)
|
||||
python_formatter = PythonFormatter(features=formatter.features)
|
||||
if format_columns is None:
|
||||
return formatter(pa_table, query_type=query_type)
|
||||
elif query_type == "column":
|
||||
if key in format_columns:
|
||||
return formatter(pa_table, query_type)
|
||||
else:
|
||||
return python_formatter(pa_table, query_type=query_type)
|
||||
else:
|
||||
pa_table_to_format = pa_table.drop(col for col in pa_table.column_names if col not in format_columns)
|
||||
formatted_output = formatter(pa_table_to_format, query_type=query_type)
|
||||
if output_all_columns:
|
||||
if isinstance(formatted_output, MutableMapping):
|
||||
pa_table_with_remaining_columns = pa_table.drop(
|
||||
col for col in pa_table.column_names if col in format_columns
|
||||
)
|
||||
remaining_columns_dict = python_formatter(pa_table_with_remaining_columns, query_type=query_type)
|
||||
formatted_output.update(remaining_columns_dict)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Custom formatting function must return a dict to work with output_all_columns=True, but got {formatted_output}"
|
||||
)
|
||||
return formatted_output
|
||||
@@ -0,0 +1,174 @@
|
||||
# Copyright 2021 The HuggingFace Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..utils.logging import get_logger
|
||||
from ..utils.py_utils import map_nested
|
||||
from .formatting import TensorFormatter
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import jax
|
||||
import jaxlib
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
DEVICE_MAPPING: Optional[dict] = None
|
||||
|
||||
|
||||
class JaxFormatter(TensorFormatter[Mapping, "jax.Array", Mapping]):
|
||||
def __init__(self, features=None, device=None, token_per_repo_id=None, **jnp_array_kwargs):
|
||||
super().__init__(features=features, token_per_repo_id=token_per_repo_id)
|
||||
import jax
|
||||
from jaxlib.xla_client import Device
|
||||
|
||||
if isinstance(device, Device):
|
||||
raise ValueError(
|
||||
f"Expected {device} to be a `str` not {type(device)}, as `jaxlib.xla_extension.Device` "
|
||||
"is not serializable neither with `pickle` nor with `dill`. Instead you can surround "
|
||||
"the device with `str()` to get its string identifier that will be internally mapped "
|
||||
"to the actual `jaxlib.xla_extension.Device`."
|
||||
)
|
||||
self.device = device if isinstance(device, str) else str(jax.devices()[0])
|
||||
# using global variable since `jaxlib.xla_extension.Device` is not serializable neither
|
||||
# with `pickle` nor with `dill`, so we need to use a global variable instead
|
||||
global DEVICE_MAPPING
|
||||
if DEVICE_MAPPING is None:
|
||||
DEVICE_MAPPING = self._map_devices_to_str()
|
||||
if self.device not in list(DEVICE_MAPPING.keys()):
|
||||
logger.warning(
|
||||
f"Device with string identifier {self.device} not listed among the available "
|
||||
f"devices: {list(DEVICE_MAPPING.keys())}, so falling back to the default "
|
||||
f"device: {str(jax.devices()[0])}."
|
||||
)
|
||||
self.device = str(jax.devices()[0])
|
||||
self.jnp_array_kwargs = jnp_array_kwargs
|
||||
|
||||
@staticmethod
|
||||
def _map_devices_to_str() -> dict[str, "jaxlib.xla_extension.Device"]:
|
||||
import jax
|
||||
|
||||
return {str(device): device for device in jax.devices()}
|
||||
|
||||
def _consolidate(self, column):
|
||||
import jax
|
||||
import jax.numpy as jnp
|
||||
|
||||
if isinstance(column, list) and column:
|
||||
if all(
|
||||
isinstance(x, jax.Array) and x.shape == column[0].shape and x.dtype == column[0].dtype for x in column
|
||||
):
|
||||
return jnp.stack(column, axis=0)
|
||||
return column
|
||||
|
||||
def _tensorize(self, value):
|
||||
import jax
|
||||
import jax.numpy as jnp
|
||||
|
||||
if isinstance(value, (str, bytes, type(None))):
|
||||
return value
|
||||
elif isinstance(value, (np.character, np.ndarray)) and np.issubdtype(value.dtype, np.character):
|
||||
return value.tolist()
|
||||
|
||||
default_dtype = {}
|
||||
|
||||
if isinstance(value, (np.number, np.ndarray)) and np.issubdtype(value.dtype, np.integer):
|
||||
# the default int precision depends on the jax config
|
||||
# see https://jax.readthedocs.io/en/latest/notebooks/Common_Gotchas_in_JAX.html#double-64bit-precision
|
||||
if jax.config.jax_enable_x64:
|
||||
default_dtype = {"dtype": jnp.int64}
|
||||
else:
|
||||
default_dtype = {"dtype": jnp.int32}
|
||||
elif isinstance(value, (np.number, np.ndarray)) and np.issubdtype(value.dtype, np.floating):
|
||||
default_dtype = {"dtype": jnp.float32}
|
||||
|
||||
if config.PIL_AVAILABLE and "PIL" in sys.modules:
|
||||
import PIL.Image
|
||||
|
||||
if isinstance(value, PIL.Image.Image):
|
||||
value = np.asarray(value)
|
||||
if config.TORCHVISION_AVAILABLE and "torchvision" in sys.modules:
|
||||
try:
|
||||
from torchvision.io import VideoReader
|
||||
|
||||
if isinstance(value, VideoReader):
|
||||
return value # TODO(QL): set output to jax arrays ?
|
||||
except ImportError:
|
||||
pass
|
||||
if config.TORCHCODEC_AVAILABLE and "torchcodec" in sys.modules:
|
||||
from torchcodec.decoders import AudioDecoder, VideoDecoder
|
||||
|
||||
if isinstance(value, (VideoDecoder, AudioDecoder)):
|
||||
return value # TODO(QL): set output to jax arrays ?
|
||||
|
||||
# using global variable since `jaxlib.xla_extension.Device` is not serializable neither
|
||||
# with `pickle` nor with `dill`, so we need to use a global variable instead
|
||||
global DEVICE_MAPPING
|
||||
if DEVICE_MAPPING is None:
|
||||
DEVICE_MAPPING = self._map_devices_to_str()
|
||||
|
||||
with jax.default_device(DEVICE_MAPPING[self.device]):
|
||||
# calling jnp.array on a np.ndarray does copy the data
|
||||
# see https://github.com/google/jax/issues/4486
|
||||
return jnp.array(value, **{**default_dtype, **self.jnp_array_kwargs})
|
||||
|
||||
def _recursive_tensorize(self, data_struct):
|
||||
import jax
|
||||
|
||||
# support for torch, tf, jax etc.
|
||||
if config.TORCH_AVAILABLE and "torch" in sys.modules:
|
||||
import torch
|
||||
|
||||
if isinstance(data_struct, torch.Tensor):
|
||||
return self._tensorize(data_struct.detach().cpu().numpy()[()])
|
||||
if hasattr(data_struct, "__array__") and not isinstance(data_struct, jax.Array):
|
||||
data_struct = data_struct.__array__()
|
||||
# support for nested types like struct of list of struct
|
||||
if isinstance(data_struct, np.ndarray):
|
||||
if data_struct.dtype == object: # jax arrays cannot be instantied from an array of objects
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
elif isinstance(data_struct, (list, tuple)):
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
return self._tensorize(data_struct)
|
||||
|
||||
def recursive_tensorize(self, data_struct: dict):
|
||||
return map_nested(self._recursive_tensorize, data_struct, map_list=False)
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> Mapping:
|
||||
row = self.numpy_arrow_extractor().extract_row(pa_table)
|
||||
row = self.python_features_decoder.decode_row(row)
|
||||
return self.recursive_tensorize(row)
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> "jax.Array":
|
||||
column = self.numpy_arrow_extractor().extract_column(pa_table)
|
||||
column = self.python_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
column = self.recursive_tensorize(column)
|
||||
column = self._consolidate(column)
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> Mapping:
|
||||
batch = self.numpy_arrow_extractor().extract_batch(pa_table)
|
||||
batch = self.python_features_decoder.decode_batch(batch)
|
||||
batch = self.recursive_tensorize(batch)
|
||||
for column_name in batch:
|
||||
batch[column_name] = self._consolidate(batch[column_name])
|
||||
return batch
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright 2020 The HuggingFace Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..utils.py_utils import map_nested
|
||||
from .formatting import TensorFormatter
|
||||
|
||||
|
||||
class NumpyFormatter(TensorFormatter[Mapping, np.ndarray, Mapping]):
|
||||
def __init__(self, features=None, token_per_repo_id=None, **np_array_kwargs):
|
||||
super().__init__(features=features, token_per_repo_id=token_per_repo_id)
|
||||
self.np_array_kwargs = np_array_kwargs
|
||||
|
||||
def _consolidate(self, column):
|
||||
if isinstance(column, list):
|
||||
if column and all(
|
||||
isinstance(x, np.ndarray) and x.shape == column[0].shape and x.dtype == column[0].dtype for x in column
|
||||
):
|
||||
return np.stack(column)
|
||||
else:
|
||||
# don't use np.array(column, dtype=object)
|
||||
# since it fails in certain cases
|
||||
# see https://stackoverflow.com/q/51005699
|
||||
out = np.empty(len(column), dtype=object)
|
||||
out[:] = column
|
||||
return out
|
||||
return column
|
||||
|
||||
def _tensorize(self, value):
|
||||
if isinstance(value, (str, bytes, type(None))):
|
||||
return value
|
||||
elif isinstance(value, (np.character, np.ndarray)) and np.issubdtype(value.dtype, np.character):
|
||||
return value
|
||||
elif isinstance(value, np.number):
|
||||
return value
|
||||
|
||||
default_dtype = {}
|
||||
|
||||
if isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.integer):
|
||||
default_dtype = {"dtype": np.int64}
|
||||
elif isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.floating):
|
||||
default_dtype = {"dtype": np.float32}
|
||||
|
||||
if config.PIL_AVAILABLE and "PIL" in sys.modules:
|
||||
import PIL.Image
|
||||
|
||||
if isinstance(value, PIL.Image.Image):
|
||||
return np.asarray(value, **self.np_array_kwargs)
|
||||
if config.TORCHVISION_AVAILABLE and "torchvision" in sys.modules:
|
||||
try:
|
||||
from torchvision.io import VideoReader
|
||||
|
||||
if isinstance(value, VideoReader):
|
||||
return value # TODO(QL): set output to np arrays ?
|
||||
except ImportError:
|
||||
pass
|
||||
if config.TORCHCODEC_AVAILABLE and "torchcodec" in sys.modules:
|
||||
from torchcodec.decoders import AudioDecoder, VideoDecoder
|
||||
|
||||
if isinstance(value, (VideoDecoder, AudioDecoder)):
|
||||
return value # TODO(QL): set output to np arrays ?
|
||||
|
||||
return np.asarray(value, **{**default_dtype, **self.np_array_kwargs})
|
||||
|
||||
def _recursive_tensorize(self, data_struct):
|
||||
# support for torch, tf, jax etc.
|
||||
if config.TORCH_AVAILABLE and "torch" in sys.modules:
|
||||
import torch
|
||||
|
||||
if isinstance(data_struct, torch.Tensor):
|
||||
return self._tensorize(data_struct.detach().cpu().numpy()[()])
|
||||
if hasattr(data_struct, "__array__") and not isinstance(data_struct, (np.ndarray, np.character, np.number)):
|
||||
data_struct = data_struct.__array__()
|
||||
# support for nested types like struct of list of struct
|
||||
if isinstance(data_struct, np.ndarray):
|
||||
if data_struct.dtype == object:
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
if isinstance(data_struct, (list, tuple)):
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
return self._tensorize(data_struct)
|
||||
|
||||
def recursive_tensorize(self, data_struct: dict):
|
||||
return map_nested(self._recursive_tensorize, data_struct, map_list=False)
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> Mapping:
|
||||
row = self.numpy_arrow_extractor().extract_row(pa_table)
|
||||
row = self.python_features_decoder.decode_row(row)
|
||||
return self.recursive_tensorize(row)
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> np.ndarray:
|
||||
column = self.numpy_arrow_extractor().extract_column(pa_table)
|
||||
column = self.python_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
column = self.recursive_tensorize(column)
|
||||
column = self._consolidate(column)
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> Mapping:
|
||||
batch = self.numpy_arrow_extractor().extract_batch(pa_table)
|
||||
batch = self.python_features_decoder.decode_batch(batch)
|
||||
batch = self.recursive_tensorize(batch)
|
||||
for column_name in batch:
|
||||
batch[column_name] = self._consolidate(batch[column_name])
|
||||
return batch
|
||||
@@ -0,0 +1,124 @@
|
||||
# Copyright 2020 The HuggingFace Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..features import Features
|
||||
from ..features.features import decode_nested_example
|
||||
from ..utils.py_utils import no_op_if_value_is_null
|
||||
from .formatting import BaseArrowExtractor, TableFormatter
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import polars as pl
|
||||
|
||||
|
||||
class PolarsArrowExtractor(BaseArrowExtractor["pl.DataFrame", "pl.Series", "pl.DataFrame"]):
|
||||
def extract_row(self, pa_table: pa.Table) -> "pl.DataFrame":
|
||||
if config.POLARS_AVAILABLE:
|
||||
if "polars" not in sys.modules:
|
||||
import polars
|
||||
else:
|
||||
polars = sys.modules["polars"]
|
||||
|
||||
return polars.from_arrow(pa_table.slice(length=1))
|
||||
else:
|
||||
raise ValueError("Polars needs to be installed to be able to return Polars dataframes.")
|
||||
|
||||
def extract_column(self, pa_table: pa.Table) -> "pl.Series":
|
||||
if config.POLARS_AVAILABLE:
|
||||
if "polars" not in sys.modules:
|
||||
import polars
|
||||
else:
|
||||
polars = sys.modules["polars"]
|
||||
|
||||
return polars.from_arrow(pa_table.select([0]))[pa_table.column_names[0]]
|
||||
else:
|
||||
raise ValueError("Polars needs to be installed to be able to return Polars dataframes.")
|
||||
|
||||
def extract_batch(self, pa_table: pa.Table) -> "pl.DataFrame":
|
||||
if config.POLARS_AVAILABLE:
|
||||
if "polars" not in sys.modules:
|
||||
import polars
|
||||
else:
|
||||
polars = sys.modules["polars"]
|
||||
|
||||
return polars.from_arrow(pa_table)
|
||||
else:
|
||||
raise ValueError("Polars needs to be installed to be able to return Polars dataframes.")
|
||||
|
||||
|
||||
class PolarsFeaturesDecoder:
|
||||
def __init__(self, features: Optional[Features]):
|
||||
self.features = features
|
||||
import polars as pl # noqa: F401 - import pl at initialization
|
||||
|
||||
def decode_row(self, row: "pl.DataFrame") -> "pl.DataFrame":
|
||||
decode = (
|
||||
{
|
||||
column_name: no_op_if_value_is_null(partial(decode_nested_example, feature))
|
||||
for column_name, feature in self.features.items()
|
||||
if self.features._column_requires_decoding[column_name]
|
||||
}
|
||||
if self.features
|
||||
else {}
|
||||
)
|
||||
if decode:
|
||||
row[list(decode.keys())] = row.map_rows(decode)
|
||||
return row
|
||||
|
||||
def decode_column(self, column: "pl.Series", column_name: str) -> "pl.Series":
|
||||
decode = (
|
||||
no_op_if_value_is_null(partial(decode_nested_example, self.features[column_name]))
|
||||
if self.features and column_name in self.features and self.features._column_requires_decoding[column_name]
|
||||
else None
|
||||
)
|
||||
if decode:
|
||||
column = column.map_elements(decode)
|
||||
return column
|
||||
|
||||
def decode_batch(self, batch: "pl.DataFrame") -> "pl.DataFrame":
|
||||
return self.decode_row(batch)
|
||||
|
||||
|
||||
class PolarsFormatter(TableFormatter["pl.DataFrame", "pl.Series", "pl.DataFrame"]):
|
||||
table_type = "polars dataframe"
|
||||
column_type = "polars series"
|
||||
|
||||
def __init__(self, features=None, **np_array_kwargs):
|
||||
super().__init__(features=features)
|
||||
self.np_array_kwargs = np_array_kwargs
|
||||
self.polars_arrow_extractor = PolarsArrowExtractor
|
||||
self.polars_features_decoder = PolarsFeaturesDecoder(features)
|
||||
import polars as pl # noqa: F401 - import pl at initialization
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> "pl.DataFrame":
|
||||
row = self.polars_arrow_extractor().extract_row(pa_table)
|
||||
row = self.polars_features_decoder.decode_row(row)
|
||||
return row
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> "pl.Series":
|
||||
column = self.polars_arrow_extractor().extract_column(pa_table)
|
||||
column = self.polars_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> "pl.DataFrame":
|
||||
row = self.polars_arrow_extractor().extract_batch(pa_table)
|
||||
row = self.polars_features_decoder.decode_batch(row)
|
||||
return row
|
||||
@@ -0,0 +1,129 @@
|
||||
# Copyright 2020 The HuggingFace Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..utils.py_utils import map_nested
|
||||
from .formatting import TensorFormatter
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import tensorflow as tf
|
||||
|
||||
|
||||
class TFFormatter(TensorFormatter[Mapping, "tf.Tensor", Mapping]):
|
||||
def __init__(self, features=None, token_per_repo_id=None, **tf_tensor_kwargs):
|
||||
super().__init__(features=features, token_per_repo_id=token_per_repo_id)
|
||||
self.tf_tensor_kwargs = tf_tensor_kwargs
|
||||
import tensorflow as tf # noqa: F401 - import tf at initialization
|
||||
|
||||
def _consolidate(self, column):
|
||||
import tensorflow as tf
|
||||
|
||||
if isinstance(column, list) and column:
|
||||
if all(
|
||||
isinstance(x, tf.Tensor) and x.shape == column[0].shape and x.dtype == column[0].dtype for x in column
|
||||
):
|
||||
return tf.stack(column)
|
||||
elif all(
|
||||
isinstance(x, (tf.Tensor, tf.RaggedTensor)) and x.ndim == 1 and x.dtype == column[0].dtype
|
||||
for x in column
|
||||
):
|
||||
# only rag 1-D tensors, otherwise some dimensions become ragged even though they were consolidated
|
||||
return tf.ragged.stack(column)
|
||||
|
||||
return column
|
||||
|
||||
def _tensorize(self, value):
|
||||
import tensorflow as tf
|
||||
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
default_dtype = {}
|
||||
|
||||
if isinstance(value, (np.number, np.ndarray)) and np.issubdtype(value.dtype, np.integer):
|
||||
default_dtype = {"dtype": tf.int64}
|
||||
elif isinstance(value, (np.number, np.ndarray)) and np.issubdtype(value.dtype, np.floating):
|
||||
default_dtype = {"dtype": tf.float32}
|
||||
|
||||
if config.PIL_AVAILABLE and "PIL" in sys.modules:
|
||||
import PIL.Image
|
||||
|
||||
if isinstance(value, PIL.Image.Image):
|
||||
value = np.asarray(value)
|
||||
if config.TORCHVISION_AVAILABLE and "torchvision" in sys.modules:
|
||||
try:
|
||||
from torchvision.io import VideoReader
|
||||
|
||||
if isinstance(value, VideoReader):
|
||||
return value # TODO(QL): set output to tf tensors ?
|
||||
except ImportError:
|
||||
pass
|
||||
if config.TORCHCODEC_AVAILABLE and "torchcodec" in sys.modules:
|
||||
from torchcodec.decoders import AudioDecoder, VideoDecoder
|
||||
|
||||
if isinstance(value, (VideoDecoder, AudioDecoder)):
|
||||
return value # TODO(QL): set output to jax arrays ?
|
||||
|
||||
return tf.convert_to_tensor(value, **{**default_dtype, **self.tf_tensor_kwargs})
|
||||
|
||||
def _recursive_tensorize(self, data_struct):
|
||||
import tensorflow as tf
|
||||
|
||||
# support for torch, tf, jax etc.
|
||||
if config.TORCH_AVAILABLE and "torch" in sys.modules:
|
||||
import torch
|
||||
|
||||
if isinstance(data_struct, torch.Tensor):
|
||||
return self._tensorize(data_struct.detach().cpu().numpy()[()])
|
||||
if hasattr(data_struct, "__array__") and not isinstance(data_struct, tf.Tensor):
|
||||
data_struct = data_struct.__array__()
|
||||
# support for nested types like struct of list of struct
|
||||
if isinstance(data_struct, np.ndarray):
|
||||
if data_struct.dtype == object: # tf tensors cannot be instantied from an array of objects
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
elif isinstance(data_struct, (list, tuple)):
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
return self._tensorize(data_struct)
|
||||
|
||||
def recursive_tensorize(self, data_struct: dict):
|
||||
return map_nested(self._recursive_tensorize, data_struct, map_list=False)
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> Mapping:
|
||||
row = self.numpy_arrow_extractor().extract_row(pa_table)
|
||||
row = self.python_features_decoder.decode_row(row)
|
||||
return self.recursive_tensorize(row)
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> "tf.Tensor":
|
||||
column = self.numpy_arrow_extractor().extract_column(pa_table)
|
||||
column = self.python_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
column = self.recursive_tensorize(column)
|
||||
column = self._consolidate(column)
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> Mapping:
|
||||
batch = self.numpy_arrow_extractor().extract_batch(pa_table)
|
||||
batch = self.python_features_decoder.decode_batch(batch)
|
||||
batch = self.recursive_tensorize(batch)
|
||||
for column_name in batch:
|
||||
batch[column_name] = self._consolidate(batch[column_name])
|
||||
return batch
|
||||
@@ -0,0 +1,130 @@
|
||||
# Copyright 2020 The HuggingFace Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
from .. import config
|
||||
from ..utils.py_utils import map_nested
|
||||
from .formatting import TensorFormatter
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch
|
||||
|
||||
|
||||
class TorchFormatter(TensorFormatter[Mapping, "torch.Tensor", Mapping]):
|
||||
def __init__(self, features=None, token_per_repo_id=None, **torch_tensor_kwargs):
|
||||
super().__init__(features=features, token_per_repo_id=token_per_repo_id)
|
||||
self.torch_tensor_kwargs = torch_tensor_kwargs
|
||||
import torch # noqa import torch at initialization
|
||||
|
||||
def _consolidate(self, column):
|
||||
import torch
|
||||
|
||||
if isinstance(column, list) and column:
|
||||
if all(
|
||||
isinstance(x, torch.Tensor) and x.shape == column[0].shape and x.dtype == column[0].dtype
|
||||
for x in column
|
||||
):
|
||||
return torch.stack(column)
|
||||
return column
|
||||
|
||||
def _tensorize(self, value):
|
||||
import torch
|
||||
|
||||
if isinstance(value, (str, bytes, type(None))):
|
||||
return value
|
||||
elif isinstance(value, (np.character, np.ndarray)) and np.issubdtype(value.dtype, np.character):
|
||||
return value.tolist()
|
||||
|
||||
default_dtype = {}
|
||||
|
||||
if isinstance(value, (np.number, np.ndarray)) and np.issubdtype(value.dtype, np.integer):
|
||||
default_dtype = {"dtype": torch.int64}
|
||||
|
||||
# Convert dtype to np.int64 if it's either np.uint16 or np.uint32 to ensure compatibility.
|
||||
# np.uint64 is excluded from this conversion as there is no compatible PyTorch dtype that can handle it without loss.
|
||||
if value.dtype in [np.uint16, np.uint32]:
|
||||
value = value.astype(np.int64)
|
||||
|
||||
elif isinstance(value, (np.number, np.ndarray)) and np.issubdtype(value.dtype, np.floating):
|
||||
default_dtype = {"dtype": torch.float32}
|
||||
|
||||
if config.PIL_AVAILABLE and "PIL" in sys.modules:
|
||||
import PIL.Image
|
||||
|
||||
if isinstance(value, PIL.Image.Image):
|
||||
value = np.asarray(value)
|
||||
if value.ndim == 2:
|
||||
value = value[:, :, np.newaxis]
|
||||
|
||||
value = value.transpose((2, 0, 1))
|
||||
if config.TORCHVISION_AVAILABLE and "torchvision" in sys.modules:
|
||||
try:
|
||||
from torchvision.io import VideoReader
|
||||
|
||||
if isinstance(value, VideoReader):
|
||||
return value # TODO(QL): set output to torch tensors ?
|
||||
except ImportError:
|
||||
pass
|
||||
if config.TORCHCODEC_AVAILABLE and "torchcodec" in sys.modules:
|
||||
from torchcodec.decoders import AudioDecoder, VideoDecoder
|
||||
|
||||
if isinstance(value, (VideoDecoder, AudioDecoder)):
|
||||
return value # TODO(QL): set output to jax arrays ?
|
||||
|
||||
return torch.tensor(value, **{**default_dtype, **self.torch_tensor_kwargs})
|
||||
|
||||
def _recursive_tensorize(self, data_struct):
|
||||
import torch
|
||||
|
||||
# support for torch, tf, jax etc.
|
||||
if hasattr(data_struct, "__array__") and not isinstance(data_struct, torch.Tensor):
|
||||
data_struct = data_struct.__array__()
|
||||
# support for nested types like struct of list of struct
|
||||
if isinstance(data_struct, np.ndarray):
|
||||
if data_struct.dtype == object: # torch tensors cannot be instantied from an array of objects
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
elif isinstance(data_struct, (list, tuple)):
|
||||
return self._consolidate([self.recursive_tensorize(substruct) for substruct in data_struct])
|
||||
return self._tensorize(data_struct)
|
||||
|
||||
def recursive_tensorize(self, data_struct: dict):
|
||||
return map_nested(self._recursive_tensorize, data_struct, map_list=False)
|
||||
|
||||
def format_row(self, pa_table: pa.Table) -> Mapping:
|
||||
row = self.numpy_arrow_extractor().extract_row(pa_table)
|
||||
row = self.python_features_decoder.decode_row(row)
|
||||
return self.recursive_tensorize(row)
|
||||
|
||||
def format_column(self, pa_table: pa.Table) -> "torch.Tensor":
|
||||
column = self.numpy_arrow_extractor().extract_column(pa_table)
|
||||
column = self.python_features_decoder.decode_column(column, pa_table.column_names[0])
|
||||
column = self.recursive_tensorize(column)
|
||||
column = self._consolidate(column)
|
||||
return column
|
||||
|
||||
def format_batch(self, pa_table: pa.Table) -> Mapping:
|
||||
batch = self.numpy_arrow_extractor().extract_batch(pa_table)
|
||||
batch = self.python_features_decoder.decode_batch(batch)
|
||||
batch = self.recursive_tensorize(batch)
|
||||
for column_name in batch:
|
||||
batch[column_name] = self._consolidate(batch[column_name])
|
||||
return batch
|
||||
@@ -0,0 +1,135 @@
|
||||
from itertools import chain
|
||||
from typing import Optional, Union
|
||||
|
||||
from huggingface_hub import (
|
||||
CommitInfo,
|
||||
CommitOperationAdd,
|
||||
CommitOperationDelete,
|
||||
DatasetCard,
|
||||
DatasetCardData,
|
||||
HfApi,
|
||||
HfFileSystem,
|
||||
)
|
||||
|
||||
import datasets.config
|
||||
from datasets import __version__
|
||||
from datasets.info import DatasetInfosDict
|
||||
from datasets.load import load_dataset_builder
|
||||
from datasets.utils.metadata import MetadataConfigs
|
||||
|
||||
|
||||
def delete_from_hub(
|
||||
repo_id: str,
|
||||
config_name: str,
|
||||
revision: Optional[str] = None,
|
||||
token: Optional[Union[bool, str]] = None,
|
||||
) -> CommitInfo:
|
||||
"""Delete a dataset configuration from a [data-only dataset](repository_structure) on the Hub.
|
||||
|
||||
Args:
|
||||
repo_id (`str`): ID of the Hub dataset repository, in the following format: `<user>/<dataset_name>` or
|
||||
`<org>/<dataset_name>`.
|
||||
config_name (`str`): Name of the dataset configuration.
|
||||
revision (`str`, *optional*): Branch to delete the configuration from. Defaults to the `"main"` branch.
|
||||
token (`bool` or `str`, *optional*): Authentication token for the Hugging Face Hub.
|
||||
|
||||
Returns:
|
||||
`huggingface_hub.CommitInfo`
|
||||
"""
|
||||
operations = []
|
||||
# data_files
|
||||
fs = HfFileSystem(endpoint=datasets.config.HF_ENDPOINT, token=token)
|
||||
builder = load_dataset_builder(repo_id, config_name, revision=revision, token=token)
|
||||
for data_file in chain(*builder.config.data_files.values()):
|
||||
data_file_resolved_path = fs.resolve_path(data_file)
|
||||
if data_file_resolved_path.repo_id == repo_id:
|
||||
operations.append(CommitOperationDelete(path_in_repo=data_file_resolved_path.path_in_repo))
|
||||
# README.md
|
||||
dataset_card = DatasetCard.load(repo_id)
|
||||
# config_names
|
||||
if dataset_card.data.get("config_names", None) and config_name in dataset_card.data["config_names"]:
|
||||
dataset_card.data["config_names"].remove(config_name)
|
||||
# metadata_configs
|
||||
metadata_configs = MetadataConfigs.from_dataset_card_data(dataset_card.data)
|
||||
if metadata_configs:
|
||||
_ = metadata_configs.pop(config_name, None)
|
||||
dataset_card_data = DatasetCardData()
|
||||
metadata_configs.to_dataset_card_data(dataset_card_data)
|
||||
if datasets.config.METADATA_CONFIGS_FIELD in dataset_card_data:
|
||||
dataset_card.data[datasets.config.METADATA_CONFIGS_FIELD] = dataset_card_data[
|
||||
datasets.config.METADATA_CONFIGS_FIELD
|
||||
]
|
||||
else:
|
||||
_ = dataset_card.data.pop(datasets.config.METADATA_CONFIGS_FIELD, None)
|
||||
# dataset_info
|
||||
dataset_infos: DatasetInfosDict = DatasetInfosDict.from_dataset_card_data(dataset_card.data)
|
||||
if dataset_infos:
|
||||
_ = dataset_infos.pop(config_name, None)
|
||||
dataset_card_data = DatasetCardData()
|
||||
dataset_infos.to_dataset_card_data(dataset_card_data)
|
||||
if "dataset_info" in dataset_card_data:
|
||||
dataset_card.data["dataset_info"] = dataset_card_data["dataset_info"]
|
||||
else:
|
||||
_ = dataset_card.data.pop("dataset_info", None)
|
||||
# Commit
|
||||
operations.append(
|
||||
CommitOperationAdd(path_in_repo=datasets.config.REPOCARD_FILENAME, path_or_fileobj=str(dataset_card).encode())
|
||||
)
|
||||
api = HfApi(
|
||||
endpoint=datasets.config.HF_ENDPOINT,
|
||||
token=token,
|
||||
library_name="datasets",
|
||||
library_version=__version__,
|
||||
)
|
||||
commit_info = api.create_commit(
|
||||
repo_id,
|
||||
operations=operations,
|
||||
commit_message=f"Delete '{config_name}' config",
|
||||
commit_description=f"Delete '{config_name}' config.",
|
||||
token=token,
|
||||
repo_type="dataset",
|
||||
revision=revision,
|
||||
create_pr=True,
|
||||
)
|
||||
print(f"You can find your PR to delete the dataset config at: {commit_info.pr_url}")
|
||||
return commit_info
|
||||
|
||||
|
||||
def _delete_files(dataset_id, revision=None, token=None):
|
||||
hf_api = HfApi(
|
||||
endpoint=datasets.config.HF_ENDPOINT,
|
||||
token=token,
|
||||
library_name="datasets",
|
||||
library_version=__version__,
|
||||
)
|
||||
repo_files = hf_api.list_repo_files(
|
||||
dataset_id,
|
||||
repo_type="dataset",
|
||||
)
|
||||
if repo_files:
|
||||
legacy_json_file = []
|
||||
data_files = []
|
||||
for filename in repo_files:
|
||||
if filename in {".gitattributes", "README.md"}:
|
||||
continue
|
||||
elif filename == "dataset_infos.json":
|
||||
legacy_json_file.append(filename)
|
||||
else:
|
||||
data_files.append(filename)
|
||||
if legacy_json_file:
|
||||
hf_api.delete_file(
|
||||
"dataset_infos.json",
|
||||
dataset_id,
|
||||
repo_type="dataset",
|
||||
revision=revision,
|
||||
commit_message="Delete legacy dataset_infos.json",
|
||||
)
|
||||
if data_files:
|
||||
for filename in data_files:
|
||||
hf_api.delete_file(
|
||||
filename,
|
||||
dataset_id,
|
||||
repo_type="dataset",
|
||||
revision=revision,
|
||||
commit_message="Delete data file",
|
||||
)
|
||||
@@ -0,0 +1,440 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors and the TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
"""DatasetInfo record information we know about a dataset.
|
||||
|
||||
This includes things that we know about the dataset statically, i.e.:
|
||||
- description
|
||||
- canonical location
|
||||
- does it have validation and tests splits
|
||||
- size
|
||||
- etc.
|
||||
|
||||
This also includes the things that can and should be computed once we've
|
||||
processed the dataset as well:
|
||||
- number of examples (in each split)
|
||||
- etc.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Optional, Union
|
||||
|
||||
import fsspec
|
||||
from fsspec.core import url_to_fs
|
||||
from huggingface_hub import DatasetCard, DatasetCardData
|
||||
|
||||
from . import config
|
||||
from .features import Features
|
||||
from .splits import SplitDict
|
||||
from .utils import Version
|
||||
from .utils.logging import get_logger
|
||||
from .utils.py_utils import asdict, unique_values
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupervisedKeysData:
|
||||
input: str = ""
|
||||
output: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadChecksumsEntryData:
|
||||
key: str = ""
|
||||
value: str = ""
|
||||
|
||||
|
||||
class MissingCachedSizesConfigError(Exception):
|
||||
"""The expected cached sizes of the download file are missing."""
|
||||
|
||||
|
||||
class NonMatchingCachedSizesError(Exception):
|
||||
"""The prepared split doesn't have expected sizes."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PostProcessedInfo:
|
||||
features: Optional[Features] = None
|
||||
resources_checksums: Optional[dict] = None
|
||||
|
||||
def __post_init__(self):
|
||||
# Convert back to the correct classes when we reload from dict
|
||||
if self.features is not None and not isinstance(self.features, Features):
|
||||
self.features = Features.from_dict(self.features)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, post_processed_info_dict: dict) -> "PostProcessedInfo":
|
||||
field_names = {f.name for f in dataclasses.fields(cls)}
|
||||
return cls(**{k: v for k, v in post_processed_info_dict.items() if k in field_names})
|
||||
|
||||
|
||||
@dataclass
|
||||
class DatasetInfo:
|
||||
"""Information about a dataset.
|
||||
|
||||
`DatasetInfo` documents datasets, including its name, version, and features.
|
||||
See the constructor arguments and properties for a full list.
|
||||
|
||||
Not all fields are known on construction and may be updated later.
|
||||
|
||||
Attributes:
|
||||
description (`str`):
|
||||
A description of the dataset.
|
||||
citation (`str`):
|
||||
A BibTeX citation of the dataset.
|
||||
homepage (`str`):
|
||||
A URL to the official homepage for the dataset.
|
||||
license (`str`):
|
||||
The dataset's license. It can be the name of the license or a paragraph containing the terms of the license.
|
||||
features ([`Features`], *optional*):
|
||||
The features used to specify the dataset's column types.
|
||||
post_processed (`PostProcessedInfo`, *optional*):
|
||||
Deprecated. Information regarding the resources of a possible post-processing of a dataset. For example, it can contain the information of an index.
|
||||
supervised_keys (`SupervisedKeysData`, *optional*):
|
||||
Specifies the input feature and the label for supervised learning if applicable for the dataset (legacy from TFDS).
|
||||
builder_name (`str`, *optional*):
|
||||
The name of the `GeneratorBasedBuilder` subclass used to create the dataset. It is also the snake_case version of the dataset builder class name.
|
||||
config_name (`str`, *optional*):
|
||||
The name of the configuration derived from [`BuilderConfig`].
|
||||
version (`str` or [`Version`], *optional*):
|
||||
The version of the dataset.
|
||||
splits (`dict`, *optional*):
|
||||
The mapping between split name and metadata.
|
||||
download_checksums (`dict`, *optional*):
|
||||
The mapping between the URL to download the dataset's checksums and corresponding metadata.
|
||||
download_size (`int`, *optional*):
|
||||
The size of the files to download to generate the dataset, in bytes.
|
||||
post_processing_size (`int`, *optional*):
|
||||
Deprecated. Size of the dataset in bytes after post-processing, if any.
|
||||
dataset_size (`int`, *optional*):
|
||||
The combined size in bytes of the Arrow tables for all splits.
|
||||
size_in_bytes (`int`, *optional*):
|
||||
The combined size in bytes of all files associated with the dataset (downloaded files + Arrow files).
|
||||
**config_kwargs (additional keyword arguments):
|
||||
Keyword arguments to be passed to the [`BuilderConfig`] and used in the [`DatasetBuilder`].
|
||||
"""
|
||||
|
||||
# Set in the dataset builders
|
||||
description: str = dataclasses.field(default_factory=str)
|
||||
citation: str = dataclasses.field(default_factory=str)
|
||||
homepage: str = dataclasses.field(default_factory=str)
|
||||
license: str = dataclasses.field(default_factory=str)
|
||||
features: Optional[Features] = None
|
||||
post_processed: Optional[PostProcessedInfo] = None # kept for bawkard compat
|
||||
supervised_keys: Optional[SupervisedKeysData] = None
|
||||
|
||||
# Set later by the builder
|
||||
builder_name: Optional[str] = None
|
||||
dataset_name: Optional[str] = None # for packaged builders, to be different from builder_name
|
||||
config_name: Optional[str] = None
|
||||
version: Optional[Union[str, Version]] = None
|
||||
# Set later by `download_and_prepare`
|
||||
splits: Optional[SplitDict] = None
|
||||
download_checksums: Optional[dict] = None
|
||||
download_size: Optional[int] = None
|
||||
post_processing_size: Optional[int] = None
|
||||
dataset_size: Optional[int] = None
|
||||
size_in_bytes: Optional[int] = None
|
||||
|
||||
_INCLUDED_INFO_IN_YAML: ClassVar[list[str]] = [
|
||||
"config_name",
|
||||
"download_size",
|
||||
"dataset_size",
|
||||
"features",
|
||||
"splits",
|
||||
]
|
||||
|
||||
def __post_init__(self):
|
||||
# Convert back to the correct classes when we reload from dict
|
||||
if self.features is not None and not isinstance(self.features, Features):
|
||||
self.features = Features.from_dict(self.features)
|
||||
if self.post_processed is not None and not isinstance(self.post_processed, PostProcessedInfo):
|
||||
self.post_processed = PostProcessedInfo.from_dict(self.post_processed)
|
||||
if self.version is not None and not isinstance(self.version, Version):
|
||||
if isinstance(self.version, str):
|
||||
self.version = Version(self.version)
|
||||
else:
|
||||
self.version = Version.from_dict(self.version)
|
||||
if self.splits is not None and not isinstance(self.splits, SplitDict):
|
||||
self.splits = SplitDict.from_split_dict(self.splits)
|
||||
if self.supervised_keys is not None and not isinstance(self.supervised_keys, SupervisedKeysData):
|
||||
if isinstance(self.supervised_keys, (tuple, list)):
|
||||
self.supervised_keys = SupervisedKeysData(*self.supervised_keys)
|
||||
else:
|
||||
self.supervised_keys = SupervisedKeysData(**self.supervised_keys)
|
||||
|
||||
def write_to_directory(self, dataset_info_dir, pretty_print=False, storage_options: Optional[dict] = None):
|
||||
"""Write `DatasetInfo` and license (if present) as JSON files to `dataset_info_dir`.
|
||||
|
||||
Args:
|
||||
dataset_info_dir (`str`):
|
||||
Destination directory.
|
||||
pretty_print (`bool`, defaults to `False`):
|
||||
If `True`, the JSON will be pretty-printed with the indent level of 4.
|
||||
storage_options (`dict`, *optional*):
|
||||
Key/value pairs to be passed on to the file-system backend, if any.
|
||||
|
||||
<Added version="2.9.0"/>
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import load_dataset
|
||||
>>> ds = load_dataset("cornell-movie-review-data/rotten_tomatoes", split="validation")
|
||||
>>> ds.info.write_to_directory("/path/to/directory/")
|
||||
```
|
||||
"""
|
||||
fs: fsspec.AbstractFileSystem
|
||||
fs, *_ = url_to_fs(dataset_info_dir, **(storage_options or {}))
|
||||
with fs.open(posixpath.join(dataset_info_dir, config.DATASET_INFO_FILENAME), "wb") as f:
|
||||
self._dump_info(f, pretty_print=pretty_print)
|
||||
if self.license:
|
||||
with fs.open(posixpath.join(dataset_info_dir, config.LICENSE_FILENAME), "wb") as f:
|
||||
self._dump_license(f)
|
||||
|
||||
def _dump_info(self, file, pretty_print=False):
|
||||
"""Dump info in `file` file-like object open in bytes mode (to support remote files)"""
|
||||
file.write(json.dumps(asdict(self), indent=4 if pretty_print else None).encode("utf-8"))
|
||||
|
||||
def _dump_license(self, file):
|
||||
"""Dump license in `file` file-like object open in bytes mode (to support remote files)"""
|
||||
file.write(self.license.encode("utf-8"))
|
||||
|
||||
@classmethod
|
||||
def from_merge(cls, dataset_infos: list["DatasetInfo"]):
|
||||
dataset_infos = [dset_info.copy() for dset_info in dataset_infos if dset_info is not None]
|
||||
|
||||
if len(dataset_infos) > 0 and all(dataset_infos[0] == dset_info for dset_info in dataset_infos):
|
||||
# if all dataset_infos are equal we don't need to merge. Just return the first.
|
||||
return dataset_infos[0]
|
||||
|
||||
description = "\n\n".join(unique_values(info.description for info in dataset_infos)).strip()
|
||||
citation = "\n\n".join(unique_values(info.citation for info in dataset_infos)).strip()
|
||||
homepage = "\n\n".join(unique_values(info.homepage for info in dataset_infos)).strip()
|
||||
license = "\n\n".join(unique_values(info.license for info in dataset_infos)).strip()
|
||||
features = None
|
||||
supervised_keys = None
|
||||
|
||||
return cls(
|
||||
description=description,
|
||||
citation=citation,
|
||||
homepage=homepage,
|
||||
license=license,
|
||||
features=features,
|
||||
supervised_keys=supervised_keys,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, dataset_info_dir: str, storage_options: Optional[dict] = None) -> "DatasetInfo":
|
||||
"""Create [`DatasetInfo`] from the JSON file in `dataset_info_dir`.
|
||||
|
||||
This function updates all the dynamically generated fields (num_examples,
|
||||
hash, time of creation,...) of the [`DatasetInfo`].
|
||||
|
||||
This will overwrite all previous metadata.
|
||||
|
||||
Args:
|
||||
dataset_info_dir (`str`):
|
||||
The directory containing the metadata file. This
|
||||
should be the root directory of a specific dataset version.
|
||||
storage_options (`dict`, *optional*):
|
||||
Key/value pairs to be passed on to the file-system backend, if any.
|
||||
|
||||
<Added version="2.9.0"/>
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import DatasetInfo
|
||||
>>> ds_info = DatasetInfo.from_directory("/path/to/directory/")
|
||||
```
|
||||
"""
|
||||
fs: fsspec.AbstractFileSystem
|
||||
fs, *_ = url_to_fs(dataset_info_dir, **(storage_options or {}))
|
||||
logger.debug(f"Loading Dataset info from {dataset_info_dir}")
|
||||
if not dataset_info_dir:
|
||||
raise ValueError("Calling DatasetInfo.from_directory() with undefined dataset_info_dir.")
|
||||
with fs.open(posixpath.join(dataset_info_dir, config.DATASET_INFO_FILENAME), "r", encoding="utf-8") as f:
|
||||
dataset_info_dict = json.load(f)
|
||||
return cls.from_dict(dataset_info_dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dataset_info_dict: dict) -> "DatasetInfo":
|
||||
field_names = {f.name for f in dataclasses.fields(cls)}
|
||||
return cls(**{k: v for k, v in dataset_info_dict.items() if k in field_names})
|
||||
|
||||
def update(self, other_dataset_info: "DatasetInfo", ignore_none=True):
|
||||
self_dict = self.__dict__
|
||||
self_dict.update(
|
||||
**{
|
||||
k: copy.deepcopy(v)
|
||||
for k, v in other_dataset_info.__dict__.items()
|
||||
if (v is not None or not ignore_none)
|
||||
}
|
||||
)
|
||||
|
||||
def copy(self) -> "DatasetInfo":
|
||||
return self.__class__(**{k: copy.deepcopy(v) for k, v in self.__dict__.items()})
|
||||
|
||||
def _to_yaml_dict(self) -> dict:
|
||||
yaml_dict = {}
|
||||
dataset_info_dict = asdict(self)
|
||||
for key in dataset_info_dict:
|
||||
if key in self._INCLUDED_INFO_IN_YAML:
|
||||
value = getattr(self, key)
|
||||
if hasattr(value, "_to_yaml_list"): # Features, SplitDict
|
||||
yaml_dict[key] = value._to_yaml_list()
|
||||
elif hasattr(value, "_to_yaml_string"): # Version
|
||||
yaml_dict[key] = value._to_yaml_string()
|
||||
else:
|
||||
yaml_dict[key] = value
|
||||
return yaml_dict
|
||||
|
||||
@classmethod
|
||||
def _from_yaml_dict(cls, yaml_data: dict) -> "DatasetInfo":
|
||||
yaml_data = copy.deepcopy(yaml_data)
|
||||
if yaml_data.get("features") is not None:
|
||||
yaml_data["features"] = Features._from_yaml_list(yaml_data["features"])
|
||||
if yaml_data.get("splits") is not None:
|
||||
yaml_data["splits"] = SplitDict._from_yaml_list(yaml_data["splits"])
|
||||
field_names = {f.name for f in dataclasses.fields(cls)}
|
||||
return cls(**{k: v for k, v in yaml_data.items() if k in field_names})
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
self.__class__.__qualname__
|
||||
+ "("
|
||||
+ ", ".join(
|
||||
[f"{f.name}={repr(getattr(self, f.name))}" for f in dataclasses.fields(self) if getattr(self, f.name)]
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
|
||||
class DatasetInfosDict(dict[str, DatasetInfo]):
|
||||
def write_to_directory(self, dataset_infos_dir, overwrite=False, pretty_print=False) -> None:
|
||||
total_dataset_infos = {}
|
||||
dataset_infos_path = os.path.join(dataset_infos_dir, config.DATASETDICT_INFOS_FILENAME)
|
||||
dataset_readme_path = os.path.join(dataset_infos_dir, config.REPOCARD_FILENAME)
|
||||
if not overwrite:
|
||||
total_dataset_infos = self.from_directory(dataset_infos_dir)
|
||||
total_dataset_infos.update(self)
|
||||
if os.path.exists(dataset_infos_path):
|
||||
# for backward compatibility, let's update the JSON file if it exists
|
||||
with open(dataset_infos_path, "w", encoding="utf-8") as f:
|
||||
dataset_infos_dict = {
|
||||
config_name: asdict(dset_info) for config_name, dset_info in total_dataset_infos.items()
|
||||
}
|
||||
json.dump(dataset_infos_dict, f, indent=4 if pretty_print else None)
|
||||
# Dump the infos in the YAML part of the README.md file
|
||||
if os.path.exists(dataset_readme_path):
|
||||
dataset_card = DatasetCard.load(dataset_readme_path)
|
||||
dataset_card_data = dataset_card.data
|
||||
else:
|
||||
dataset_card = None
|
||||
dataset_card_data = DatasetCardData()
|
||||
if total_dataset_infos:
|
||||
total_dataset_infos.to_dataset_card_data(dataset_card_data)
|
||||
dataset_card = (
|
||||
DatasetCard("---\n" + str(dataset_card_data) + "\n---\n") if dataset_card is None else dataset_card
|
||||
)
|
||||
dataset_card.save(Path(dataset_readme_path))
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, dataset_infos_dir) -> "DatasetInfosDict":
|
||||
logger.debug(f"Loading Dataset Infos from {dataset_infos_dir}")
|
||||
# Load the info from the YAML part of README.md
|
||||
if os.path.exists(os.path.join(dataset_infos_dir, config.REPOCARD_FILENAME)):
|
||||
dataset_card_data = DatasetCard.load(Path(dataset_infos_dir) / config.REPOCARD_FILENAME).data
|
||||
if "dataset_info" in dataset_card_data:
|
||||
return cls.from_dataset_card_data(dataset_card_data)
|
||||
if os.path.exists(os.path.join(dataset_infos_dir, config.DATASETDICT_INFOS_FILENAME)):
|
||||
# this is just to have backward compatibility with dataset_infos.json files
|
||||
with open(os.path.join(dataset_infos_dir, config.DATASETDICT_INFOS_FILENAME), encoding="utf-8") as f:
|
||||
return cls(
|
||||
{
|
||||
config_name: DatasetInfo.from_dict(dataset_info_dict)
|
||||
for config_name, dataset_info_dict in json.load(f).items()
|
||||
}
|
||||
)
|
||||
else:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_dataset_card_data(cls, dataset_card_data: DatasetCardData) -> "DatasetInfosDict":
|
||||
if isinstance(dataset_card_data.get("dataset_info"), (list, dict)):
|
||||
if isinstance(dataset_card_data["dataset_info"], list):
|
||||
return cls(
|
||||
{
|
||||
dataset_info_yaml_dict.get("config_name", "default"): DatasetInfo._from_yaml_dict(
|
||||
dataset_info_yaml_dict
|
||||
)
|
||||
for dataset_info_yaml_dict in dataset_card_data["dataset_info"]
|
||||
}
|
||||
)
|
||||
else:
|
||||
dataset_info = DatasetInfo._from_yaml_dict(dataset_card_data["dataset_info"])
|
||||
dataset_info.config_name = dataset_card_data["dataset_info"].get("config_name", "default")
|
||||
return cls({dataset_info.config_name: dataset_info})
|
||||
else:
|
||||
return cls()
|
||||
|
||||
def to_dataset_card_data(self, dataset_card_data: DatasetCardData) -> None:
|
||||
if self:
|
||||
# first get existing metadata info
|
||||
if "dataset_info" in dataset_card_data and isinstance(dataset_card_data["dataset_info"], dict):
|
||||
dataset_metadata_infos = {
|
||||
dataset_card_data["dataset_info"].get("config_name", "default"): dataset_card_data["dataset_info"]
|
||||
}
|
||||
elif "dataset_info" in dataset_card_data and isinstance(dataset_card_data["dataset_info"], list):
|
||||
dataset_metadata_infos = {
|
||||
config_metadata["config_name"]: config_metadata
|
||||
for config_metadata in dataset_card_data["dataset_info"]
|
||||
}
|
||||
else:
|
||||
dataset_metadata_infos = {}
|
||||
# update/rewrite existing metadata info with the one to dump
|
||||
total_dataset_infos = {
|
||||
**dataset_metadata_infos,
|
||||
**{config_name: dset_info._to_yaml_dict() for config_name, dset_info in self.items()},
|
||||
}
|
||||
# the config_name from the dataset_infos_dict takes over the config_name of the DatasetInfo
|
||||
for config_name, dset_info_yaml_dict in total_dataset_infos.items():
|
||||
dset_info_yaml_dict["config_name"] = config_name
|
||||
if len(total_dataset_infos) == 1:
|
||||
# use a struct instead of a list of configurations, since there's only one
|
||||
dataset_card_data["dataset_info"] = next(iter(total_dataset_infos.values()))
|
||||
config_name = dataset_card_data["dataset_info"].pop("config_name", None)
|
||||
if config_name != "default":
|
||||
# if config_name is not "default" preserve it and put at the first position
|
||||
dataset_card_data["dataset_info"] = {
|
||||
"config_name": config_name,
|
||||
**dataset_card_data["dataset_info"],
|
||||
}
|
||||
else:
|
||||
dataset_card_data["dataset_info"] = []
|
||||
for config_name, dataset_info_yaml_dict in sorted(total_dataset_infos.items()):
|
||||
# add the config_name field in first position
|
||||
dataset_info_yaml_dict.pop("config_name", None)
|
||||
dataset_info_yaml_dict = {"config_name": config_name, **dataset_info_yaml_dict}
|
||||
dataset_card_data["dataset_info"].append(dataset_info_yaml_dict)
|
||||
@@ -0,0 +1,350 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
"""List and inspect datasets."""
|
||||
|
||||
import os
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Optional, Union
|
||||
|
||||
from .download.download_config import DownloadConfig
|
||||
from .download.download_manager import DownloadMode
|
||||
from .download.streaming_download_manager import StreamingDownloadManager
|
||||
from .info import DatasetInfo
|
||||
from .load import (
|
||||
dataset_module_factory,
|
||||
get_dataset_builder_class,
|
||||
load_dataset_builder,
|
||||
)
|
||||
from .utils.logging import get_logger
|
||||
from .utils.version import Version
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SplitsNotFoundError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def get_dataset_infos(
|
||||
path: str,
|
||||
data_files: Optional[Union[dict, list, str]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
download_mode: Optional[Union[DownloadMode, str]] = None,
|
||||
revision: Optional[Union[str, Version]] = None,
|
||||
token: Optional[Union[bool, str]] = None,
|
||||
**config_kwargs,
|
||||
):
|
||||
"""Get the meta information about a dataset, returned as a dict mapping config name to DatasetInfoDict.
|
||||
|
||||
Args:
|
||||
path (`str`): path to the dataset repository. Can be either:
|
||||
|
||||
- a local path to the dataset directory containing the data files,
|
||||
e.g. `'./dataset/squad'`
|
||||
- a dataset identifier on the Hugging Face Hub (list all available datasets and ids with [`huggingface_hub.list_datasets`]),
|
||||
e.g. `'rajpurkar/squad'`, `'nyu-mll/glue'` or``'openai/webtext'`
|
||||
revision (`Union[str, datasets.Version]`, *optional*):
|
||||
If specified, the dataset module will be loaded from the datasets repository at this version.
|
||||
By default:
|
||||
- it is set to the local version of the lib.
|
||||
- it will also try to load it from the main branch if it's not available at the local version of the lib.
|
||||
Specifying a version that is different from your local version of the lib might cause compatibility issues.
|
||||
download_config ([`DownloadConfig`], *optional*):
|
||||
Specific download configuration parameters.
|
||||
download_mode ([`DownloadMode`] or `str`, defaults to `REUSE_DATASET_IF_EXISTS`):
|
||||
Download/generate mode.
|
||||
data_files (`Union[Dict, List, str]`, *optional*):
|
||||
Defining the data_files of the dataset configuration.
|
||||
token (`str` or `bool`, *optional*):
|
||||
Optional string or boolean to use as Bearer token for remote files on the Datasets Hub.
|
||||
If `True`, or not specified, will get token from `"~/.huggingface"`.
|
||||
**config_kwargs (additional keyword arguments):
|
||||
Optional attributes for builder class which will override the attributes if supplied.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import get_dataset_infos
|
||||
>>> get_dataset_infos('cornell-movie-review-data/rotten_tomatoes')
|
||||
{'default': DatasetInfo(description="Movie Review Dataset.\nThis is a dataset of containing 5,331 positive and 5,331 negative processed\nsentences from Rotten Tomatoes movie reviews...), ...}
|
||||
```
|
||||
"""
|
||||
config_names = get_dataset_config_names(
|
||||
path=path,
|
||||
revision=revision,
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
data_files=data_files,
|
||||
token=token,
|
||||
)
|
||||
return {
|
||||
config_name: get_dataset_config_info(
|
||||
path=path,
|
||||
config_name=config_name,
|
||||
data_files=data_files,
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
revision=revision,
|
||||
token=token,
|
||||
**config_kwargs,
|
||||
)
|
||||
for config_name in config_names
|
||||
}
|
||||
|
||||
|
||||
def get_dataset_config_names(
|
||||
path: str,
|
||||
revision: Optional[Union[str, Version]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
download_mode: Optional[Union[DownloadMode, str]] = None,
|
||||
data_files: Optional[Union[dict, list, str]] = None,
|
||||
**download_kwargs,
|
||||
):
|
||||
"""Get the list of available config names for a particular dataset.
|
||||
|
||||
Args:
|
||||
path (`str`): path to the dataset repository. Can be either:
|
||||
|
||||
- a local path to the dataset directory containing the data files,
|
||||
e.g. `'./dataset/squad'`
|
||||
- a dataset identifier on the Hugging Face Hub (list all available datasets and ids with [`huggingface_hub.list_datasets`]),
|
||||
e.g. `'rajpurkar/squad'`, `'nyu-mll/glue'` or``'openai/webtext'`
|
||||
revision (`Union[str, datasets.Version]`, *optional*):
|
||||
If specified, the dataset module will be loaded from the datasets repository at this version.
|
||||
By default:
|
||||
- it is set to the local version of the lib.
|
||||
- it will also try to load it from the main branch if it's not available at the local version of the lib.
|
||||
Specifying a version that is different from your local version of the lib might cause compatibility issues.
|
||||
download_config ([`DownloadConfig`], *optional*):
|
||||
Specific download configuration parameters.
|
||||
download_mode ([`DownloadMode`] or `str`, defaults to `REUSE_DATASET_IF_EXISTS`):
|
||||
Download/generate mode.
|
||||
data_files (`Union[Dict, List, str]`, *optional*):
|
||||
Defining the data_files of the dataset configuration.
|
||||
**download_kwargs (additional keyword arguments):
|
||||
Optional attributes for [`DownloadConfig`] which will override the attributes in `download_config` if supplied,
|
||||
for example `token`.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import get_dataset_config_names
|
||||
>>> get_dataset_config_names("nyu-mll/glue")
|
||||
['cola',
|
||||
'sst2',
|
||||
'mrpc',
|
||||
'qqp',
|
||||
'stsb',
|
||||
'mnli',
|
||||
'mnli_mismatched',
|
||||
'mnli_matched',
|
||||
'qnli',
|
||||
'rte',
|
||||
'wnli',
|
||||
'ax']
|
||||
```
|
||||
"""
|
||||
dataset_module = dataset_module_factory(
|
||||
path,
|
||||
revision=revision,
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
data_files=data_files,
|
||||
**download_kwargs,
|
||||
)
|
||||
builder_cls = get_dataset_builder_class(dataset_module, dataset_name=os.path.basename(path))
|
||||
return list(builder_cls.builder_configs.keys()) or [
|
||||
dataset_module.builder_kwargs.get("config_name", builder_cls.DEFAULT_CONFIG_NAME or "default")
|
||||
]
|
||||
|
||||
|
||||
def get_dataset_default_config_name(
|
||||
path: str,
|
||||
revision: Optional[Union[str, Version]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
download_mode: Optional[Union[DownloadMode, str]] = None,
|
||||
data_files: Optional[Union[dict, list, str]] = None,
|
||||
**download_kwargs,
|
||||
) -> Optional[str]:
|
||||
"""Get the default config name for a particular dataset.
|
||||
Can return None only if the dataset has multiple configurations and no default configuration.
|
||||
|
||||
Args:
|
||||
path (`str`): path to the dataset repository. Can be either:
|
||||
|
||||
- a local path to the dataset directory containing the data files,
|
||||
e.g. `'./dataset/squad'`
|
||||
- a dataset identifier on the Hugging Face Hub (list all available datasets and ids with [`huggingface_hub.list_datasets`]),
|
||||
e.g. `'rajpurkar/squad'`, `'nyu-mll/glue'` or``'openai/webtext'`
|
||||
revision (`Union[str, datasets.Version]`, *optional*):
|
||||
If specified, the dataset module will be loaded from the datasets repository at this version.
|
||||
By default:
|
||||
- it is set to the local version of the lib.
|
||||
- it will also try to load it from the main branch if it's not available at the local version of the lib.
|
||||
Specifying a version that is different from your local version of the lib might cause compatibility issues.
|
||||
download_config ([`DownloadConfig`], *optional*):
|
||||
Specific download configuration parameters.
|
||||
download_mode ([`DownloadMode`] or `str`, defaults to `REUSE_DATASET_IF_EXISTS`):
|
||||
Download/generate mode.
|
||||
data_files (`Union[Dict, List, str]`, *optional*):
|
||||
Defining the data_files of the dataset configuration.
|
||||
**download_kwargs (additional keyword arguments):
|
||||
Optional attributes for [`DownloadConfig`] which will override the attributes in `download_config` if supplied,
|
||||
for example `token`.
|
||||
|
||||
Returns:
|
||||
Optional[str]: the default config name if there is one
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import get_dataset_default_config_name
|
||||
>>> get_dataset_default_config_name("openbookqa")
|
||||
'main'
|
||||
```
|
||||
"""
|
||||
dataset_module = dataset_module_factory(
|
||||
path,
|
||||
revision=revision,
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
data_files=data_files,
|
||||
**download_kwargs,
|
||||
)
|
||||
builder_cls = get_dataset_builder_class(dataset_module, dataset_name=os.path.basename(path))
|
||||
builder_configs = list(builder_cls.builder_configs.keys())
|
||||
if builder_configs:
|
||||
default_config_name = builder_configs[0] if len(builder_configs) == 1 else None
|
||||
else:
|
||||
default_config_name = "default"
|
||||
return builder_cls.DEFAULT_CONFIG_NAME or default_config_name
|
||||
|
||||
|
||||
def get_dataset_config_info(
|
||||
path: str,
|
||||
config_name: Optional[str] = None,
|
||||
data_files: Optional[Union[str, Sequence[str], Mapping[str, Union[str, Sequence[str]]]]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
download_mode: Optional[Union[DownloadMode, str]] = None,
|
||||
revision: Optional[Union[str, Version]] = None,
|
||||
token: Optional[Union[bool, str]] = None,
|
||||
**config_kwargs,
|
||||
) -> DatasetInfo:
|
||||
"""Get the meta information (DatasetInfo) about a dataset for a particular config
|
||||
|
||||
Args:
|
||||
path (`str`): path to the dataset repository. Can be either:
|
||||
|
||||
- a local path to the dataset directory containing the data files,
|
||||
e.g. `'./dataset/squad'`
|
||||
- a dataset identifier on the Hugging Face Hub (list all available datasets and ids with [`huggingface_hub.list_datasets`]),
|
||||
e.g. `'rajpurkar/squad'`, `'nyu-mll/glue'` or``'openai/webtext'`
|
||||
config_name (:obj:`str`, optional): Defining the name of the dataset configuration.
|
||||
data_files (:obj:`str` or :obj:`Sequence` or :obj:`Mapping`, optional): Path(s) to source data file(s).
|
||||
download_config (:class:`~download.DownloadConfig`, optional): Specific download configuration parameters.
|
||||
download_mode (:class:`DownloadMode` or :obj:`str`, default ``REUSE_DATASET_IF_EXISTS``): Download/generate mode.
|
||||
revision (:class:`~utils.Version` or :obj:`str`, optional): Version of the dataset to load.
|
||||
As datasets have their own git repository on the Datasets Hub, the default version "main" corresponds to their "main" branch.
|
||||
You can specify a different version than the default "main" by using a commit SHA or a git tag of the dataset repository.
|
||||
token (``str`` or :obj:`bool`, optional): Optional string or boolean to use as Bearer token for remote files on the Datasets Hub.
|
||||
If True, or not specified, will get token from `"~/.huggingface"`.
|
||||
**config_kwargs (additional keyword arguments): optional attributes for builder class which will override the attributes if supplied.
|
||||
|
||||
"""
|
||||
builder = load_dataset_builder(
|
||||
path,
|
||||
name=config_name,
|
||||
data_files=data_files,
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
revision=revision,
|
||||
token=token,
|
||||
**config_kwargs,
|
||||
)
|
||||
info = builder.info
|
||||
if info.splits is None:
|
||||
download_config = download_config.copy() if download_config else DownloadConfig()
|
||||
if token is not None:
|
||||
download_config.token = token
|
||||
try:
|
||||
info.splits = {
|
||||
split_generator.name: {"name": split_generator.name, "dataset_name": path}
|
||||
for split_generator in builder._split_generators(
|
||||
StreamingDownloadManager(base_path=builder.base_path, download_config=download_config)
|
||||
)
|
||||
}
|
||||
except Exception as err:
|
||||
raise SplitsNotFoundError("The split names could not be parsed from the dataset config.") from err
|
||||
return info
|
||||
|
||||
|
||||
def get_dataset_split_names(
|
||||
path: str,
|
||||
config_name: Optional[str] = None,
|
||||
data_files: Optional[Union[str, Sequence[str], Mapping[str, Union[str, Sequence[str]]]]] = None,
|
||||
download_config: Optional[DownloadConfig] = None,
|
||||
download_mode: Optional[Union[DownloadMode, str]] = None,
|
||||
revision: Optional[Union[str, Version]] = None,
|
||||
token: Optional[Union[bool, str]] = None,
|
||||
**config_kwargs,
|
||||
):
|
||||
"""Get the list of available splits for a particular config and dataset.
|
||||
|
||||
Args:
|
||||
path (`str`): path to the dataset repository. Can be either:
|
||||
|
||||
- a local path to the dataset directory containing the data files,
|
||||
e.g. `'./dataset/squad'`
|
||||
- a dataset identifier on the Hugging Face Hub (list all available datasets and ids with [`huggingface_hub.list_datasets`]),
|
||||
e.g. `'rajpurkar/squad'`, `'nyu-mll/glue'` or``'openai/webtext'`
|
||||
config_name (`str`, *optional*):
|
||||
Defining the name of the dataset configuration.
|
||||
data_files (`str` or `Sequence` or `Mapping`, *optional*):
|
||||
Path(s) to source data file(s).
|
||||
download_config ([`DownloadConfig`], *optional*):
|
||||
Specific download configuration parameters.
|
||||
download_mode ([`DownloadMode`] or `str`, defaults to `REUSE_DATASET_IF_EXISTS`):
|
||||
Download/generate mode.
|
||||
revision ([`Version`] or `str`, *optional*):
|
||||
Version of the dataset to load.
|
||||
As datasets have their own git repository on the Datasets Hub, the default version "main" corresponds to their "main" branch.
|
||||
You can specify a different version than the default "main" by using a commit SHA or a git tag of the dataset repository.
|
||||
token (`str` or `bool`, *optional*):
|
||||
Optional string or boolean to use as Bearer token for remote files on the Datasets Hub.
|
||||
If `True`, or not specified, will get token from `"~/.huggingface"`.
|
||||
**config_kwargs (additional keyword arguments):
|
||||
Optional attributes for builder class which will override the attributes if supplied.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
>>> from datasets import get_dataset_split_names
|
||||
>>> get_dataset_split_names('cornell-movie-review-data/rotten_tomatoes')
|
||||
['train', 'validation', 'test']
|
||||
```
|
||||
"""
|
||||
info = get_dataset_config_info(
|
||||
path,
|
||||
config_name=config_name,
|
||||
data_files=data_files,
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
revision=revision,
|
||||
token=token,
|
||||
**config_kwargs,
|
||||
)
|
||||
return list(info.splits.keys())
|
||||
@@ -0,0 +1,53 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Union
|
||||
|
||||
from .. import Dataset, DatasetDict, Features, IterableDataset, IterableDatasetDict, NamedSplit
|
||||
from ..utils.typing import NestedDataStructureLike, PathLike
|
||||
|
||||
|
||||
class AbstractDatasetReader(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
path_or_paths: Optional[NestedDataStructureLike[PathLike]] = None,
|
||||
split: Optional[NamedSplit] = None,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.path_or_paths = path_or_paths
|
||||
self.split = split if split or isinstance(path_or_paths, dict) else "train"
|
||||
self.features = features
|
||||
self.cache_dir = cache_dir
|
||||
self.keep_in_memory = keep_in_memory
|
||||
self.streaming = streaming
|
||||
self.num_proc = num_proc
|
||||
self.kwargs = kwargs
|
||||
|
||||
@abstractmethod
|
||||
def read(self) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]:
|
||||
pass
|
||||
|
||||
|
||||
class AbstractDatasetInputStream(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.features = features
|
||||
self.cache_dir = cache_dir
|
||||
self.keep_in_memory = keep_in_memory
|
||||
self.streaming = streaming
|
||||
self.num_proc = num_proc
|
||||
self.kwargs = kwargs
|
||||
|
||||
@abstractmethod
|
||||
def read(self) -> Union[Dataset, IterableDataset]:
|
||||
pass
|
||||
@@ -0,0 +1,143 @@
|
||||
import multiprocessing
|
||||
import os
|
||||
from typing import BinaryIO, Optional, Union
|
||||
|
||||
import fsspec
|
||||
|
||||
from .. import Dataset, Features, NamedSplit, config
|
||||
from ..formatting import query_table
|
||||
from ..packaged_modules.csv.csv import Csv
|
||||
from ..utils import tqdm as hf_tqdm
|
||||
from ..utils.typing import NestedDataStructureLike, PathLike
|
||||
from .abc import AbstractDatasetReader
|
||||
|
||||
|
||||
class CsvDatasetReader(AbstractDatasetReader):
|
||||
def __init__(
|
||||
self,
|
||||
path_or_paths: NestedDataStructureLike[PathLike],
|
||||
split: Optional[NamedSplit] = None,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
path_or_paths,
|
||||
split=split,
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
keep_in_memory=keep_in_memory,
|
||||
streaming=streaming,
|
||||
num_proc=num_proc,
|
||||
**kwargs,
|
||||
)
|
||||
path_or_paths = path_or_paths if isinstance(path_or_paths, dict) else {self.split: path_or_paths}
|
||||
self.builder = Csv(
|
||||
cache_dir=cache_dir,
|
||||
data_files=path_or_paths,
|
||||
features=features,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
# Build iterable dataset
|
||||
if self.streaming:
|
||||
dataset = self.builder.as_streaming_dataset(split=self.split)
|
||||
# Build regular (map-style) dataset
|
||||
else:
|
||||
download_config = None
|
||||
download_mode = None
|
||||
verification_mode = None
|
||||
base_path = None
|
||||
|
||||
self.builder.download_and_prepare(
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
verification_mode=verification_mode,
|
||||
base_path=base_path,
|
||||
num_proc=self.num_proc,
|
||||
)
|
||||
dataset = self.builder.as_dataset(split=self.split, in_memory=self.keep_in_memory)
|
||||
return dataset
|
||||
|
||||
|
||||
class CsvDatasetWriter:
|
||||
def __init__(
|
||||
self,
|
||||
dataset: Dataset,
|
||||
path_or_buf: Union[PathLike, BinaryIO],
|
||||
batch_size: Optional[int] = None,
|
||||
num_proc: Optional[int] = None,
|
||||
storage_options: Optional[dict] = None,
|
||||
**to_csv_kwargs,
|
||||
):
|
||||
if num_proc is not None and num_proc <= 0:
|
||||
raise ValueError(f"num_proc {num_proc} must be an integer > 0.")
|
||||
|
||||
self.dataset = dataset
|
||||
self.path_or_buf = path_or_buf
|
||||
self.batch_size = batch_size if batch_size else config.DEFAULT_MAX_BATCH_SIZE
|
||||
self.num_proc = num_proc
|
||||
self.encoding = "utf-8"
|
||||
self.storage_options = storage_options or {}
|
||||
self.to_csv_kwargs = to_csv_kwargs
|
||||
|
||||
def write(self) -> int:
|
||||
_ = self.to_csv_kwargs.pop("path_or_buf", None)
|
||||
header = self.to_csv_kwargs.pop("header", True)
|
||||
index = self.to_csv_kwargs.pop("index", False)
|
||||
|
||||
if isinstance(self.path_or_buf, (str, bytes, os.PathLike)):
|
||||
with fsspec.open(self.path_or_buf, "wb", **(self.storage_options or {})) as buffer:
|
||||
written = self._write(file_obj=buffer, header=header, index=index, **self.to_csv_kwargs)
|
||||
else:
|
||||
written = self._write(file_obj=self.path_or_buf, header=header, index=index, **self.to_csv_kwargs)
|
||||
return written
|
||||
|
||||
def _batch_csv(self, args):
|
||||
offset, header, index, to_csv_kwargs = args
|
||||
|
||||
batch = query_table(
|
||||
table=self.dataset.data,
|
||||
key=slice(offset, offset + self.batch_size),
|
||||
indices=self.dataset._indices,
|
||||
)
|
||||
csv_str = batch.to_pandas().to_csv(
|
||||
path_or_buf=None, header=header if (offset == 0) else False, index=index, **to_csv_kwargs
|
||||
)
|
||||
return csv_str.encode(self.encoding)
|
||||
|
||||
def _write(self, file_obj: BinaryIO, header, index, **to_csv_kwargs) -> int:
|
||||
"""Writes the pyarrow table as CSV to a binary file handle.
|
||||
|
||||
Caller is responsible for opening and closing the handle.
|
||||
"""
|
||||
written = 0
|
||||
|
||||
if self.num_proc is None or self.num_proc == 1:
|
||||
for offset in hf_tqdm(
|
||||
range(0, len(self.dataset), self.batch_size),
|
||||
unit="ba",
|
||||
desc="Creating CSV from Arrow format",
|
||||
):
|
||||
csv_str = self._batch_csv((offset, header, index, to_csv_kwargs))
|
||||
written += file_obj.write(csv_str)
|
||||
|
||||
else:
|
||||
num_rows, batch_size = len(self.dataset), self.batch_size
|
||||
with multiprocessing.Pool(self.num_proc) as pool:
|
||||
for csv_str in hf_tqdm(
|
||||
pool.imap(
|
||||
self._batch_csv,
|
||||
[(offset, header, index, to_csv_kwargs) for offset in range(0, num_rows, batch_size)],
|
||||
),
|
||||
total=(num_rows // batch_size) + 1 if num_rows % batch_size else num_rows // batch_size,
|
||||
unit="ba",
|
||||
desc="Creating CSV from Arrow format",
|
||||
):
|
||||
written += file_obj.write(csv_str)
|
||||
|
||||
return written
|
||||
@@ -0,0 +1,62 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .. import Features, NamedSplit, Split
|
||||
from ..packaged_modules.generator.generator import Generator
|
||||
from .abc import AbstractDatasetInputStream
|
||||
|
||||
|
||||
class GeneratorDatasetInputStream(AbstractDatasetInputStream):
|
||||
def __init__(
|
||||
self,
|
||||
generator: Callable,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
gen_kwargs: Optional[dict] = None,
|
||||
num_proc: Optional[int] = None,
|
||||
split: NamedSplit = Split.TRAIN,
|
||||
fingerprint: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
keep_in_memory=keep_in_memory,
|
||||
streaming=streaming,
|
||||
num_proc=num_proc,
|
||||
**kwargs,
|
||||
)
|
||||
self.builder = Generator(
|
||||
cache_dir=cache_dir,
|
||||
features=features,
|
||||
generator=generator,
|
||||
gen_kwargs=gen_kwargs,
|
||||
split=split,
|
||||
config_id="default-fingerprint=" + fingerprint if fingerprint else None,
|
||||
**kwargs,
|
||||
)
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
def read(self):
|
||||
# Build iterable dataset
|
||||
if self.streaming:
|
||||
dataset = self.builder.as_streaming_dataset(split=self.builder.config.split)
|
||||
# Build regular (map-style) dataset
|
||||
else:
|
||||
download_config = None
|
||||
download_mode = None
|
||||
verification_mode = None
|
||||
base_path = None
|
||||
|
||||
self.builder.download_and_prepare(
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
verification_mode=verification_mode,
|
||||
base_path=base_path,
|
||||
num_proc=self.num_proc,
|
||||
)
|
||||
dataset = self.builder.as_dataset(split=self.builder.config.split, in_memory=self.keep_in_memory)
|
||||
if self.fingerprint:
|
||||
dataset._fingerprint = self.fingerprint
|
||||
return dataset
|
||||
@@ -0,0 +1,178 @@
|
||||
import multiprocessing
|
||||
import os
|
||||
from functools import partial
|
||||
from typing import BinaryIO, Optional, Union
|
||||
|
||||
import fsspec
|
||||
|
||||
from .. import Dataset, Features, NamedSplit, config
|
||||
from ..formatting import query_table
|
||||
from ..packaged_modules.json.json import Json
|
||||
from ..utils import tqdm as hf_tqdm
|
||||
from ..utils.json import get_json_field_paths_from_feature, json_decode_field
|
||||
from ..utils.typing import NestedDataStructureLike, PathLike
|
||||
from .abc import AbstractDatasetReader
|
||||
|
||||
|
||||
class JsonDatasetReader(AbstractDatasetReader):
|
||||
def __init__(
|
||||
self,
|
||||
path_or_paths: NestedDataStructureLike[PathLike],
|
||||
split: Optional[NamedSplit] = None,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
field: Optional[str] = None,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
path_or_paths,
|
||||
split=split,
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
keep_in_memory=keep_in_memory,
|
||||
streaming=streaming,
|
||||
num_proc=num_proc,
|
||||
**kwargs,
|
||||
)
|
||||
self.field = field
|
||||
path_or_paths = path_or_paths if isinstance(path_or_paths, dict) else {self.split: path_or_paths}
|
||||
self.builder = Json(
|
||||
cache_dir=cache_dir,
|
||||
data_files=path_or_paths,
|
||||
features=features,
|
||||
field=field,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
# Build iterable dataset
|
||||
if self.streaming:
|
||||
dataset = self.builder.as_streaming_dataset(split=self.split)
|
||||
# Build regular (map-style) dataset
|
||||
else:
|
||||
download_config = None
|
||||
download_mode = None
|
||||
verification_mode = None
|
||||
base_path = None
|
||||
|
||||
self.builder.download_and_prepare(
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
verification_mode=verification_mode,
|
||||
base_path=base_path,
|
||||
num_proc=self.num_proc,
|
||||
)
|
||||
dataset = self.builder.as_dataset(split=self.split, in_memory=self.keep_in_memory)
|
||||
return dataset
|
||||
|
||||
|
||||
class JsonDatasetWriter:
|
||||
def __init__(
|
||||
self,
|
||||
dataset: Dataset,
|
||||
path_or_buf: Union[PathLike, BinaryIO],
|
||||
batch_size: Optional[int] = None,
|
||||
num_proc: Optional[int] = None,
|
||||
storage_options: Optional[dict] = None,
|
||||
**to_json_kwargs,
|
||||
):
|
||||
if num_proc is not None and num_proc <= 0:
|
||||
raise ValueError(f"num_proc {num_proc} must be an integer > 0.")
|
||||
|
||||
self.dataset = dataset
|
||||
self.path_or_buf = path_or_buf
|
||||
self.batch_size = batch_size if batch_size else config.DEFAULT_MAX_BATCH_SIZE
|
||||
self.num_proc = num_proc
|
||||
self.encoding = "utf-8"
|
||||
self.storage_options = storage_options or {}
|
||||
self.to_json_kwargs = to_json_kwargs
|
||||
|
||||
def write(self) -> int:
|
||||
_ = self.to_json_kwargs.pop("path_or_buf", None)
|
||||
orient = self.to_json_kwargs.pop("orient", "records")
|
||||
lines = self.to_json_kwargs.pop("lines", True if orient == "records" else False)
|
||||
if "index" not in self.to_json_kwargs and orient in ["split", "table"]:
|
||||
self.to_json_kwargs["index"] = False
|
||||
|
||||
# Determine the default compression value based on self.path_or_buf type
|
||||
default_compression = "infer" if isinstance(self.path_or_buf, (str, bytes, os.PathLike)) else None
|
||||
compression = self.to_json_kwargs.pop("compression", default_compression)
|
||||
|
||||
if compression not in [None, "infer", "gzip", "bz2", "xz"]:
|
||||
raise NotImplementedError(f"`datasets` currently does not support {compression} compression")
|
||||
|
||||
if not lines and self.batch_size < self.dataset.num_rows:
|
||||
raise NotImplementedError(
|
||||
"Output JSON will not be formatted correctly when lines = False and batch_size < number of rows in the dataset. Use pandas.DataFrame.to_json() instead."
|
||||
)
|
||||
|
||||
if isinstance(self.path_or_buf, (str, bytes, os.PathLike)):
|
||||
with fsspec.open(
|
||||
self.path_or_buf, "wb", compression=compression, **(self.storage_options or {})
|
||||
) as buffer:
|
||||
written = self._write(file_obj=buffer, orient=orient, lines=lines, **self.to_json_kwargs)
|
||||
else:
|
||||
if compression:
|
||||
raise NotImplementedError(
|
||||
f"The compression parameter is not supported when writing to a buffer, but compression={compression}"
|
||||
" was passed. Please provide a local path instead."
|
||||
)
|
||||
written = self._write(file_obj=self.path_or_buf, orient=orient, lines=lines, **self.to_json_kwargs)
|
||||
return written
|
||||
|
||||
def _batch_json(self, args):
|
||||
offset, orient, lines, to_json_kwargs = args
|
||||
|
||||
batch = query_table(
|
||||
table=self.dataset.data,
|
||||
key=slice(offset, offset + self.batch_size),
|
||||
indices=self.dataset._indices,
|
||||
)
|
||||
batch = batch.to_pandas()
|
||||
for json_field_path in get_json_field_paths_from_feature(self.dataset.features):
|
||||
col, *json_field_subpath = json_field_path
|
||||
batch[col] = batch[col].apply(partial(json_decode_field, json_field_path=json_field_subpath))
|
||||
json_str = batch.to_json(path_or_buf=None, orient=orient, lines=lines, **to_json_kwargs)
|
||||
if not json_str.endswith("\n"):
|
||||
json_str += "\n"
|
||||
return json_str.encode(self.encoding)
|
||||
|
||||
def _write(
|
||||
self,
|
||||
file_obj: BinaryIO,
|
||||
orient,
|
||||
lines,
|
||||
**to_json_kwargs,
|
||||
) -> int:
|
||||
"""Writes the pyarrow table as JSON lines to a binary file handle.
|
||||
|
||||
Caller is responsible for opening and closing the handle.
|
||||
"""
|
||||
written = 0
|
||||
|
||||
if self.num_proc is None or self.num_proc == 1:
|
||||
for offset in hf_tqdm(
|
||||
range(0, len(self.dataset), self.batch_size),
|
||||
unit="ba",
|
||||
desc="Creating json from Arrow format",
|
||||
):
|
||||
json_str = self._batch_json((offset, orient, lines, to_json_kwargs))
|
||||
written += file_obj.write(json_str)
|
||||
else:
|
||||
num_rows, batch_size = len(self.dataset), self.batch_size
|
||||
with multiprocessing.Pool(self.num_proc) as pool:
|
||||
for json_str in hf_tqdm(
|
||||
pool.imap(
|
||||
self._batch_json,
|
||||
[(offset, orient, lines, to_json_kwargs) for offset in range(0, num_rows, batch_size)],
|
||||
),
|
||||
total=(num_rows // batch_size) + 1 if num_rows % batch_size else num_rows // batch_size,
|
||||
unit="ba",
|
||||
desc="Creating json from Arrow format",
|
||||
):
|
||||
written += file_obj.write(json_str)
|
||||
|
||||
return written
|
||||
@@ -0,0 +1,159 @@
|
||||
import json
|
||||
import os
|
||||
from typing import BinaryIO, Optional, Union
|
||||
|
||||
import fsspec
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
from .. import Dataset, Features, NamedSplit, config
|
||||
from ..arrow_writer import get_writer_batch_size_from_data_size, get_writer_batch_size_from_features
|
||||
from ..features.features import require_storage_embed
|
||||
from ..formatting import query_table
|
||||
from ..packaged_modules import _PACKAGED_DATASETS_MODULES
|
||||
from ..packaged_modules.parquet.parquet import Parquet
|
||||
from ..utils import tqdm as hf_tqdm
|
||||
from ..utils.typing import NestedDataStructureLike, PathLike
|
||||
from .abc import AbstractDatasetReader
|
||||
|
||||
|
||||
class ParquetDatasetReader(AbstractDatasetReader):
|
||||
def __init__(
|
||||
self,
|
||||
path_or_paths: NestedDataStructureLike[PathLike],
|
||||
split: Optional[NamedSplit] = None,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
path_or_paths,
|
||||
split=split,
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
keep_in_memory=keep_in_memory,
|
||||
streaming=streaming,
|
||||
num_proc=num_proc,
|
||||
**kwargs,
|
||||
)
|
||||
path_or_paths = path_or_paths if isinstance(path_or_paths, dict) else {self.split: path_or_paths}
|
||||
hash = _PACKAGED_DATASETS_MODULES["parquet"][1]
|
||||
self.builder = Parquet(
|
||||
cache_dir=cache_dir,
|
||||
data_files=path_or_paths,
|
||||
features=features,
|
||||
hash=hash,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
# Build iterable dataset
|
||||
if self.streaming:
|
||||
dataset = self.builder.as_streaming_dataset(split=self.split)
|
||||
# Build regular (map-style) dataset
|
||||
else:
|
||||
download_config = None
|
||||
download_mode = None
|
||||
verification_mode = None
|
||||
base_path = None
|
||||
|
||||
self.builder.download_and_prepare(
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
verification_mode=verification_mode,
|
||||
base_path=base_path,
|
||||
num_proc=self.num_proc,
|
||||
)
|
||||
dataset = self.builder.as_dataset(split=self.split, in_memory=self.keep_in_memory)
|
||||
return dataset
|
||||
|
||||
|
||||
class ParquetDatasetWriter:
|
||||
def __init__(
|
||||
self,
|
||||
dataset: Dataset,
|
||||
path_or_buf: Union[PathLike, BinaryIO],
|
||||
batch_size: Optional[int] = None,
|
||||
storage_options: Optional[dict] = None,
|
||||
use_content_defined_chunking: Union[bool, dict] = True,
|
||||
write_page_index: bool = True,
|
||||
**parquet_writer_kwargs,
|
||||
):
|
||||
self.dataset = dataset
|
||||
self.path_or_buf = path_or_buf
|
||||
self.batch_size = (
|
||||
batch_size
|
||||
or get_writer_batch_size_from_features(dataset.features)
|
||||
or get_writer_batch_size_from_data_size(len(dataset), dataset._estimate_nbytes())
|
||||
)
|
||||
self.storage_options = storage_options or {}
|
||||
self.parquet_writer_kwargs = parquet_writer_kwargs
|
||||
if use_content_defined_chunking is True:
|
||||
use_content_defined_chunking = config.DEFAULT_CDC_OPTIONS
|
||||
self.use_content_defined_chunking = use_content_defined_chunking
|
||||
self.write_page_index = write_page_index
|
||||
|
||||
def write(self) -> int:
|
||||
if isinstance(self.path_or_buf, (str, bytes, os.PathLike)):
|
||||
with fsspec.open(self.path_or_buf, "wb", **(self.storage_options or {})) as buffer:
|
||||
written = self._write(
|
||||
file_obj=buffer,
|
||||
batch_size=self.batch_size,
|
||||
**self.parquet_writer_kwargs,
|
||||
)
|
||||
else:
|
||||
written = self._write(
|
||||
file_obj=self.path_or_buf,
|
||||
batch_size=self.batch_size,
|
||||
**self.parquet_writer_kwargs,
|
||||
)
|
||||
return written
|
||||
|
||||
def _write(self, file_obj: BinaryIO, batch_size: int, **parquet_writer_kwargs) -> int:
|
||||
"""Writes the pyarrow table as Parquet to a binary file handle.
|
||||
|
||||
Caller is responsible for opening and closing the handle.
|
||||
"""
|
||||
written = 0
|
||||
_ = parquet_writer_kwargs.pop("path_or_buf", None)
|
||||
schema = self.dataset.features.arrow_schema
|
||||
|
||||
writer = pq.ParquetWriter(
|
||||
file_obj,
|
||||
schema=schema,
|
||||
use_content_defined_chunking=self.use_content_defined_chunking,
|
||||
write_page_index=self.write_page_index,
|
||||
compression={
|
||||
col: "none" if require_storage_embed(feature) else "snappy"
|
||||
for col, feature in self.dataset.features.items()
|
||||
},
|
||||
use_dictionary=[
|
||||
col for col, feature in self.dataset.features.items() if not require_storage_embed(feature)
|
||||
],
|
||||
column_encoding={
|
||||
col: "PLAIN" for col, feature in self.dataset.features.items() if require_storage_embed(feature)
|
||||
},
|
||||
**parquet_writer_kwargs,
|
||||
)
|
||||
|
||||
for offset in hf_tqdm(
|
||||
range(0, len(self.dataset), batch_size),
|
||||
unit="ba",
|
||||
desc="Creating parquet from Arrow format",
|
||||
):
|
||||
batch = query_table(
|
||||
table=self.dataset._data,
|
||||
key=slice(offset, offset + batch_size),
|
||||
indices=self.dataset._indices,
|
||||
)
|
||||
writer.write_table(batch)
|
||||
written += batch.nbytes
|
||||
|
||||
# TODO(kszucs): we may want to persist multiple parameters
|
||||
if self.use_content_defined_chunking is not False:
|
||||
writer.add_key_value_metadata({"content_defined_chunking": json.dumps(self.use_content_defined_chunking)})
|
||||
|
||||
writer.close()
|
||||
return written
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Optional
|
||||
|
||||
import pyspark
|
||||
|
||||
from .. import Features, NamedSplit
|
||||
from ..download import DownloadMode
|
||||
from ..packaged_modules.spark.spark import Spark
|
||||
from .abc import AbstractDatasetReader
|
||||
|
||||
|
||||
class SparkDatasetReader(AbstractDatasetReader):
|
||||
"""A dataset reader that reads from a Spark DataFrame.
|
||||
|
||||
When caching, cache materialization is parallelized over Spark; an NFS that is accessible to the driver must be
|
||||
provided. Streaming is not currently supported.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
df: pyspark.sql.DataFrame,
|
||||
split: Optional[NamedSplit] = None,
|
||||
features: Optional[Features] = None,
|
||||
streaming: bool = True,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
working_dir: str = None,
|
||||
load_from_cache_file: bool = True,
|
||||
file_format: str = "arrow",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
split=split,
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
keep_in_memory=keep_in_memory,
|
||||
streaming=streaming,
|
||||
**kwargs,
|
||||
)
|
||||
self._load_from_cache_file = load_from_cache_file
|
||||
self._file_format = file_format
|
||||
self.builder = Spark(
|
||||
df=df,
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
working_dir=working_dir,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
if self.streaming:
|
||||
return self.builder.as_streaming_dataset(split=self.split)
|
||||
download_mode = None if self._load_from_cache_file else DownloadMode.FORCE_REDOWNLOAD
|
||||
self.builder.download_and_prepare(
|
||||
download_mode=download_mode,
|
||||
file_format=self._file_format,
|
||||
)
|
||||
return self.builder.as_dataset(split=self.split)
|
||||
@@ -0,0 +1,122 @@
|
||||
import multiprocessing
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from .. import Dataset, Features, config
|
||||
from ..formatting import query_table
|
||||
from ..packaged_modules.sql.sql import Sql
|
||||
from ..utils import tqdm as hf_tqdm
|
||||
from .abc import AbstractDatasetInputStream
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sqlite3
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
class SqlDatasetReader(AbstractDatasetInputStream):
|
||||
def __init__(
|
||||
self,
|
||||
sql: Union[str, "sqlalchemy.sql.Selectable"],
|
||||
con: Union[str, "sqlalchemy.engine.Connection", "sqlalchemy.engine.Engine", "sqlite3.Connection"],
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(features=features, cache_dir=cache_dir, keep_in_memory=keep_in_memory, **kwargs)
|
||||
self.builder = Sql(
|
||||
cache_dir=cache_dir,
|
||||
features=features,
|
||||
sql=sql,
|
||||
con=con,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
download_config = None
|
||||
download_mode = None
|
||||
verification_mode = None
|
||||
base_path = None
|
||||
|
||||
self.builder.download_and_prepare(
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
verification_mode=verification_mode,
|
||||
base_path=base_path,
|
||||
)
|
||||
|
||||
# Build dataset for splits
|
||||
dataset = self.builder.as_dataset(split="train", in_memory=self.keep_in_memory)
|
||||
return dataset
|
||||
|
||||
|
||||
class SqlDatasetWriter:
|
||||
def __init__(
|
||||
self,
|
||||
dataset: Dataset,
|
||||
name: str,
|
||||
con: Union[str, "sqlalchemy.engine.Connection", "sqlalchemy.engine.Engine", "sqlite3.Connection"],
|
||||
batch_size: Optional[int] = None,
|
||||
num_proc: Optional[int] = None,
|
||||
**to_sql_kwargs,
|
||||
):
|
||||
if num_proc is not None and num_proc <= 0:
|
||||
raise ValueError(f"num_proc {num_proc} must be an integer > 0.")
|
||||
|
||||
self.dataset = dataset
|
||||
self.name = name
|
||||
self.con = con
|
||||
self.batch_size = batch_size if batch_size else config.DEFAULT_MAX_BATCH_SIZE
|
||||
self.num_proc = num_proc
|
||||
self.to_sql_kwargs = to_sql_kwargs
|
||||
|
||||
def write(self) -> int:
|
||||
_ = self.to_sql_kwargs.pop("sql", None)
|
||||
_ = self.to_sql_kwargs.pop("con", None)
|
||||
index = self.to_sql_kwargs.pop("index", False)
|
||||
|
||||
written = self._write(index=index, **self.to_sql_kwargs)
|
||||
return written
|
||||
|
||||
def _batch_sql(self, args):
|
||||
offset, index, to_sql_kwargs = args
|
||||
to_sql_kwargs = {**to_sql_kwargs, "if_exists": "append"} if offset > 0 else to_sql_kwargs
|
||||
batch = query_table(
|
||||
table=self.dataset.data,
|
||||
key=slice(offset, offset + self.batch_size),
|
||||
indices=self.dataset._indices,
|
||||
)
|
||||
df = batch.to_pandas()
|
||||
num_rows = df.to_sql(self.name, self.con, index=index, **to_sql_kwargs)
|
||||
return num_rows or len(df)
|
||||
|
||||
def _write(self, index, **to_sql_kwargs) -> int:
|
||||
"""Writes the pyarrow table as SQL to a database.
|
||||
|
||||
Caller is responsible for opening and closing the SQL connection.
|
||||
"""
|
||||
written = 0
|
||||
|
||||
if self.num_proc is None or self.num_proc == 1:
|
||||
for offset in hf_tqdm(
|
||||
range(0, len(self.dataset), self.batch_size),
|
||||
unit="ba",
|
||||
desc="Creating SQL from Arrow format",
|
||||
):
|
||||
written += self._batch_sql((offset, index, to_sql_kwargs))
|
||||
else:
|
||||
num_rows, batch_size = len(self.dataset), self.batch_size
|
||||
with multiprocessing.Pool(self.num_proc) as pool:
|
||||
for num_rows in hf_tqdm(
|
||||
pool.imap(
|
||||
self._batch_sql,
|
||||
[(offset, index, to_sql_kwargs) for offset in range(0, num_rows, batch_size)],
|
||||
),
|
||||
total=(num_rows // batch_size) + 1 if num_rows % batch_size else num_rows // batch_size,
|
||||
unit="ba",
|
||||
desc="Creating SQL from Arrow format",
|
||||
):
|
||||
written += num_rows
|
||||
|
||||
return written
|
||||
@@ -0,0 +1,58 @@
|
||||
from typing import Optional
|
||||
|
||||
from .. import Features, NamedSplit
|
||||
from ..packaged_modules.text.text import Text
|
||||
from ..utils.typing import NestedDataStructureLike, PathLike
|
||||
from .abc import AbstractDatasetReader
|
||||
|
||||
|
||||
class TextDatasetReader(AbstractDatasetReader):
|
||||
def __init__(
|
||||
self,
|
||||
path_or_paths: NestedDataStructureLike[PathLike],
|
||||
split: Optional[NamedSplit] = None,
|
||||
features: Optional[Features] = None,
|
||||
cache_dir: str = None,
|
||||
keep_in_memory: bool = False,
|
||||
streaming: bool = False,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
path_or_paths,
|
||||
split=split,
|
||||
features=features,
|
||||
cache_dir=cache_dir,
|
||||
keep_in_memory=keep_in_memory,
|
||||
streaming=streaming,
|
||||
num_proc=num_proc,
|
||||
**kwargs,
|
||||
)
|
||||
path_or_paths = path_or_paths if isinstance(path_or_paths, dict) else {self.split: path_or_paths}
|
||||
self.builder = Text(
|
||||
cache_dir=cache_dir,
|
||||
data_files=path_or_paths,
|
||||
features=features,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
# Build iterable dataset
|
||||
if self.streaming:
|
||||
dataset = self.builder.as_streaming_dataset(split=self.split)
|
||||
# Build regular (map-style) dataset
|
||||
else:
|
||||
download_config = None
|
||||
download_mode = None
|
||||
verification_mode = None
|
||||
base_path = None
|
||||
|
||||
self.builder.download_and_prepare(
|
||||
download_config=download_config,
|
||||
download_mode=download_mode,
|
||||
verification_mode=verification_mode,
|
||||
base_path=base_path,
|
||||
num_proc=self.num_proc,
|
||||
)
|
||||
dataset = self.builder.as_dataset(split=self.split, in_memory=self.keep_in_memory)
|
||||
return dataset
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
# Copyright 2020 The HuggingFace Datasets Authors and the TensorFlow Datasets Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Lint as: python3
|
||||
"""Utilities for file names."""
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
_uppercase_uppercase_re = re.compile(r"([A-Z]+)([A-Z][a-z])")
|
||||
_lowercase_uppercase_re = re.compile(r"([a-z\d])([A-Z])")
|
||||
|
||||
_single_underscore_re = re.compile(r"(?<!_)_(?!_)")
|
||||
_multiple_underscores_re = re.compile(r"(_{2,})")
|
||||
|
||||
_split_re = r"^\w+(\.\w+)*$"
|
||||
|
||||
INVALID_WINDOWS_CHARACTERS_IN_PATH = r"<>:/\|?*"
|
||||
|
||||
|
||||
def camelcase_to_snakecase(name):
|
||||
"""Convert camel-case string to snake-case."""
|
||||
name = _uppercase_uppercase_re.sub(r"\1_\2", name)
|
||||
name = _lowercase_uppercase_re.sub(r"\1_\2", name)
|
||||
return name.lower()
|
||||
|
||||
|
||||
def snakecase_to_camelcase(name):
|
||||
"""Convert snake-case string to camel-case string."""
|
||||
name = _single_underscore_re.split(name)
|
||||
name = [_multiple_underscores_re.split(n) for n in name]
|
||||
return "".join(n.capitalize() for n in itertools.chain.from_iterable(name) if n != "")
|
||||
|
||||
|
||||
def filename_prefix_for_name(name):
|
||||
if os.path.basename(name) != name:
|
||||
raise ValueError(f"Should be a dataset name, not a path: {name}")
|
||||
return camelcase_to_snakecase(name)
|
||||
|
||||
|
||||
def filename_prefix_for_split(name, split):
|
||||
if os.path.basename(name) != name:
|
||||
raise ValueError(f"Should be a dataset name, not a path: {name}")
|
||||
if not re.match(_split_re, split):
|
||||
raise ValueError(f"Split name should match '{_split_re}'' but got '{split}'.")
|
||||
return f"{filename_prefix_for_name(name)}-{split}"
|
||||
|
||||
|
||||
def filepattern_for_dataset_split(dataset_name, split, data_dir, filetype_suffix=None):
|
||||
prefix = filename_prefix_for_split(dataset_name, split)
|
||||
if filetype_suffix:
|
||||
prefix += f".{filetype_suffix}"
|
||||
filepath = os.path.join(data_dir, prefix)
|
||||
return f"{filepath}*"
|
||||
|
||||
|
||||
def filenames_for_dataset_split(path, dataset_name, split, filetype_suffix=None, shard_lengths=None):
|
||||
prefix = filename_prefix_for_split(dataset_name, split)
|
||||
prefix = os.path.join(path, prefix)
|
||||
|
||||
if shard_lengths and len(shard_lengths) > 1:
|
||||
num_shards = len(shard_lengths)
|
||||
filenames = [f"{prefix}-{shard_id:05d}-of-{num_shards:05d}" for shard_id in range(num_shards)]
|
||||
if filetype_suffix:
|
||||
filenames = [filename + f".{filetype_suffix}" for filename in filenames]
|
||||
return filenames
|
||||
else:
|
||||
filename = prefix
|
||||
if filetype_suffix:
|
||||
filename += f".{filetype_suffix}"
|
||||
return [filename]
|
||||
@@ -0,0 +1,148 @@
|
||||
import inspect
|
||||
import re
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from huggingface_hub.utils import insecure_hashlib
|
||||
|
||||
from .arrow import arrow
|
||||
from .audiofolder import audiofolder
|
||||
from .cache import cache
|
||||
from .conll import conll
|
||||
from .csv import csv
|
||||
from .eval import eval
|
||||
from .hdf5 import hdf5
|
||||
from .iceberg import iceberg
|
||||
from .imagefolder import imagefolder
|
||||
from .json import json
|
||||
from .lance import lance
|
||||
from .meshfolder import meshfolder
|
||||
from .niftifolder import niftifolder
|
||||
from .pandas import pandas
|
||||
from .parquet import parquet
|
||||
from .pdffolder import pdffolder
|
||||
from .sql import sql
|
||||
from .text import text
|
||||
from .tsfile import tsfile
|
||||
from .videofolder import videofolder
|
||||
from .webdataset import webdataset
|
||||
from .xml import xml
|
||||
|
||||
|
||||
def _hash_python_lines(lines: list[str]) -> str:
|
||||
filtered_lines = []
|
||||
for line in lines:
|
||||
line = re.sub(r"#.*", "", line) # remove comments
|
||||
if line:
|
||||
filtered_lines.append(line)
|
||||
full_str = "\n".join(filtered_lines)
|
||||
|
||||
# Make a hash from all this code
|
||||
full_bytes = full_str.encode("utf-8")
|
||||
return insecure_hashlib.sha256(full_bytes).hexdigest()
|
||||
|
||||
|
||||
# get importable module names and hash for caching
|
||||
_PACKAGED_DATASETS_MODULES = {
|
||||
"csv": (csv.__name__, _hash_python_lines(inspect.getsource(csv).splitlines())),
|
||||
"json": (json.__name__, _hash_python_lines(inspect.getsource(json).splitlines())),
|
||||
"pandas": (pandas.__name__, _hash_python_lines(inspect.getsource(pandas).splitlines())),
|
||||
"parquet": (parquet.__name__, _hash_python_lines(inspect.getsource(parquet).splitlines())),
|
||||
"arrow": (arrow.__name__, _hash_python_lines(inspect.getsource(arrow).splitlines())),
|
||||
"text": (text.__name__, _hash_python_lines(inspect.getsource(text).splitlines())),
|
||||
"conll": (conll.__name__, _hash_python_lines(inspect.getsource(conll).splitlines())),
|
||||
"imagefolder": (imagefolder.__name__, _hash_python_lines(inspect.getsource(imagefolder).splitlines())),
|
||||
"audiofolder": (audiofolder.__name__, _hash_python_lines(inspect.getsource(audiofolder).splitlines())),
|
||||
"videofolder": (videofolder.__name__, _hash_python_lines(inspect.getsource(videofolder).splitlines())),
|
||||
"meshfolder": (meshfolder.__name__, _hash_python_lines(inspect.getsource(meshfolder).splitlines())),
|
||||
"pdffolder": (pdffolder.__name__, _hash_python_lines(inspect.getsource(pdffolder).splitlines())),
|
||||
"niftifolder": (niftifolder.__name__, _hash_python_lines(inspect.getsource(niftifolder).splitlines())),
|
||||
"webdataset": (webdataset.__name__, _hash_python_lines(inspect.getsource(webdataset).splitlines())),
|
||||
"xml": (xml.__name__, _hash_python_lines(inspect.getsource(xml).splitlines())),
|
||||
"hdf5": (hdf5.__name__, _hash_python_lines(inspect.getsource(hdf5).splitlines())),
|
||||
"eval": (eval.__name__, _hash_python_lines(inspect.getsource(eval).splitlines())),
|
||||
"lance": (lance.__name__, _hash_python_lines(inspect.getsource(lance).splitlines())),
|
||||
"tsfile": (tsfile.__name__, _hash_python_lines(inspect.getsource(tsfile).splitlines())),
|
||||
"iceberg": (iceberg.__name__, _hash_python_lines(inspect.getsource(iceberg).splitlines())),
|
||||
}
|
||||
|
||||
# get importable module names and hash for caching
|
||||
_PACKAGED_DATASETS_MODULES_2_15_HASHES = {
|
||||
"csv": "eea64c71ca8b46dd3f537ed218fc9bf495d5707789152eb2764f5c78fa66d59d",
|
||||
"json": "8bb11242116d547c741b2e8a1f18598ffdd40a1d4f2a2872c7a28b697434bc96",
|
||||
"pandas": "3ac4ffc4563c796122ef66899b9485a3f1a977553e2d2a8a318c72b8cc6f2202",
|
||||
"parquet": "ca31c69184d9832faed373922c2acccec0b13a0bb5bbbe19371385c3ff26f1d1",
|
||||
"arrow": "74f69db2c14c2860059d39860b1f400a03d11bf7fb5a8258ca38c501c878c137",
|
||||
"text": "c4a140d10f020282918b5dd1b8a49f0104729c6177f60a6b49ec2a365ec69f34",
|
||||
"imagefolder": "7b7ce5247a942be131d49ad4f3de5866083399a0f250901bd8dc202f8c5f7ce5",
|
||||
"audiofolder": "d3c1655c66c8f72e4efb5c79e952975fa6e2ce538473a6890241ddbddee9071c",
|
||||
}
|
||||
|
||||
# Used to infer the module to use based on the data files extensions
|
||||
_EXTENSION_TO_MODULE: dict[str, tuple[str, dict]] = {
|
||||
".csv": ("csv", {}),
|
||||
".tsv": ("csv", {"sep": "\t"}),
|
||||
".json": ("json", {}),
|
||||
".jsonl": ("json", {}),
|
||||
# ndjson is no longer maintained (see: https://github.com/ndjson/ndjson-spec/issues/35#issuecomment-1285673417)
|
||||
".ndjson": ("json", {}),
|
||||
".parquet": ("parquet", {}),
|
||||
".geoparquet": ("parquet", {}),
|
||||
".gpq": ("parquet", {}),
|
||||
".arrow": ("arrow", {}),
|
||||
".txt": ("text", {}),
|
||||
".conll": ("conll", {}),
|
||||
".conllu": ("conll", {"comment_prefix": "#"}),
|
||||
".tar": ("webdataset", {}),
|
||||
".xml": ("xml", {}),
|
||||
".hdf5": ("hdf5", {}),
|
||||
".h5": ("hdf5", {}),
|
||||
".eval": ("eval", {}),
|
||||
".lance": ("lance", {}),
|
||||
".tsfile": ("tsfile", {}),
|
||||
}
|
||||
_EXTENSION_TO_MODULE.update({ext: ("imagefolder", {}) for ext in imagefolder.ImageFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext.upper(): ("imagefolder", {}) for ext in imagefolder.ImageFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext: ("audiofolder", {}) for ext in audiofolder.AudioFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext.upper(): ("audiofolder", {}) for ext in audiofolder.AudioFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext: ("videofolder", {}) for ext in videofolder.VideoFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext.upper(): ("videofolder", {}) for ext in videofolder.VideoFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext: ("meshfolder", {}) for ext in meshfolder.MeshFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext.upper(): ("meshfolder", {}) for ext in meshfolder.MeshFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext: ("pdffolder", {}) for ext in pdffolder.PdfFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext.upper(): ("pdffolder", {}) for ext in pdffolder.PdfFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext: ("niftifolder", {}) for ext in niftifolder.NiftiFolder.EXTENSIONS})
|
||||
_EXTENSION_TO_MODULE.update({ext.upper(): ("niftifolder", {}) for ext in niftifolder.NiftiFolder.EXTENSIONS})
|
||||
|
||||
# Used to filter data files based on extensions given a module name
|
||||
_MODULE_TO_EXTENSIONS: dict[str, list[str]] = {}
|
||||
for _ext, (_module, _) in _EXTENSION_TO_MODULE.items():
|
||||
_MODULE_TO_EXTENSIONS.setdefault(_module, []).append(_ext)
|
||||
|
||||
for _module in _MODULE_TO_EXTENSIONS:
|
||||
_MODULE_TO_EXTENSIONS[_module].append(".zip")
|
||||
|
||||
# Used to filter data files based on file names
|
||||
_MODULE_TO_METADATA_FILE_NAMES: Dict[str, List[str]] = {}
|
||||
for _module in _MODULE_TO_EXTENSIONS:
|
||||
_MODULE_TO_METADATA_FILE_NAMES[_module] = []
|
||||
_MODULE_TO_METADATA_FILE_NAMES["imagefolder"] = imagefolder.ImageFolder.METADATA_FILENAMES
|
||||
_MODULE_TO_METADATA_FILE_NAMES["audiofolder"] = imagefolder.ImageFolder.METADATA_FILENAMES
|
||||
_MODULE_TO_METADATA_FILE_NAMES["videofolder"] = imagefolder.ImageFolder.METADATA_FILENAMES
|
||||
_MODULE_TO_METADATA_FILE_NAMES["meshfolder"] = meshfolder.MeshFolder.METADATA_FILENAMES
|
||||
_MODULE_TO_METADATA_FILE_NAMES["pdffolder"] = imagefolder.ImageFolder.METADATA_FILENAMES
|
||||
_MODULE_TO_METADATA_FILE_NAMES["niftifolder"] = imagefolder.ImageFolder.METADATA_FILENAMES
|
||||
_MODULE_TO_METADATA_FILE_NAMES["lance"] = lance.Lance.METADATA_FILE_NAMES
|
||||
|
||||
_MODULE_TO_METADATA_EXTENSIONS: Dict[str, List[str]] = {}
|
||||
for _module in _MODULE_TO_EXTENSIONS:
|
||||
_MODULE_TO_METADATA_EXTENSIONS[_module] = []
|
||||
_MODULE_TO_METADATA_EXTENSIONS["lance"] = lance.Lance.METADATA_EXTENSIONS
|
||||
|
||||
# Total
|
||||
|
||||
_ALL_EXTENSIONS = list(_EXTENSION_TO_MODULE.keys()) + [".zip"]
|
||||
_ALL_METADATA_EXTENSIONS = sorted({_ext for _exts in _MODULE_TO_METADATA_EXTENSIONS.values() for _ext in _exts})
|
||||
_ALL_ALLOWED_EXTENSIONS = _ALL_EXTENSIONS + _ALL_METADATA_EXTENSIONS
|
||||
_ALL_METADATA_FILENAMES = sorted(
|
||||
{file_name for file_names in _MODULE_TO_METADATA_FILE_NAMES.values() for file_name in file_names}
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArrowConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for Arrow."""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class Arrow(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = ArrowConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""We handle string, list and dicts in datafiles"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
data_files = dl_manager.download(self.config.data_files)
|
||||
splits = []
|
||||
for split_name, files in data_files.items():
|
||||
# Infer features if they are stored in the arrow schema
|
||||
if self.info.features is None:
|
||||
for file in files:
|
||||
with open(file, "rb") as f:
|
||||
try:
|
||||
reader = pa.ipc.open_stream(f)
|
||||
except (OSError, pa.lib.ArrowInvalid):
|
||||
reader = pa.ipc.open_file(f)
|
||||
self.info.features = datasets.Features.from_arrow_schema(reader.schema)
|
||||
break
|
||||
splits.append(datasets.SplitGenerator(name=split_name, gen_kwargs={"files": files}))
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.info.features is not None:
|
||||
# more expensive cast to support nested features with keys in a different order
|
||||
# allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, self.info.features.arrow_schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, files):
|
||||
yield from files
|
||||
|
||||
def _generate_tables(self, files):
|
||||
for file_idx, file in enumerate(files):
|
||||
with open(file, "rb") as f:
|
||||
try:
|
||||
try:
|
||||
batches = pa.ipc.open_stream(f)
|
||||
except (OSError, pa.lib.ArrowInvalid):
|
||||
reader = pa.ipc.open_file(f)
|
||||
batches = (reader.get_batch(i) for i in range(reader.num_record_batches))
|
||||
for batch_idx, record_batch in enumerate(batches):
|
||||
pa_table = pa.Table.from_batches([record_batch])
|
||||
# Uncomment for debugging (will print the Arrow table size and elements)
|
||||
# logger.warning(f"pa_table: {pa_table} num rows: {pa_table.num_rows}")
|
||||
# logger.warning('\n'.join(str(pa_table.slice(i, 1).to_pydict()) for i in range(pa_table.num_rows)))
|
||||
yield Key(file_idx, batch_idx), self._cast_table(pa_table)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to read file '{file}' with error {type(e)}: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,86 @@
|
||||
import datasets
|
||||
|
||||
from ..folder_based_builder import folder_based_builder
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
class AudioFolderConfig(folder_based_builder.FolderBasedBuilderConfig):
|
||||
"""Builder Config for AudioFolder."""
|
||||
|
||||
drop_labels: bool = None
|
||||
drop_metadata: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class AudioFolder(folder_based_builder.FolderBasedBuilder):
|
||||
BASE_FEATURE = datasets.Audio
|
||||
BASE_COLUMN_NAME = "audio"
|
||||
BUILDER_CONFIG_CLASS = AudioFolderConfig
|
||||
EXTENSIONS: list[str] # definition at the bottom of the script
|
||||
|
||||
|
||||
# Obtained with:
|
||||
# ```
|
||||
# import soundfile as sf
|
||||
#
|
||||
# AUDIO_EXTENSIONS = [f".{format.lower()}" for format in sf.available_formats().keys()]
|
||||
#
|
||||
# # .opus decoding is supported if libsndfile >= 1.0.31:
|
||||
# AUDIO_EXTENSIONS.extend([".opus"])
|
||||
# ```
|
||||
# We intentionally did not run this code on launch because:
|
||||
# (1) Soundfile was an optional dependency, so importing it in global namespace is not allowed
|
||||
# (2) To ensure the list of supported extensions is deterministic
|
||||
# (3) We use TorchCodec now anyways instead of Soundfile
|
||||
AUDIO_EXTENSIONS = [
|
||||
".aiff",
|
||||
".au",
|
||||
".avr",
|
||||
".caf",
|
||||
".flac",
|
||||
".htk",
|
||||
".svx",
|
||||
".mat4",
|
||||
".mat5",
|
||||
".mpc2k",
|
||||
".ogg",
|
||||
".paf",
|
||||
".pvf",
|
||||
".raw",
|
||||
".rf64",
|
||||
".sd2",
|
||||
".sds",
|
||||
".ircam",
|
||||
".voc",
|
||||
".w64",
|
||||
".wav",
|
||||
".nist",
|
||||
".wavex",
|
||||
".wve",
|
||||
".xi",
|
||||
".mp3",
|
||||
".opus",
|
||||
".3gp",
|
||||
".3g2",
|
||||
".avi",
|
||||
".asf",
|
||||
".flv",
|
||||
".mp4",
|
||||
".mov",
|
||||
".m4v",
|
||||
".mkv",
|
||||
".mpg",
|
||||
".webm",
|
||||
".f4v",
|
||||
".wmv",
|
||||
".wma",
|
||||
".ogg",
|
||||
".ogm",
|
||||
".mxf",
|
||||
".nut",
|
||||
]
|
||||
AudioFolder.EXTENSIONS = AUDIO_EXTENSIONS
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
import datasets.config
|
||||
import datasets.data_files
|
||||
from datasets.builder import Key
|
||||
from datasets.naming import camelcase_to_snakecase, filenames_for_dataset_split
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
def _get_modification_time(cached_directory_path):
|
||||
return (Path(cached_directory_path)).stat().st_mtime
|
||||
|
||||
|
||||
def _find_hash_in_cache(
|
||||
dataset_name: str,
|
||||
config_name: Optional[str],
|
||||
cache_dir: Optional[str],
|
||||
config_kwargs: dict,
|
||||
custom_features: Optional[datasets.Features],
|
||||
) -> tuple[str, str, str]:
|
||||
if config_name or config_kwargs or custom_features:
|
||||
config_id = datasets.BuilderConfig(config_name or "default").create_config_id(
|
||||
config_kwargs=config_kwargs, custom_features=custom_features
|
||||
)
|
||||
else:
|
||||
config_id = None
|
||||
cache_dir = os.path.expanduser(str(cache_dir or datasets.config.HF_DATASETS_CACHE))
|
||||
namespace_and_dataset_name = dataset_name.split("/")
|
||||
namespace_and_dataset_name[-1] = camelcase_to_snakecase(namespace_and_dataset_name[-1])
|
||||
cached_relative_path = "___".join(namespace_and_dataset_name)
|
||||
cached_datasets_directory_path_root = os.path.join(cache_dir, cached_relative_path)
|
||||
cached_directory_paths = [
|
||||
cached_directory_path
|
||||
for cached_directory_path in glob.glob(
|
||||
os.path.join(cached_datasets_directory_path_root, config_id or "*", "*", "*")
|
||||
)
|
||||
if os.path.isdir(cached_directory_path)
|
||||
and (
|
||||
config_kwargs
|
||||
or custom_features
|
||||
or json.loads(Path(cached_directory_path, "dataset_info.json").read_text(encoding="utf-8"))["config_name"]
|
||||
== Path(cached_directory_path).parts[-3] # no extra params => config_id == config_name
|
||||
)
|
||||
]
|
||||
if not cached_directory_paths:
|
||||
cached_directory_paths = [
|
||||
cached_directory_path
|
||||
for cached_directory_path in glob.glob(os.path.join(cached_datasets_directory_path_root, "*", "*", "*"))
|
||||
if os.path.isdir(cached_directory_path)
|
||||
]
|
||||
available_configs = sorted(
|
||||
{Path(cached_directory_path).parts[-3] for cached_directory_path in cached_directory_paths}
|
||||
)
|
||||
raise ValueError(
|
||||
f"Couldn't find cache for {dataset_name}"
|
||||
+ (f" for config '{config_id}'" if config_id else "")
|
||||
+ (f"\nAvailable configs in the cache: {available_configs}" if available_configs else "")
|
||||
)
|
||||
# get most recent
|
||||
cached_directory_path = Path(sorted(cached_directory_paths, key=_get_modification_time)[-1])
|
||||
version, hash = cached_directory_path.parts[-2:]
|
||||
other_configs = [
|
||||
Path(_cached_directory_path).parts[-3]
|
||||
for _cached_directory_path in glob.glob(os.path.join(cached_datasets_directory_path_root, "*", version, hash))
|
||||
if os.path.isdir(_cached_directory_path)
|
||||
and (
|
||||
config_kwargs
|
||||
or custom_features
|
||||
or json.loads(Path(_cached_directory_path, "dataset_info.json").read_text(encoding="utf-8"))["config_name"]
|
||||
== Path(_cached_directory_path).parts[-3] # no extra params => config_id == config_name
|
||||
)
|
||||
]
|
||||
if not config_id and len(other_configs) > 1:
|
||||
raise ValueError(
|
||||
f"There are multiple '{dataset_name}' configurations in the cache: {', '.join(other_configs)}"
|
||||
f"\nPlease specify which configuration to reload from the cache, e.g."
|
||||
f"\n\tload_dataset('{dataset_name}', '{other_configs[0]}')"
|
||||
)
|
||||
config_name = cached_directory_path.parts[-3]
|
||||
warning_msg = (
|
||||
f"Found the latest cached dataset configuration '{config_name}' at {cached_directory_path} "
|
||||
f"(last modified on {time.ctime(_get_modification_time(cached_directory_path))})."
|
||||
)
|
||||
logger.warning(warning_msg)
|
||||
return config_name, version, hash
|
||||
|
||||
|
||||
class Cache(datasets.ArrowBasedBuilder):
|
||||
def __init__(
|
||||
self,
|
||||
cache_dir: Optional[str] = None,
|
||||
dataset_name: Optional[str] = None,
|
||||
config_name: Optional[str] = None,
|
||||
version: Optional[str] = "0.0.0",
|
||||
hash: Optional[str] = None,
|
||||
base_path: Optional[str] = None,
|
||||
info: Optional[datasets.DatasetInfo] = None,
|
||||
features: Optional[datasets.Features] = None,
|
||||
token: Optional[Union[bool, str]] = None,
|
||||
repo_id: Optional[str] = None,
|
||||
data_files: Optional[Union[str, list, dict, datasets.data_files.DataFilesDict]] = None,
|
||||
data_dir: Optional[str] = None,
|
||||
storage_options: Optional[dict] = None,
|
||||
writer_batch_size: Optional[int] = None,
|
||||
**config_kwargs,
|
||||
):
|
||||
if repo_id is None and dataset_name is None:
|
||||
raise ValueError("repo_id or dataset_name is required for the Cache dataset builder")
|
||||
if data_files is not None:
|
||||
config_kwargs["data_files"] = data_files
|
||||
if data_dir is not None:
|
||||
config_kwargs["data_dir"] = data_dir
|
||||
if hash == "auto" and version == "auto":
|
||||
config_name, version, hash = _find_hash_in_cache(
|
||||
dataset_name=repo_id or dataset_name,
|
||||
config_name=config_name,
|
||||
cache_dir=cache_dir,
|
||||
config_kwargs=config_kwargs,
|
||||
custom_features=features,
|
||||
)
|
||||
elif hash == "auto" or version == "auto":
|
||||
raise NotImplementedError("Pass both hash='auto' and version='auto' instead")
|
||||
super().__init__(
|
||||
cache_dir=cache_dir,
|
||||
dataset_name=dataset_name,
|
||||
config_name=config_name,
|
||||
version=version,
|
||||
hash=hash,
|
||||
base_path=base_path,
|
||||
info=info,
|
||||
token=token,
|
||||
repo_id=repo_id,
|
||||
storage_options=storage_options,
|
||||
writer_batch_size=writer_batch_size,
|
||||
)
|
||||
|
||||
def _info(self) -> datasets.DatasetInfo:
|
||||
return datasets.DatasetInfo()
|
||||
|
||||
def download_and_prepare(self, output_dir: Optional[str] = None, *args, **kwargs):
|
||||
if not os.path.exists(self.cache_dir):
|
||||
raise ValueError(f"Cache directory for {self.dataset_name} doesn't exist at {self.cache_dir}")
|
||||
if output_dir is not None and output_dir != self.cache_dir:
|
||||
shutil.copytree(self.cache_dir, output_dir)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
# used to stream from cache
|
||||
if isinstance(self.info.splits, datasets.SplitDict):
|
||||
split_infos: list[datasets.SplitInfo] = list(self.info.splits.values())
|
||||
else:
|
||||
raise ValueError(f"Missing splits info for {self.dataset_name} in cache directory {self.cache_dir}")
|
||||
return [
|
||||
datasets.SplitGenerator(
|
||||
name=split_info.name,
|
||||
gen_kwargs={
|
||||
"files": filenames_for_dataset_split(
|
||||
self.cache_dir,
|
||||
dataset_name=self.dataset_name,
|
||||
split=split_info.name,
|
||||
filetype_suffix="arrow",
|
||||
shard_lengths=split_info.shard_lengths,
|
||||
)
|
||||
},
|
||||
)
|
||||
for split_info in split_infos
|
||||
]
|
||||
|
||||
def _generate_shards(self, files):
|
||||
yield from files
|
||||
|
||||
def _generate_tables(self, files):
|
||||
# used to stream from cache
|
||||
for file_idx, file in enumerate(files):
|
||||
with open(file, "rb") as f:
|
||||
try:
|
||||
for batch_idx, record_batch in enumerate(pa.ipc.open_stream(f)):
|
||||
pa_table = pa.Table.from_batches([record_batch])
|
||||
# Uncomment for debugging (will print the Arrow table size and elements)
|
||||
# logger.warning(f"pa_table: {pa_table} num rows: {pa_table.num_rows}")
|
||||
# logger.warning('\n'.join(str(pa_table.slice(i, 1).to_pydict()) for i in range(pa_table.num_rows)))
|
||||
yield Key(file_idx, batch_idx), pa_table
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to read file '{file}' with error {type(e)}: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,155 @@
|
||||
"""CoNLL format dataset builder.
|
||||
|
||||
Reads CoNLL-style files where each line carries one token plus its tag columns,
|
||||
columns are whitespace-separated, and empty lines mark sentence boundaries.
|
||||
Each example is one sentence, with each configured column produced as a list
|
||||
aligned with the tokens list.
|
||||
|
||||
Supports CoNLL-2000 (chunking), CoNLL-2003 (NER), CoNLL-U (Universal
|
||||
Dependencies), and any custom column schema by overriding ``column_names``.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.features.features import require_storage_cast
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
DEFAULT_COLUMN_NAMES = ["tokens"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConllConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for CoNLL-style files.
|
||||
|
||||
Args:
|
||||
features: (`Features`, *optional*):
|
||||
Cast the data to `features`.
|
||||
column_names: (`list[str]`, defaults to `["tokens"]`):
|
||||
Names for each whitespace-separated column. The first name is the
|
||||
token column; subsequent names cover the tag columns. Common
|
||||
schemas:
|
||||
|
||||
- CoNLL-2003 NER: `["tokens", "pos_tags", "chunk_tags", "ner_tags"]`
|
||||
- CoNLL-2000 chunking: `["tokens", "pos_tags", "chunk_tags"]`
|
||||
- CoNLL-U: `["id", "form", "lemma", "upos", "xpos", "feats",
|
||||
"head", "deprel", "deps", "misc"]`
|
||||
delimiter: (`str`, *optional*):
|
||||
Column delimiter inside each line. If `None` (default), any
|
||||
whitespace is used, matching the standard CoNLL convention.
|
||||
encoding: (`str`, defaults to `"utf-8"`):
|
||||
Encoding to decode the file.
|
||||
encoding_errors: (`str`, *optional*):
|
||||
Argument to define what to do in case of encoding error. Same as
|
||||
the `errors` argument in `open()`.
|
||||
skip_docstart: (`bool`, defaults to `True`):
|
||||
Skip CoNLL-2003 `-DOCSTART-` document-boundary marker lines.
|
||||
comment_prefix: (`str`, *optional*):
|
||||
If set, lines starting with this prefix are skipped. CoNLL-U
|
||||
comments start with `#`.
|
||||
"""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
column_names: list[str] = field(default_factory=lambda: list(DEFAULT_COLUMN_NAMES))
|
||||
delimiter: Optional[str] = None
|
||||
encoding: str = "utf-8"
|
||||
encoding_errors: Optional[str] = None
|
||||
skip_docstart: bool = True
|
||||
comment_prefix: Optional[str] = None
|
||||
|
||||
|
||||
class Conll(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = ConllConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""The `data_files` kwarg in load_dataset() can be a str, List[str],
|
||||
Dict[str,str], or Dict[str,List[str]].
|
||||
|
||||
If str or List[str], then the dataset returns only the 'train' split.
|
||||
If dict, then keys should be from the `datasets.Split` enum.
|
||||
"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
base_data_files = dl_manager.download(self.config.data_files)
|
||||
extracted_data_files = dl_manager.extract(base_data_files)
|
||||
splits = []
|
||||
for split_name, files in extracted_data_files.items():
|
||||
files_iterables = [dl_manager.iter_files(file) for file in files]
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={
|
||||
"files_iterables": files_iterables,
|
||||
"base_files": base_data_files[split_name],
|
||||
},
|
||||
)
|
||||
)
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.config.features is not None:
|
||||
schema = self.config.features.arrow_schema
|
||||
if all(not require_storage_cast(feature) for feature in self.config.features.values()):
|
||||
pa_table = pa_table.cast(schema)
|
||||
else:
|
||||
pa_table = table_cast(pa_table, schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, base_files, files_iterables):
|
||||
yield from base_files
|
||||
|
||||
def _generate_tables(self, base_files, files_iterables):
|
||||
column_names = list(self.config.column_names)
|
||||
if not column_names:
|
||||
raise ValueError("ConllConfig.column_names must be a non-empty list")
|
||||
num_cols = len(column_names)
|
||||
delimiter = self.config.delimiter
|
||||
skip_docstart = self.config.skip_docstart
|
||||
comment_prefix = self.config.comment_prefix
|
||||
|
||||
for shard_idx, files_iterable in enumerate(files_iterables):
|
||||
for file in files_iterable:
|
||||
sentences: list[list[list[str]]] = []
|
||||
current: list[list[str]] = [[] for _ in range(num_cols)]
|
||||
with open(file, encoding=self.config.encoding, errors=self.config.encoding_errors) as f:
|
||||
for raw_line in f:
|
||||
# Strip the trailing newline only — preserve embedded whitespace
|
||||
line = raw_line.rstrip("\r\n")
|
||||
if not line.strip():
|
||||
# Sentence boundary
|
||||
if current[0]:
|
||||
sentences.append(current)
|
||||
current = [[] for _ in range(num_cols)]
|
||||
continue
|
||||
if skip_docstart and line.startswith("-DOCSTART-"):
|
||||
continue
|
||||
if comment_prefix is not None and line.startswith(comment_prefix):
|
||||
continue
|
||||
parts = line.split(delimiter) if delimiter is not None else line.split()
|
||||
# Pad or truncate to num_cols so column alignment is preserved
|
||||
if len(parts) < num_cols:
|
||||
parts = parts + [""] * (num_cols - len(parts))
|
||||
elif len(parts) > num_cols:
|
||||
parts = parts[:num_cols]
|
||||
for i, val in enumerate(parts):
|
||||
current[i].append(val)
|
||||
# Tail sentence with no trailing blank line
|
||||
if current[0]:
|
||||
sentences.append(current)
|
||||
|
||||
if sentences:
|
||||
arrays = [pa.array([sentence[col_idx] for sentence in sentences]) for col_idx in range(num_cols)]
|
||||
pa_table = pa.Table.from_arrays(arrays, names=column_names)
|
||||
yield Key(shard_idx, 0), self._cast_table(pa_table)
|
||||
@@ -0,0 +1,206 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
import datasets.config
|
||||
from datasets.builder import Key
|
||||
from datasets.features.features import require_storage_cast
|
||||
from datasets.table import table_cast
|
||||
from datasets.utils.py_utils import Literal
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
_PANDAS_READ_CSV_NO_DEFAULT_PARAMETERS = ["names", "prefix"]
|
||||
_PANDAS_READ_CSV_DEPRECATED_PARAMETERS = ["warn_bad_lines", "error_bad_lines", "mangle_dupe_cols"]
|
||||
_PANDAS_READ_CSV_NEW_1_3_0_PARAMETERS = ["encoding_errors", "on_bad_lines"]
|
||||
_PANDAS_READ_CSV_NEW_2_0_0_PARAMETERS = ["date_format"]
|
||||
_PANDAS_READ_CSV_DEPRECATED_2_2_0_PARAMETERS = ["verbose"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CsvConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for CSV."""
|
||||
|
||||
sep: str = ","
|
||||
delimiter: Optional[str] = None
|
||||
header: Optional[Union[int, list[int], str]] = "infer"
|
||||
names: Optional[list[str]] = None
|
||||
column_names: Optional[list[str]] = None
|
||||
index_col: Optional[Union[int, str, list[int], list[str]]] = None
|
||||
usecols: Optional[Union[list[int], list[str]]] = None
|
||||
prefix: Optional[str] = None
|
||||
mangle_dupe_cols: bool = True
|
||||
engine: Optional[Literal["c", "python", "pyarrow"]] = None
|
||||
converters: dict[Union[int, str], Callable[[Any], Any]] = None
|
||||
true_values: Optional[list] = None
|
||||
false_values: Optional[list] = None
|
||||
skipinitialspace: bool = False
|
||||
skiprows: Optional[Union[int, list[int]]] = None
|
||||
nrows: Optional[int] = None
|
||||
na_values: Optional[Union[str, list[str]]] = None
|
||||
keep_default_na: bool = True
|
||||
na_filter: bool = True
|
||||
verbose: bool = False
|
||||
skip_blank_lines: bool = True
|
||||
thousands: Optional[str] = None
|
||||
decimal: str = "."
|
||||
lineterminator: Optional[str] = None
|
||||
quotechar: str = '"'
|
||||
quoting: int = 0
|
||||
escapechar: Optional[str] = None
|
||||
comment: Optional[str] = None
|
||||
encoding: Optional[str] = None
|
||||
dialect: Optional[str] = None
|
||||
error_bad_lines: bool = True
|
||||
warn_bad_lines: bool = True
|
||||
skipfooter: int = 0
|
||||
doublequote: bool = True
|
||||
memory_map: bool = False
|
||||
float_precision: Optional[str] = None
|
||||
chunksize: int = 10_000
|
||||
features: Optional[datasets.Features] = None
|
||||
encoding_errors: Optional[str] = "strict"
|
||||
on_bad_lines: Literal["error", "warn", "skip"] = "error"
|
||||
date_format: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.delimiter is not None:
|
||||
self.sep = self.delimiter
|
||||
if self.column_names is not None:
|
||||
self.names = self.column_names
|
||||
|
||||
@property
|
||||
def pd_read_csv_kwargs(self):
|
||||
pd_read_csv_kwargs = {
|
||||
"sep": self.sep,
|
||||
"header": self.header,
|
||||
"names": self.names,
|
||||
"index_col": self.index_col,
|
||||
"usecols": self.usecols,
|
||||
"prefix": self.prefix,
|
||||
"mangle_dupe_cols": self.mangle_dupe_cols,
|
||||
"engine": self.engine,
|
||||
"converters": self.converters,
|
||||
"true_values": self.true_values,
|
||||
"false_values": self.false_values,
|
||||
"skipinitialspace": self.skipinitialspace,
|
||||
"skiprows": self.skiprows,
|
||||
"nrows": self.nrows,
|
||||
"na_values": self.na_values,
|
||||
"keep_default_na": self.keep_default_na,
|
||||
"na_filter": self.na_filter,
|
||||
"verbose": self.verbose,
|
||||
"skip_blank_lines": self.skip_blank_lines,
|
||||
"thousands": self.thousands,
|
||||
"decimal": self.decimal,
|
||||
"lineterminator": self.lineterminator,
|
||||
"quotechar": self.quotechar,
|
||||
"quoting": self.quoting,
|
||||
"escapechar": self.escapechar,
|
||||
"comment": self.comment,
|
||||
"encoding": self.encoding,
|
||||
"dialect": self.dialect,
|
||||
"error_bad_lines": self.error_bad_lines,
|
||||
"warn_bad_lines": self.warn_bad_lines,
|
||||
"skipfooter": self.skipfooter,
|
||||
"doublequote": self.doublequote,
|
||||
"memory_map": self.memory_map,
|
||||
"float_precision": self.float_precision,
|
||||
"chunksize": self.chunksize,
|
||||
"encoding_errors": self.encoding_errors,
|
||||
"on_bad_lines": self.on_bad_lines,
|
||||
"date_format": self.date_format,
|
||||
}
|
||||
|
||||
# some kwargs must not be passed if they don't have a default value
|
||||
# some others are deprecated and we can also not pass them if they are the default value
|
||||
for pd_read_csv_parameter in _PANDAS_READ_CSV_NO_DEFAULT_PARAMETERS + _PANDAS_READ_CSV_DEPRECATED_PARAMETERS:
|
||||
if pd_read_csv_kwargs[pd_read_csv_parameter] == getattr(CsvConfig(), pd_read_csv_parameter):
|
||||
del pd_read_csv_kwargs[pd_read_csv_parameter]
|
||||
|
||||
# Remove 1.3 new arguments
|
||||
if not (datasets.config.PANDAS_VERSION.major >= 1 and datasets.config.PANDAS_VERSION.minor >= 3):
|
||||
for pd_read_csv_parameter in _PANDAS_READ_CSV_NEW_1_3_0_PARAMETERS:
|
||||
del pd_read_csv_kwargs[pd_read_csv_parameter]
|
||||
|
||||
# Remove 2.0 new arguments
|
||||
if not (datasets.config.PANDAS_VERSION.major >= 2):
|
||||
for pd_read_csv_parameter in _PANDAS_READ_CSV_NEW_2_0_0_PARAMETERS:
|
||||
del pd_read_csv_kwargs[pd_read_csv_parameter]
|
||||
|
||||
# Remove 2.2 deprecated arguments
|
||||
if datasets.config.PANDAS_VERSION.release >= (2, 2):
|
||||
for pd_read_csv_parameter in _PANDAS_READ_CSV_DEPRECATED_2_2_0_PARAMETERS:
|
||||
if pd_read_csv_kwargs[pd_read_csv_parameter] == getattr(CsvConfig(), pd_read_csv_parameter):
|
||||
del pd_read_csv_kwargs[pd_read_csv_parameter]
|
||||
|
||||
return pd_read_csv_kwargs
|
||||
|
||||
|
||||
class Csv(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = CsvConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""We handle string, list and dicts in datafiles"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
base_data_files = dl_manager.download(self.config.data_files)
|
||||
extracted_data_files = dl_manager.extract(base_data_files)
|
||||
splits = []
|
||||
for split_name, extracted_files in extracted_data_files.items():
|
||||
files_iterables = [dl_manager.iter_files(extracted_file) for extracted_file in extracted_files]
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={"files_iterables": files_iterables, "base_files": base_data_files[split_name]},
|
||||
)
|
||||
)
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.config.features is not None:
|
||||
schema = self.config.features.arrow_schema
|
||||
if all(not require_storage_cast(feature) for feature in self.config.features.values()):
|
||||
# cheaper cast
|
||||
pa_table = pa.Table.from_arrays([pa_table[field.name] for field in schema], schema=schema)
|
||||
else:
|
||||
# more expensive cast; allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, base_files, files_iterables):
|
||||
yield from base_files
|
||||
|
||||
def _generate_tables(self, base_files, files_iterables):
|
||||
schema = self.config.features.arrow_schema if self.config.features else None
|
||||
# dtype allows reading an int column as str
|
||||
dtype = (
|
||||
{
|
||||
name: dtype.to_pandas_dtype() if not require_storage_cast(feature) else object
|
||||
for name, dtype, feature in zip(schema.names, schema.types, self.config.features.values())
|
||||
}
|
||||
if schema is not None
|
||||
else None
|
||||
)
|
||||
for shard_idx, files_iterable in enumerate(files_iterables):
|
||||
for file in files_iterable:
|
||||
csv_file_reader = pd.read_csv(file, iterator=True, dtype=dtype, **self.config.pd_read_csv_kwargs)
|
||||
try:
|
||||
for batch_idx, df in enumerate(csv_file_reader):
|
||||
pa_table = pa.Table.from_pandas(df)
|
||||
# Uncomment for debugging (will print the Arrow table size and elements)
|
||||
# logger.warning(f"pa_table: {pa_table} num rows: {pa_table.num_rows}")
|
||||
# logger.warning('\n'.join(str(pa_table.slice(i, 1).to_pydict()) for i in range(pa_table.num_rows)))
|
||||
yield Key(shard_idx, batch_idx), self._cast_table(pa_table)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to read file '{file}' with error {type(e)}: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,77 @@
|
||||
import json
|
||||
import os
|
||||
from itertools import islice
|
||||
from typing import Iterable
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
class Eval(datasets.GeneratorBasedBuilder):
|
||||
NUM_EXAMPLES_FOR_FEATURES_INFERENCE = 5
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo()
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""We handle string, list and dicts in datafiles"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
base_data_files = dl_manager.download(self.config.data_files)
|
||||
extracted_data_files = dl_manager.extract(base_data_files)
|
||||
splits = []
|
||||
for split_name, logs in extracted_data_files.items():
|
||||
logs_files_iterables = [dl_manager.iter_files(log) for log in logs]
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={
|
||||
"logs_files_iterables": logs_files_iterables,
|
||||
"base_files": base_data_files[split_name],
|
||||
},
|
||||
)
|
||||
)
|
||||
if not self.info.features:
|
||||
first_examples = list(
|
||||
islice(
|
||||
self._iter_samples_from_log_files(logs_files_iterables[0]),
|
||||
self.NUM_EXAMPLES_FOR_FEATURES_INFERENCE,
|
||||
)
|
||||
)
|
||||
pa_tables = [pa.Table.from_pylist([example]) for example in first_examples]
|
||||
inferred_arrow_schema = pa.concat_tables(pa_tables, promote_options="default").schema
|
||||
self.info.features = datasets.Features.from_arrow_schema(inferred_arrow_schema)
|
||||
|
||||
return splits
|
||||
|
||||
def _sort_samples_key(self, sample_path: str):
|
||||
# looks like "{sample_idx}_epoch_{epoch_idx}""
|
||||
(sample_idx_str, epoch_idx_str) = os.path.splitext(os.path.basename(sample_path))[0].split("_epoch_")
|
||||
return (int(epoch_idx_str), int(sample_idx_str))
|
||||
|
||||
def _iter_samples_from_log_files(self, log_files: Iterable[str]):
|
||||
sample_files = [log_file for log_file in log_files if os.path.basename(os.path.dirname(log_file)) == "samples"]
|
||||
sample_files.sort(key=self._sort_samples_key)
|
||||
for sample_file in sample_files:
|
||||
with open(sample_file) as f:
|
||||
sample = json.load(f)
|
||||
for field in sample:
|
||||
if isinstance(sample[field], dict):
|
||||
sample[field] = json.dumps(sample[field])
|
||||
if isinstance(sample[field], list):
|
||||
sample[field] = [json.dumps(x) for x in sample[field]]
|
||||
yield sample
|
||||
|
||||
def _generate_shards(self, base_files, logs_files_iterables):
|
||||
yield from base_files
|
||||
|
||||
def _generate_examples(self, base_files, logs_files_iterables):
|
||||
for file_idx, log_files in enumerate(logs_files_iterables):
|
||||
for sample_idx, sample in enumerate(self._iter_samples_from_log_files(log_files)):
|
||||
yield Key(file_idx, sample_idx), sample
|
||||
@@ -0,0 +1,444 @@
|
||||
import collections
|
||||
import io
|
||||
import itertools
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Iterator, Optional, Union
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as ds
|
||||
import pyarrow.json as paj
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
import datasets
|
||||
from datasets import config
|
||||
from datasets.builder import Key
|
||||
from datasets.features.features import FeatureType, _visit, _visit_with_path, _VisitPath, require_storage_cast
|
||||
from datasets.utils.file_utils import readline
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
def count_path_segments(path):
|
||||
return path.replace("\\", "/").count("/")
|
||||
|
||||
|
||||
@dataclass
|
||||
class FolderBasedBuilderConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for AutoFolder."""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
drop_labels: bool = None
|
||||
drop_metadata: bool = None
|
||||
metadata_filenames: list[str] = None
|
||||
filters: Optional[Union[ds.Expression, list[tuple], list[list[tuple]]]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class FolderBasedBuilder(datasets.GeneratorBasedBuilder):
|
||||
"""
|
||||
Base class for generic data loaders for vision and image data.
|
||||
|
||||
|
||||
Abstract class attributes to be overridden by a child class:
|
||||
BASE_FEATURE: feature object to decode data (i.e. datasets.Image, datasets.Audio, ...)
|
||||
BASE_COLUMN_NAME: string key name of a base feature (i.e. "image", "audio", ...)
|
||||
BUILDER_CONFIG_CLASS: builder config inherited from `folder_based_builder.FolderBasedBuilderConfig`
|
||||
EXTENSIONS: list of allowed extensions (only files with these extensions and METADATA_FILENAME files
|
||||
will be included in a dataset)
|
||||
"""
|
||||
|
||||
BASE_FEATURE: type[FeatureType]
|
||||
BASE_COLUMN_NAME: str
|
||||
BUILDER_CONFIG_CLASS: FolderBasedBuilderConfig
|
||||
EXTENSIONS: list[str]
|
||||
|
||||
METADATA_FILENAMES: list[str] = ["metadata.csv", "metadata.jsonl", "metadata.parquet"]
|
||||
|
||||
def _info(self):
|
||||
if not self.config.data_dir and not self.config.data_files:
|
||||
raise ValueError(
|
||||
"Folder-based datasets require either `data_dir` or `data_files` to be specified. "
|
||||
"Neither was provided."
|
||||
)
|
||||
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
# Do an early pass if:
|
||||
# * `drop_labels` is None (default) or False, to infer the class labels
|
||||
# * `drop_metadata` is None (default) or False, to find the metadata files
|
||||
do_analyze = not self.config.drop_labels or not self.config.drop_metadata
|
||||
labels, path_depths = set(), set()
|
||||
all_metadata_files = collections.defaultdict(set)
|
||||
metadata_filenames = self.config.metadata_filenames or self.METADATA_FILENAMES
|
||||
|
||||
def analyze(files_or_archives, downloaded_files_or_dirs, split):
|
||||
if len(downloaded_files_or_dirs) == 0:
|
||||
return
|
||||
# The files are separated from the archives at this point, so check the first sample
|
||||
# to see if it's a file or a directory and iterate accordingly
|
||||
if os.path.isfile(downloaded_files_or_dirs[0]):
|
||||
original_files, downloaded_files = files_or_archives, downloaded_files_or_dirs
|
||||
for original_file, downloaded_file in zip(original_files, downloaded_files):
|
||||
original_file, downloaded_file = str(original_file), str(downloaded_file)
|
||||
_, original_file_ext = os.path.splitext(original_file)
|
||||
if original_file_ext.lower() in self.EXTENSIONS:
|
||||
if not self.config.drop_labels:
|
||||
labels.add(os.path.basename(os.path.dirname(original_file)))
|
||||
path_depths.add(count_path_segments(original_file))
|
||||
elif os.path.basename(original_file) in metadata_filenames:
|
||||
all_metadata_files[split].add((original_file, None, downloaded_file))
|
||||
else:
|
||||
original_file_name = os.path.basename(original_file)
|
||||
logger.debug(
|
||||
f"The file '{original_file_name}' was ignored: it is not a {self.BASE_COLUMN_NAME}, and is not {metadata_filenames} either."
|
||||
)
|
||||
else:
|
||||
archives, downloaded_dirs = files_or_archives, downloaded_files_or_dirs
|
||||
for archive, downloaded_dir in zip(archives, downloaded_dirs):
|
||||
archive, downloaded_dir = str(archive), str(downloaded_dir)
|
||||
for downloaded_dir_file in dl_manager.iter_files(downloaded_dir):
|
||||
_, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
|
||||
if downloaded_dir_file_ext in self.EXTENSIONS:
|
||||
if not self.config.drop_labels:
|
||||
labels.add(os.path.basename(os.path.dirname(downloaded_dir_file)))
|
||||
path_depths.add(count_path_segments(downloaded_dir_file))
|
||||
elif os.path.basename(downloaded_dir_file) in metadata_filenames:
|
||||
all_metadata_files[split].add((None, downloaded_dir, downloaded_dir_file))
|
||||
else:
|
||||
archive_file_name = os.path.basename(archive)
|
||||
original_file_name = os.path.basename(downloaded_dir_file)
|
||||
logger.debug(
|
||||
f"The file '{original_file_name}' from the archive '{archive_file_name}' was ignored: it is not a {self.BASE_COLUMN_NAME}, and is not {metadata_filenames} either."
|
||||
)
|
||||
|
||||
data_files = self.config.data_files
|
||||
splits = []
|
||||
for split_name, files in data_files.items():
|
||||
files, metadata_files, archives = self._split_files_and_metadata_and_archives(files)
|
||||
downloaded_files = dl_manager.download(files)
|
||||
downloaded_metadata_files = dl_manager.download(metadata_files)
|
||||
downloaded_dirs = dl_manager.download_and_extract(archives)
|
||||
if do_analyze: # drop_metadata is None or False, drop_labels is None or False
|
||||
logger.info(f"Searching for labels and/or metadata files in {split_name} data files...")
|
||||
analyze(files, downloaded_files, split_name)
|
||||
analyze(metadata_files, downloaded_metadata_files, split_name)
|
||||
analyze(archives, downloaded_dirs, split_name)
|
||||
|
||||
if all_metadata_files:
|
||||
# add metadata if `all_metadata_files` are found and `drop_metadata` is None (default) or False
|
||||
add_metadata = not self.config.drop_metadata
|
||||
# if `all_metadata_files` are found, don't add labels
|
||||
add_labels = False
|
||||
else:
|
||||
# if `all_metadata_files` are not found, don't add metadata
|
||||
add_metadata = False
|
||||
# if `all_metadata_files` are not found and `drop_labels` is None (default) -
|
||||
# add labels if files are on the same level in directory hierarchy and there is more than one label
|
||||
add_labels = (
|
||||
(len(labels) > 1 and len(path_depths) == 1)
|
||||
if self.config.drop_labels is None
|
||||
else not self.config.drop_labels
|
||||
)
|
||||
|
||||
if add_labels:
|
||||
logger.info("Adding the labels inferred from data directories to the dataset's features...")
|
||||
if add_metadata:
|
||||
logger.info("Adding metadata to the dataset...")
|
||||
else:
|
||||
add_labels, add_metadata, all_metadata_files = False, False, {}
|
||||
|
||||
# files info (original_file, None, downloaded_file)
|
||||
files = tuple(zip(files, [None] * len(files), downloaded_files))
|
||||
# archives info (original_archive_file, downloaded_dir, downloaded_files)
|
||||
files += tuple(
|
||||
(archive, downloaded_dir, dl_manager.iter_files(downloaded_dir))
|
||||
for archive, downloaded_dir in zip(archives, downloaded_dirs)
|
||||
)
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={
|
||||
"files": files,
|
||||
"metadata_files": all_metadata_files.get(split_name, []),
|
||||
"add_labels": add_labels,
|
||||
"add_metadata": add_metadata,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if add_metadata:
|
||||
# Verify that:
|
||||
# * all metadata files have the same set of features in each split
|
||||
# * the `file_name` key is one of the metadata keys and is of type string
|
||||
features_per_metadata_file: list[tuple[str, datasets.Features]] = []
|
||||
|
||||
# Check that all metadata files share the same format
|
||||
metadata_ext = {
|
||||
os.path.splitext(original_metadata_file or downloaded_metadata_file)[-1]
|
||||
for original_metadata_file, _, downloaded_metadata_file in itertools.chain.from_iterable(
|
||||
all_metadata_files.values()
|
||||
)
|
||||
}
|
||||
if len(metadata_ext) > 1:
|
||||
raise ValueError(f"Found metadata files with different extensions: {list(metadata_ext)}")
|
||||
metadata_ext = metadata_ext.pop()
|
||||
|
||||
for split_metadata_files in all_metadata_files.values():
|
||||
pa_metadata_table = None
|
||||
for _, _, downloaded_metadata_file in split_metadata_files:
|
||||
for pa_metadata_table in self._read_metadata(downloaded_metadata_file, metadata_ext=metadata_ext):
|
||||
break # just fetch the first rows
|
||||
if pa_metadata_table is not None:
|
||||
features_per_metadata_file.append(
|
||||
(downloaded_metadata_file, datasets.Features.from_arrow_schema(pa_metadata_table.schema))
|
||||
)
|
||||
break # no need to fetch all the files
|
||||
for downloaded_metadata_file, metadata_features in features_per_metadata_file:
|
||||
if metadata_features != features_per_metadata_file[0][1]:
|
||||
raise ValueError(
|
||||
f"Metadata files {downloaded_metadata_file} and {features_per_metadata_file[0][0]} have different features: {features_per_metadata_file[0]} != {metadata_features}"
|
||||
)
|
||||
metadata_features = features_per_metadata_file[0][1]
|
||||
feature_not_found = True
|
||||
|
||||
def _set_feature(feature):
|
||||
nonlocal feature_not_found
|
||||
if isinstance(feature, dict):
|
||||
out = type(feature)()
|
||||
for key in feature:
|
||||
if (key == "file_name" or key.endswith("_file_name")) and (
|
||||
feature[key] == datasets.Value("string") or feature[key] == datasets.Value("large_string")
|
||||
):
|
||||
key = key[: -len("_file_name")] or self.BASE_COLUMN_NAME
|
||||
out[key] = self.BASE_FEATURE()
|
||||
feature_not_found = False
|
||||
elif (key == "file_names" or key.endswith("_file_names")) and (
|
||||
feature[key]
|
||||
in [datasets.List(datasets.Value("string")), datasets.List(datasets.Value("large_string"))]
|
||||
):
|
||||
key = key[: -len("_file_names")] or (self.BASE_COLUMN_NAME + "s")
|
||||
out[key] = datasets.List(self.BASE_FEATURE())
|
||||
feature_not_found = False
|
||||
elif (key == "file_names" or key.endswith("_file_names")) and (
|
||||
feature[key] == [datasets.Value("string")]
|
||||
or feature[key] == [datasets.Value("large_string")]
|
||||
):
|
||||
key = key[: -len("_file_names")] or (self.BASE_COLUMN_NAME + "s")
|
||||
out[key] = [self.BASE_FEATURE()]
|
||||
feature_not_found = False
|
||||
else:
|
||||
out[key] = feature[key]
|
||||
return out
|
||||
return feature
|
||||
|
||||
metadata_features = _visit(metadata_features, _set_feature)
|
||||
|
||||
if feature_not_found:
|
||||
raise ValueError(
|
||||
"`file_name`, `*_file_name`, `file_names` or `*_file_names` must be present as dictionary key in metadata files"
|
||||
)
|
||||
else:
|
||||
metadata_features = None
|
||||
|
||||
# Normally, we would do this in _info, but we need to know the labels and/or metadata
|
||||
# before building the features
|
||||
if self.config.features is None:
|
||||
if add_metadata:
|
||||
self.info.features = metadata_features
|
||||
elif add_labels:
|
||||
self.info.features = datasets.Features(
|
||||
{
|
||||
self.BASE_COLUMN_NAME: self.BASE_FEATURE(),
|
||||
"label": datasets.ClassLabel(names=sorted(labels)),
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.info.features = datasets.Features({self.BASE_COLUMN_NAME: self.BASE_FEATURE()})
|
||||
|
||||
return splits
|
||||
|
||||
def _split_files_and_metadata_and_archives(self, data_files):
|
||||
files, metadata_files, archives = [], [], []
|
||||
metadata_filenames = self.config.metadata_filenames or self.METADATA_FILENAMES
|
||||
for data_file in data_files:
|
||||
data_file_root, data_file_ext = os.path.splitext(data_file)
|
||||
_, second_data_file_ext = os.path.splitext(data_file_root)
|
||||
if data_file_ext.lower() in self.EXTENSIONS or second_data_file_ext.lower() in self.EXTENSIONS:
|
||||
files.append(data_file)
|
||||
elif os.path.basename(data_file) in metadata_filenames:
|
||||
metadata_files.append(data_file)
|
||||
elif data_file_ext.lower() == ".zip":
|
||||
archives.append(data_file)
|
||||
return files, metadata_files, archives
|
||||
|
||||
def _read_metadata(self, metadata_file: str, metadata_ext: str = "") -> Iterator[pa.Table]:
|
||||
"""using the same logic as the Csv, Json and Parquet dataset builders to stream the data"""
|
||||
if self.config.filters is not None:
|
||||
filter_expr = (
|
||||
pq.filters_to_expression(self.config.filters)
|
||||
if isinstance(self.config.filters, list)
|
||||
else self.config.filters
|
||||
)
|
||||
else:
|
||||
filter_expr = None
|
||||
if metadata_ext == ".csv":
|
||||
chunksize = 10_000 # 10k lines
|
||||
schema = self.config.features.arrow_schema if self.config.features else None
|
||||
# dtype allows reading an int column as str
|
||||
dtype = (
|
||||
{
|
||||
name: dtype.to_pandas_dtype() if not require_storage_cast(feature) else object
|
||||
for name, dtype, feature in zip(schema.names, schema.types, self.config.features.values())
|
||||
}
|
||||
if schema is not None
|
||||
else None
|
||||
)
|
||||
csv_file_reader = pd.read_csv(metadata_file, iterator=True, dtype=dtype, chunksize=chunksize)
|
||||
for df in csv_file_reader:
|
||||
pa_table = pa.Table.from_pandas(df)
|
||||
if self.config.filters is not None:
|
||||
pa_table = pa_table.filter(filter_expr)
|
||||
if len(pa_table) > 0:
|
||||
yield pa_table
|
||||
elif metadata_ext == ".jsonl":
|
||||
with open(metadata_file, "rb") as f:
|
||||
chunksize: int = 10 << 20 # 10MB
|
||||
# Use block_size equal to the chunk size divided by 32 to leverage multithreading
|
||||
# Set a default minimum value of 16kB if the chunk size is really small
|
||||
block_size = max(chunksize // 32, 16 << 10)
|
||||
while True:
|
||||
batch = f.read(chunksize)
|
||||
if not batch:
|
||||
break
|
||||
# Finish current line
|
||||
try:
|
||||
batch += f.readline()
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
batch += readline(f)
|
||||
while True:
|
||||
try:
|
||||
pa_table = paj.read_json(
|
||||
io.BytesIO(batch), read_options=paj.ReadOptions(block_size=block_size)
|
||||
)
|
||||
break
|
||||
except (pa.ArrowInvalid, pa.ArrowNotImplementedError) as e:
|
||||
if (
|
||||
isinstance(e, pa.ArrowInvalid)
|
||||
and "straddling" not in str(e)
|
||||
or block_size > len(batch)
|
||||
):
|
||||
raise
|
||||
else:
|
||||
# Increase the block size in case it was too small.
|
||||
# The block size will be reset for the next file.
|
||||
logger.debug(
|
||||
f"Batch of {len(batch)} bytes couldn't be parsed with block_size={block_size}. Retrying with block_size={block_size * 2}."
|
||||
)
|
||||
block_size *= 2
|
||||
if self.config.filters is not None:
|
||||
pa_table = pa_table.filter(filter_expr)
|
||||
if len(pa_table) > 0:
|
||||
yield pa_table
|
||||
else:
|
||||
with open(metadata_file, "rb") as f:
|
||||
parquet_fragment = ds.ParquetFileFormat().make_fragment(f)
|
||||
if parquet_fragment.row_groups:
|
||||
batch_size = parquet_fragment.row_groups[0].num_rows
|
||||
else:
|
||||
batch_size = config.DEFAULT_MAX_BATCH_SIZE
|
||||
for record_batch in parquet_fragment.to_batches(
|
||||
batch_size=batch_size,
|
||||
filter=filter_expr,
|
||||
batch_readahead=0,
|
||||
fragment_readahead=0,
|
||||
):
|
||||
yield pa.Table.from_batches([record_batch])
|
||||
|
||||
def _generate_shards(self, files, metadata_files, add_metadata, add_labels):
|
||||
if add_metadata:
|
||||
for _, _, downloaded_metadata_file in metadata_files:
|
||||
yield downloaded_metadata_file
|
||||
else:
|
||||
for _, downloaded_dir, downloaded_file in files:
|
||||
yield downloaded_dir or downloaded_file
|
||||
|
||||
def _generate_examples(self, files, metadata_files, add_metadata, add_labels):
|
||||
if add_metadata:
|
||||
feature_paths = []
|
||||
|
||||
def find_feature_path(feature, feature_path):
|
||||
nonlocal feature_paths
|
||||
if feature_path and isinstance(feature, self.BASE_FEATURE):
|
||||
feature_paths.append(feature_path)
|
||||
|
||||
_visit_with_path(self.info.features, find_feature_path)
|
||||
|
||||
for shard_idx, metadata_file_info in enumerate(metadata_files):
|
||||
if len(metadata_file_info) == 2:
|
||||
original_metadata_file, downloaded_metadata_file = metadata_file_info
|
||||
else:
|
||||
original_metadata_file, downloaded_metadata_dir, downloaded_metadata_file = metadata_file_info
|
||||
metadata_ext = os.path.splitext(original_metadata_file or downloaded_metadata_file)[-1]
|
||||
downloaded_metadata_dir = os.path.dirname(downloaded_metadata_file)
|
||||
|
||||
def set_feature(item, feature_path: _VisitPath):
|
||||
if len(feature_path) == 2 and isinstance(feature_path[0], str) and feature_path[1] == 0:
|
||||
item[feature_path[0]] = item.pop("file_names", None) or item.pop(
|
||||
feature_path[0] + "_file_names", None
|
||||
)
|
||||
elif len(feature_path) == 1 and isinstance(feature_path[0], str):
|
||||
item[feature_path[0]] = item.pop("file_name", None) or item.pop(
|
||||
feature_path[0] + "_file_name", None
|
||||
)
|
||||
elif len(feature_path) == 0:
|
||||
if item is not None:
|
||||
file_relpath = os.path.normpath(item).replace("\\", "/")
|
||||
item = os.path.join(downloaded_metadata_dir, file_relpath)
|
||||
return item
|
||||
|
||||
for pa_metadata_table in self._read_metadata(downloaded_metadata_file, metadata_ext=metadata_ext):
|
||||
for sample_idx, sample in enumerate(pa_metadata_table.to_pylist()):
|
||||
for feature_path in feature_paths:
|
||||
_nested_apply(sample, feature_path, set_feature)
|
||||
yield Key(shard_idx, sample_idx), sample
|
||||
else:
|
||||
if self.config.filters is not None:
|
||||
filter_expr = (
|
||||
pq.filters_to_expression(self.config.filters)
|
||||
if isinstance(self.config.filters, list)
|
||||
else self.config.filters
|
||||
)
|
||||
for shard_idx, (original_file, _, downloaded_files) in enumerate(files):
|
||||
if isinstance(downloaded_files, str):
|
||||
downloaded_files = [downloaded_files]
|
||||
for sample_idx, downloaded_file in enumerate(downloaded_files):
|
||||
sample = {self.BASE_COLUMN_NAME: downloaded_file}
|
||||
if add_labels:
|
||||
sample["label"] = os.path.basename(os.path.dirname(original_file or downloaded_file))
|
||||
if self.config.filters is not None:
|
||||
pa_table = pa.Table.from_pylist([sample]).filter(filter_expr)
|
||||
if len(pa_table) == 0:
|
||||
continue
|
||||
yield Key(shard_idx, sample_idx), sample
|
||||
|
||||
|
||||
def _nested_apply(item: Any, feature_path: _VisitPath, func: Callable[[Any, _VisitPath], Any]):
|
||||
# see _visit_with_path() to see how feature paths are constructed
|
||||
item = func(item, feature_path)
|
||||
if feature_path:
|
||||
key = feature_path[0]
|
||||
if key == 0:
|
||||
for i in range(len(item)):
|
||||
item[i] = _nested_apply(item[i], feature_path[1:], func)
|
||||
else:
|
||||
item[key] = _nested_apply(item[key], feature_path[1:], func)
|
||||
return item
|
||||
@@ -0,0 +1,38 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.utils.sharding import _number_of_shards_in_gen_kwargs, _split_gen_kwargs
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratorConfig(datasets.BuilderConfig):
|
||||
generator: Optional[Callable] = None
|
||||
gen_kwargs: Optional[dict] = None
|
||||
features: Optional[datasets.Features] = None
|
||||
split: datasets.NamedSplit = datasets.Split.TRAIN
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.generator is None:
|
||||
raise ValueError("generator must be specified")
|
||||
|
||||
if self.gen_kwargs is None:
|
||||
self.gen_kwargs = {}
|
||||
|
||||
|
||||
class Generator(datasets.GeneratorBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = GeneratorConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
return [datasets.SplitGenerator(name=self.config.split, gen_kwargs=self.config.gen_kwargs)]
|
||||
|
||||
def _generate_examples(self, **gen_kwargs):
|
||||
num_shards = _number_of_shards_in_gen_kwargs(gen_kwargs)
|
||||
for shard_idx, shard_gen_kwargs in enumerate(_split_gen_kwargs(gen_kwargs, max_num_jobs=num_shards)):
|
||||
for sample_idx, sample in enumerate(self.config.generator(**shard_gen_kwargs)):
|
||||
yield Key(shard_idx, sample_idx), sample
|
||||
@@ -0,0 +1,389 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.features.features import (
|
||||
Array2D,
|
||||
Array3D,
|
||||
Array4D,
|
||||
Array5D,
|
||||
Features,
|
||||
LargeList,
|
||||
List,
|
||||
Value,
|
||||
_ArrayXD,
|
||||
_arrow_to_datasets_dtype,
|
||||
)
|
||||
from datasets.table import cast_table_to_features
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import h5py
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
EXTENSIONS = [".h5", ".hdf5"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HDF5Config(datasets.BuilderConfig):
|
||||
"""BuilderConfig for HDF5."""
|
||||
|
||||
batch_size: Optional[int] = None
|
||||
features: Optional[datasets.Features] = None
|
||||
|
||||
|
||||
class HDF5(datasets.ArrowBasedBuilder):
|
||||
"""ArrowBasedBuilder that converts HDF5 files to Arrow tables using the HF extension types."""
|
||||
|
||||
BUILDER_CONFIG_CLASS = HDF5Config
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
import h5py
|
||||
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
data_files = dl_manager.download(self.config.data_files)
|
||||
splits = []
|
||||
for split_name, files in data_files.items():
|
||||
# Infer features from first file
|
||||
if self.info.features is None:
|
||||
for first_file in files:
|
||||
with open(first_file, "rb") as f:
|
||||
with h5py.File(f, "r") as h5:
|
||||
self.info.features = _recursive_infer_features(h5)
|
||||
break
|
||||
splits.append(datasets.SplitGenerator(name=split_name, gen_kwargs={"files": files}))
|
||||
return splits
|
||||
|
||||
def _generate_shards(self, files):
|
||||
yield from files
|
||||
|
||||
def _generate_tables(self, files):
|
||||
import h5py
|
||||
|
||||
batch_size_cfg = self.config.batch_size
|
||||
for file_idx, file in enumerate(files):
|
||||
try:
|
||||
with open(file, "rb") as f:
|
||||
with h5py.File(f, "r") as h5:
|
||||
# Infer features and lengths from first file
|
||||
if self.info.features is None:
|
||||
self.info.features = _recursive_infer_features(h5)
|
||||
num_rows = _check_dataset_lengths(h5, self.info.features)
|
||||
if num_rows is None:
|
||||
logger.warning(f"File {file} contains no data, skipping...")
|
||||
continue
|
||||
effective_batch = batch_size_cfg or self._writer_batch_size or num_rows
|
||||
for batch_idx, start in enumerate(range(0, num_rows, effective_batch)):
|
||||
end = min(start + effective_batch, num_rows)
|
||||
pa_table = _recursive_load_arrays(h5, self.info.features, start, end)
|
||||
if pa_table is None:
|
||||
logger.warning(f"File {file} contains no data, skipping...")
|
||||
continue
|
||||
yield Key(file_idx, batch_idx), cast_table_to_features(pa_table, self.info.features)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to read file '{file}' with error {type(e)}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# ┌───────────┐
|
||||
# │ Complex │
|
||||
# └───────────┘
|
||||
|
||||
|
||||
def _is_complex_dtype(dtype: np.dtype) -> bool:
|
||||
if dtype.kind == "c":
|
||||
return True
|
||||
if dtype.subdtype is not None:
|
||||
return _is_complex_dtype(dtype.subdtype[0])
|
||||
return False
|
||||
|
||||
|
||||
def _create_complex_features(dset) -> Features:
|
||||
if dset.dtype.subdtype is not None:
|
||||
dtype, data_shape = dset.dtype.subdtype
|
||||
else:
|
||||
data_shape = dset.shape[1:]
|
||||
dtype = dset.dtype
|
||||
|
||||
if dtype == np.complex64:
|
||||
# two float32s
|
||||
value_type = Value("float32")
|
||||
elif dtype == np.complex128:
|
||||
# two float64s
|
||||
value_type = Value("float64")
|
||||
else:
|
||||
logger.warning(f"Found complex dtype {dtype} that is not supported. Converting to float64...")
|
||||
value_type = Value("float64")
|
||||
|
||||
return Features(
|
||||
{
|
||||
"real": _create_sized_feature_impl(data_shape, value_type),
|
||||
"imag": _create_sized_feature_impl(data_shape, value_type),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _convert_complex_to_nested(arr: np.ndarray) -> pa.StructArray:
|
||||
data = {
|
||||
"real": datasets.features.features.numpy_to_pyarrow_listarray(arr.real),
|
||||
"imag": datasets.features.features.numpy_to_pyarrow_listarray(arr.imag),
|
||||
}
|
||||
return pa.StructArray.from_arrays([data["real"], data["imag"]], names=["real", "imag"])
|
||||
|
||||
|
||||
# ┌────────────┐
|
||||
# │ Compound │
|
||||
# └────────────┘
|
||||
|
||||
|
||||
def _is_compound_dtype(dtype: np.dtype) -> bool:
|
||||
return dtype.kind == "V"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _CompoundGroup:
|
||||
dset: "h5py.Dataset"
|
||||
data: np.ndarray = None
|
||||
|
||||
def items(self):
|
||||
for field_name in self.dset.dtype.names:
|
||||
field_dtype = self.dset.dtype[field_name]
|
||||
yield field_name, _CompoundField(self.data, field_name, field_dtype)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _CompoundField:
|
||||
data: Optional[np.ndarray]
|
||||
name: str
|
||||
dtype: np.dtype
|
||||
shape: tuple[int, ...] = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.shape = (len(self.data) if self.data is not None else 0,) + self.dtype.shape
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.data[key][self.name]
|
||||
|
||||
|
||||
def _create_compound_features(dset) -> Features:
|
||||
mock_group = _CompoundGroup(dset)
|
||||
return _recursive_infer_features(mock_group)
|
||||
|
||||
|
||||
def _convert_compound_to_nested(arr, dset) -> pa.StructArray:
|
||||
mock_group = _CompoundGroup(dset, data=arr)
|
||||
features = _create_compound_features(dset)
|
||||
return _recursive_load_arrays(mock_group, features, 0, len(arr))
|
||||
|
||||
|
||||
# ┌───────────────────┐
|
||||
# │ Variable-Length │
|
||||
# └───────────────────┘
|
||||
|
||||
|
||||
def _is_vlen_dtype(dtype: np.dtype) -> bool:
|
||||
if dtype.metadata and "vlen" in dtype.metadata:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _create_vlen_features(dset) -> Features:
|
||||
vlen_dtype = dset.dtype.metadata["vlen"]
|
||||
if vlen_dtype in (str, bytes):
|
||||
return Value("string")
|
||||
inner_feature = _np_to_pa_to_hf_value(vlen_dtype)
|
||||
return List(inner_feature)
|
||||
|
||||
|
||||
def _convert_vlen_to_array(arr: np.ndarray) -> pa.Array:
|
||||
return datasets.features.features.numpy_to_pyarrow_listarray(arr)
|
||||
|
||||
|
||||
# ┌───────────┐
|
||||
# │ Generic │
|
||||
# └───────────┘
|
||||
|
||||
|
||||
def _recursive_infer_features(h5_obj) -> Features:
|
||||
features_dict = {}
|
||||
for path, dset in h5_obj.items():
|
||||
if _is_group(dset):
|
||||
features = _recursive_infer_features(dset)
|
||||
if features:
|
||||
features_dict[path] = features
|
||||
elif _is_dataset(dset):
|
||||
features = _infer_feature(dset)
|
||||
if features:
|
||||
features_dict[path] = features
|
||||
|
||||
return Features(features_dict)
|
||||
|
||||
|
||||
def _infer_feature(dset):
|
||||
if _is_complex_dtype(dset.dtype):
|
||||
return _create_complex_features(dset)
|
||||
elif _is_compound_dtype(dset.dtype) or dset.dtype.kind == "V":
|
||||
return _create_compound_features(dset)
|
||||
elif _is_vlen_dtype(dset.dtype):
|
||||
return _create_vlen_features(dset)
|
||||
return _create_sized_feature(dset)
|
||||
|
||||
|
||||
def _load_array(dset, path: str, start: int, end: int) -> pa.Array:
|
||||
arr = dset[start:end]
|
||||
|
||||
if _is_vlen_dtype(dset.dtype):
|
||||
return _convert_vlen_to_array(arr)
|
||||
elif _is_complex_dtype(dset.dtype):
|
||||
return _convert_complex_to_nested(arr)
|
||||
elif _is_compound_dtype(dset.dtype):
|
||||
return _convert_compound_to_nested(arr, dset)
|
||||
elif dset.dtype.kind == "O":
|
||||
raise ValueError(
|
||||
f"Object dtype dataset '{path}' is not supported. "
|
||||
f"For variable-length data, please use h5py.vlen_dtype() "
|
||||
f"when creating the HDF5 file. "
|
||||
f"See: https://docs.h5py.org/en/stable/special.html#variable-length-strings"
|
||||
)
|
||||
else:
|
||||
# If any non-batch dimension is zero, emit an unsized pa.list_
|
||||
# to avoid creating FixedSizeListArray with list_size=0.
|
||||
if any(dim == 0 for dim in dset.shape[1:]):
|
||||
inner_type = pa.from_numpy_dtype(dset.dtype)
|
||||
return pa.array([[] for _ in arr], type=pa.list_(inner_type))
|
||||
else:
|
||||
return datasets.features.features.numpy_to_pyarrow_listarray(arr)
|
||||
|
||||
|
||||
def _recursive_load_arrays(h5_obj, features: Features, start: int, end: int):
|
||||
batch_dict = {}
|
||||
for path, dset in h5_obj.items():
|
||||
if path not in features:
|
||||
continue
|
||||
if _is_group(dset):
|
||||
arr = _recursive_load_arrays(dset, features[path], start, end)
|
||||
elif _is_dataset(dset):
|
||||
arr = _load_array(dset, path, start, end)
|
||||
else:
|
||||
raise ValueError(f"Unexpected type {type(dset)}")
|
||||
|
||||
if arr is not None:
|
||||
batch_dict[path] = arr
|
||||
|
||||
if _is_file(h5_obj):
|
||||
return pa.Table.from_pydict(batch_dict)
|
||||
|
||||
if batch_dict:
|
||||
should_chunk, keys, values = False, [], []
|
||||
for k, v in batch_dict.items():
|
||||
if isinstance(v, pa.ChunkedArray):
|
||||
should_chunk = True
|
||||
v = v.combine_chunks()
|
||||
keys.append(k)
|
||||
values.append(v)
|
||||
|
||||
sarr = pa.StructArray.from_arrays(values, names=keys)
|
||||
return pa.chunked_array(sarr) if should_chunk else sarr
|
||||
|
||||
|
||||
# ┌─────────────┐
|
||||
# │ Utilities │
|
||||
# └─────────────┘
|
||||
|
||||
|
||||
def _create_sized_feature(dset):
|
||||
dset_shape = dset.shape[1:]
|
||||
value_feature = _np_to_pa_to_hf_value(dset.dtype)
|
||||
return _create_sized_feature_impl(dset_shape, value_feature)
|
||||
|
||||
|
||||
def _create_sized_feature_impl(dset_shape, value_feature):
|
||||
dtype_str = value_feature.dtype
|
||||
if any(dim == 0 for dim in dset_shape):
|
||||
logger.warning(
|
||||
f"HDF5 to Arrow: Found a dataset with shape {dset_shape} and dtype {dtype_str} that has a dimension with size 0. Shape information will be lost in the conversion to List({value_feature})."
|
||||
)
|
||||
return List(value_feature)
|
||||
|
||||
rank = len(dset_shape)
|
||||
if rank == 0:
|
||||
return value_feature
|
||||
elif rank == 1:
|
||||
return List(value_feature, length=dset_shape[0])
|
||||
elif rank <= 5:
|
||||
return _sized_arrayxd(rank)(shape=dset_shape, dtype=dtype_str)
|
||||
else:
|
||||
raise TypeError(f"Array{rank}D not supported. Maximum 5 dimensions allowed.")
|
||||
|
||||
|
||||
def _sized_arrayxd(rank: int):
|
||||
return {2: Array2D, 3: Array3D, 4: Array4D, 5: Array5D}[rank]
|
||||
|
||||
|
||||
def _np_to_pa_to_hf_value(numpy_dtype: np.dtype) -> Value:
|
||||
return Value(dtype=_arrow_to_datasets_dtype(pa.from_numpy_dtype(numpy_dtype)))
|
||||
|
||||
|
||||
def _first_dataset(h5_obj, features: Features, prefix=""):
|
||||
for path, dset in h5_obj.items():
|
||||
if path not in features:
|
||||
continue
|
||||
if _is_group(dset):
|
||||
found = _first_dataset(dset, features[path], prefix=f"{prefix}{path}/")
|
||||
if found is not None:
|
||||
return found
|
||||
elif _is_dataset(dset):
|
||||
return f"{prefix}{path}"
|
||||
|
||||
|
||||
def _check_dataset_lengths(h5_obj, features: Features) -> int:
|
||||
first_path = _first_dataset(h5_obj, features)
|
||||
if first_path is None:
|
||||
return None
|
||||
|
||||
num_rows = h5_obj[first_path].shape[0]
|
||||
for path, dset in h5_obj.items():
|
||||
if path not in features:
|
||||
continue
|
||||
if _is_dataset(dset):
|
||||
if dset.shape[0] != num_rows:
|
||||
raise ValueError(f"Dataset '{path}' has length {dset.shape[0]} but expected {num_rows}")
|
||||
return num_rows
|
||||
|
||||
|
||||
def _is_group(h5_obj) -> bool:
|
||||
import h5py
|
||||
|
||||
return isinstance(h5_obj, h5py.Group) or isinstance(h5_obj, _CompoundGroup)
|
||||
|
||||
|
||||
def _is_dataset(h5_obj) -> bool:
|
||||
import h5py
|
||||
|
||||
return isinstance(h5_obj, h5py.Dataset) or isinstance(h5_obj, _CompoundField)
|
||||
|
||||
|
||||
def _is_file(h5_obj) -> bool:
|
||||
import h5py
|
||||
|
||||
return isinstance(h5_obj, h5py.File)
|
||||
|
||||
|
||||
def _has_zero_dimensions(feature):
|
||||
if isinstance(feature, _ArrayXD):
|
||||
return any(dim == 0 for dim in feature.shape)
|
||||
elif isinstance(feature, List):
|
||||
return feature.length == 0 or _has_zero_dimensions(feature.feature)
|
||||
elif isinstance(feature, LargeList):
|
||||
return _has_zero_dimensions(feature.feature)
|
||||
else:
|
||||
return False
|
||||
@@ -0,0 +1,173 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.features import Features
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyiceberg.catalog import Catalog
|
||||
from pyiceberg.expressions import BooleanExpression
|
||||
from pyiceberg.table import FileScanTask
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IcebergConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for Apache Iceberg format.
|
||||
|
||||
Args:
|
||||
catalog (`pyiceberg.catalog.Catalog`):
|
||||
A pre-configured pyiceberg Catalog object.
|
||||
table (`str` or `Dict[str, str]`):
|
||||
Iceberg table identifier, e.g. ``"db.my_table"``.
|
||||
Pass a dict to map split names to table identifiers,
|
||||
e.g. ``{"train": "db.train", "test": "db.test"}``.
|
||||
features (`Features`, *optional*):
|
||||
Cast the data to these features.
|
||||
columns (`List[str]`, *optional*):
|
||||
List of columns to load; others are ignored.
|
||||
filters (`str` or `BooleanExpression`, *optional*):
|
||||
Row filter with predicate pushdown. Accepts a SQL-style string
|
||||
(``"col > 1 AND col2 == 'foo'"``), or a pyiceberg
|
||||
``BooleanExpression`` object. Parsed by pyiceberg internally.
|
||||
batch_size (`int`, defaults to ``131072``):
|
||||
Number of rows per RecordBatch when reading.
|
||||
snapshot_id (`int`, *optional*):
|
||||
Load a specific snapshot for time-travel queries.
|
||||
"""
|
||||
|
||||
catalog: Optional["Catalog"] = None
|
||||
table: Optional[Union[str, Dict[str, str]]] = None
|
||||
features: Optional[datasets.Features] = None
|
||||
columns: Optional[List[str]] = None
|
||||
filters: Optional[Union[str, "BooleanExpression"]] = None
|
||||
batch_size: int = 131072
|
||||
snapshot_id: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.catalog is None:
|
||||
raise ValueError("`catalog` must be a pyiceberg Catalog object, but got None.")
|
||||
if self.table is None:
|
||||
raise ValueError("`table` must be specified, e.g. table='db.my_table'")
|
||||
# Normalize table to Dict[split_name, table_identifier]
|
||||
if isinstance(self.table, str):
|
||||
self.table = {"train": self.table}
|
||||
# Generate a stable config name for caching
|
||||
if self.name == "default":
|
||||
catalog_id = f"{self.catalog.__class__.__name__}_{self.catalog.name}"
|
||||
table_id = "_".join(sorted(self.table.values()))
|
||||
self.name = f"{catalog_id}_{table_id}"
|
||||
|
||||
def create_config_id(
|
||||
self,
|
||||
config_kwargs: dict,
|
||||
custom_features: Optional[Features] = None,
|
||||
) -> str:
|
||||
# The catalog object is not picklable (contains SQLAlchemy engines, etc.),
|
||||
# so we replace it with a hashable string representation before the
|
||||
# parent class hashes config_kwargs via dill.
|
||||
config_kwargs = config_kwargs.copy()
|
||||
catalog = config_kwargs.pop("catalog", None)
|
||||
if catalog is not None:
|
||||
config_kwargs["_catalog_id"] = f"{catalog.__class__.__name__}_{catalog.name}"
|
||||
# filters may contain pyiceberg Expression objects that are not picklable
|
||||
filters = config_kwargs.pop("filters", None)
|
||||
if filters is not None:
|
||||
config_kwargs["_filters_repr"] = repr(filters)
|
||||
return super().create_config_id(config_kwargs, custom_features=custom_features)
|
||||
|
||||
|
||||
class Iceberg(datasets.ArrowBasedBuilder, datasets.builder._CountableBuilderMixin):
|
||||
BUILDER_CONFIG_CLASS = IcebergConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
splits = []
|
||||
for split_name, table_id in self.config.table.items():
|
||||
iceberg_table = self.config.catalog.load_table(table_id)
|
||||
|
||||
scan_kwargs = {}
|
||||
if self.config.filters is not None:
|
||||
scan_kwargs["row_filter"] = self.config.filters
|
||||
if self.config.columns:
|
||||
scan_kwargs["selected_fields"] = tuple(self.config.columns)
|
||||
if self.config.snapshot_id is not None:
|
||||
scan_kwargs["snapshot_id"] = self.config.snapshot_id
|
||||
|
||||
scan = iceberg_table.scan(**scan_kwargs)
|
||||
|
||||
# Infer features from Arrow schema if not user-provided
|
||||
if self.info.features is None:
|
||||
arrow_schema = scan.projection().as_arrow()
|
||||
self.info.features = datasets.Features.from_arrow_schema(arrow_schema)
|
||||
|
||||
# Plan files for parallel processing: passing a list in gen_kwargs
|
||||
# enables _split_gen_kwargs to distribute tasks across num_proc workers.
|
||||
tasks = list(scan.plan_files())
|
||||
|
||||
# Extract picklable scan context for multiprocessing compatibility.
|
||||
# The scan object itself is not picklable (holds catalog connections),
|
||||
# but these components are individually serializable.
|
||||
scan_context = (
|
||||
scan.table_metadata,
|
||||
scan.io,
|
||||
scan.projection(),
|
||||
scan.row_filter,
|
||||
scan.case_sensitive,
|
||||
scan.limit,
|
||||
)
|
||||
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={"tasks": tasks, "scan_context": scan_context},
|
||||
)
|
||||
)
|
||||
|
||||
# Drop the catalog reference so the builder becomes picklable for num_proc > 1.
|
||||
# All data needed for reading has been extracted into scan_context above.
|
||||
self.config.catalog = None
|
||||
self.config_kwargs.pop("catalog", None)
|
||||
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.info.features is not None:
|
||||
# More expensive cast to support nested features with keys in a different order
|
||||
# allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, self.info.features.arrow_schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, tasks: List["FileScanTask"], scan_context):
|
||||
for task in tasks:
|
||||
yield task.file.file_path
|
||||
|
||||
def _generate_num_examples(self, tasks: List["FileScanTask"], scan_context):
|
||||
for task in tasks:
|
||||
yield task.file.record_count
|
||||
|
||||
def _generate_tables(self, tasks: List["FileScanTask"], scan_context):
|
||||
from pyiceberg.io.pyarrow import ArrowScan
|
||||
|
||||
table_metadata, io, projected_schema, row_filter, case_sensitive, limit = scan_context
|
||||
arrow_scan = ArrowScan(
|
||||
table_metadata,
|
||||
io,
|
||||
projected_schema,
|
||||
row_filter,
|
||||
case_sensitive=case_sensitive,
|
||||
limit=limit,
|
||||
)
|
||||
for task_idx, task in enumerate(tasks):
|
||||
for batch_idx, batch in enumerate(arrow_scan.to_record_batches([task])):
|
||||
pa_table = pa.Table.from_batches([batch])
|
||||
yield Key(task_idx, batch_idx), self._cast_table(pa_table)
|
||||
@@ -0,0 +1,103 @@
|
||||
import datasets
|
||||
|
||||
from ..folder_based_builder import folder_based_builder
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
class ImageFolderConfig(folder_based_builder.FolderBasedBuilderConfig):
|
||||
"""BuilderConfig for ImageFolder."""
|
||||
|
||||
drop_labels: bool = None
|
||||
drop_metadata: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class ImageFolder(folder_based_builder.FolderBasedBuilder):
|
||||
BASE_FEATURE = datasets.Image
|
||||
BASE_COLUMN_NAME = "image"
|
||||
BUILDER_CONFIG_CLASS = ImageFolderConfig
|
||||
EXTENSIONS: list[str] # definition at the bottom of the script
|
||||
|
||||
|
||||
# Obtained with:
|
||||
# ```
|
||||
# import PIL.Image
|
||||
# IMAGE_EXTENSIONS = []
|
||||
# PIL.Image.init()
|
||||
# for ext, format in PIL.Image.EXTENSION.items():
|
||||
# if format in PIL.Image.OPEN:
|
||||
# IMAGE_EXTENSIONS.append(ext[1:])
|
||||
# ```
|
||||
# We intentionally do not run this code on launch because:
|
||||
# (1) Pillow is an optional dependency, so importing Pillow in global namespace is not allowed
|
||||
# (2) To ensure the list of supported extensions is deterministic
|
||||
IMAGE_EXTENSIONS = [
|
||||
".blp",
|
||||
".bmp",
|
||||
".dib",
|
||||
".bufr",
|
||||
".cur",
|
||||
".pcx",
|
||||
".dcx",
|
||||
".dds",
|
||||
".ps",
|
||||
".eps",
|
||||
".fit",
|
||||
".fits",
|
||||
".fli",
|
||||
".flc",
|
||||
".ftc",
|
||||
".ftu",
|
||||
".gbr",
|
||||
".gif",
|
||||
".grib",
|
||||
# ".h5", # may contain zero or several images
|
||||
# ".hdf", # may contain zero or several images
|
||||
".png",
|
||||
".apng",
|
||||
".jp2",
|
||||
".j2k",
|
||||
".jpc",
|
||||
".jpf",
|
||||
".jpx",
|
||||
".j2c",
|
||||
".icns",
|
||||
".ico",
|
||||
".im",
|
||||
".iim",
|
||||
".tif",
|
||||
".tiff",
|
||||
".jfif",
|
||||
".jpe",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".msp",
|
||||
".pcd",
|
||||
".pxr",
|
||||
".pbm",
|
||||
".pgm",
|
||||
".ppm",
|
||||
".pnm",
|
||||
".psd",
|
||||
".bw",
|
||||
".rgb",
|
||||
".rgba",
|
||||
".sgi",
|
||||
".ras",
|
||||
".tga",
|
||||
".icb",
|
||||
".vda",
|
||||
".vst",
|
||||
".webp",
|
||||
".wmf",
|
||||
".emf",
|
||||
".xbm",
|
||||
".xpm",
|
||||
]
|
||||
ImageFolder.EXTENSIONS = IMAGE_EXTENSIONS
|
||||
@@ -0,0 +1,536 @@
|
||||
import codecs
|
||||
import io
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.json as paj
|
||||
|
||||
import datasets
|
||||
import datasets.config
|
||||
from datasets import List, Value
|
||||
from datasets.builder import Key
|
||||
from datasets.table import table_cast
|
||||
from datasets.utils.file_utils import readline
|
||||
from datasets.utils.json import (
|
||||
find_mixed_struct_types_field_paths,
|
||||
get_json_field_path_from_pyarrow_json_error,
|
||||
get_json_field_paths_from_feature,
|
||||
insert_json_field_path,
|
||||
json_encode_field,
|
||||
json_encode_fields_in_json_lines,
|
||||
set_json_types_in_feature,
|
||||
ujson_dumps,
|
||||
ujson_loads,
|
||||
)
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
def pandas_read_json(path_or_buf, **kwargs):
|
||||
if datasets.config.PANDAS_VERSION.major >= 2:
|
||||
kwargs["dtype_backend"] = "pyarrow"
|
||||
return pd.read_json(path_or_buf, **kwargs)
|
||||
|
||||
|
||||
class FullReadDisallowed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class JsonConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for JSON."""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
encoding: str = "utf-8"
|
||||
encoding_errors: Optional[str] = None
|
||||
field: Optional[str] = None
|
||||
use_threads: bool = True # deprecated
|
||||
block_size: Optional[int] = None # deprecated
|
||||
chunksize: int = 10 << 20 # 10MB
|
||||
newlines_in_values: Optional[bool] = None
|
||||
on_mixed_types: Optional[Literal["use_json"]] = "use_json"
|
||||
parse_agent_traces: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class Json(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = JsonConfig
|
||||
|
||||
def _info(self):
|
||||
if self.config.block_size is not None:
|
||||
logger.warning("The JSON loader parameter `block_size` is deprecated. Please use `chunksize` instead")
|
||||
self.config.chunksize = self.config.block_size
|
||||
if self.config.use_threads is not True:
|
||||
logger.warning(
|
||||
"The JSON loader parameter `use_threads` is deprecated and doesn't have any effect anymore."
|
||||
)
|
||||
if self.config.newlines_in_values is not None:
|
||||
raise ValueError("The JSON loader parameter `newlines_in_values` is no longer supported")
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""We handle string, list and dicts in datafiles"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
base_data_files = dl_manager.download(self.config.data_files)
|
||||
extracted_data_files = dl_manager.extract(base_data_files)
|
||||
splits = []
|
||||
for split_name, extracted_files in extracted_data_files.items():
|
||||
files_iterables = [dl_manager.iter_files(extracted_file) for extracted_file in extracted_files]
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={
|
||||
"files_iterables": files_iterables,
|
||||
"base_files": base_data_files[split_name],
|
||||
"original_files": self.config.data_files[split_name],
|
||||
},
|
||||
)
|
||||
)
|
||||
if self.info.features is None:
|
||||
try:
|
||||
pa_table = next(iter(self._generate_tables(**splits[0].gen_kwargs, allow_full_read=False)))[1]
|
||||
self.info.features = datasets.Features.from_arrow_schema(pa_table.schema)
|
||||
if self.config.parse_agent_traces and has_agent_traces_markers(self.info.features):
|
||||
self.info.features = AGENT_TRACES_FEATURES
|
||||
except FullReadDisallowed:
|
||||
pass
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table, json_field_paths=()) -> pa.Table:
|
||||
if self.info.features is not None:
|
||||
# adding missing columns
|
||||
for column_name in set(self.info.features) - set(pa_table.column_names):
|
||||
type = self.info.features.arrow_schema.field(column_name).type
|
||||
pa_table = pa_table.append_column(column_name, pa.array([None] * len(pa_table), type=type))
|
||||
# convert to string when needed
|
||||
for i, column_name in enumerate(pa_table.column_names):
|
||||
if pa.types.is_struct(pa_table[column_name].type) and self.info.features.get(
|
||||
column_name, None
|
||||
) == Value("string"):
|
||||
jsonl = (
|
||||
pa_table[column_name]
|
||||
.to_pandas(types_mapper=pd.ArrowDtype)
|
||||
.to_json(orient="records", lines=True)
|
||||
)
|
||||
string_array = pa.array(
|
||||
(None if x.strip() == "null" else x.strip() for x in jsonl.split("\n") if x.strip()),
|
||||
type=pa.string(),
|
||||
)
|
||||
pa_table = pa_table.set_column(i, column_name, string_array)
|
||||
# more expensive cast to support nested structures with keys in a different order
|
||||
# allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, self.info.features.arrow_schema)
|
||||
elif json_field_paths:
|
||||
features = datasets.Features.from_arrow_schema(pa_table.schema)
|
||||
features = set_json_types_in_feature(features, json_field_paths)
|
||||
pa_table = table_cast(pa_table, features.arrow_schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, base_files, files_iterables, original_files):
|
||||
yield from base_files
|
||||
|
||||
def _generate_tables(self, base_files, files_iterables, original_files, allow_full_read=True):
|
||||
json_field_paths = []
|
||||
is_agent_traces = False
|
||||
|
||||
if self.info.features is not None:
|
||||
if self.info.features == AGENT_TRACES_FEATURES:
|
||||
is_agent_traces = True
|
||||
if datasets.config.TEICH_AVAILABLE:
|
||||
from teich import convert_traces_to_training_data
|
||||
else:
|
||||
raise ImportError("To support decoding agent traces, please install 'teich'.")
|
||||
json_field_paths = get_json_field_paths_from_feature(self.info.features)
|
||||
|
||||
for shard_idx, files_iterable in enumerate(files_iterables):
|
||||
for file in files_iterable:
|
||||
# If the file is one json object and if we need to look at the items in one specific field
|
||||
if self.config.field is not None:
|
||||
if not allow_full_read:
|
||||
raise FullReadDisallowed()
|
||||
with open(file, encoding=self.config.encoding, errors=self.config.encoding_errors) as f:
|
||||
dataset = ujson_loads(f.read())
|
||||
# We keep only the field we are interested in
|
||||
dataset = dataset[self.config.field]
|
||||
df = pandas_read_json(io.StringIO(ujson_dumps(dataset)))
|
||||
if df.columns.tolist() == [0]:
|
||||
df.columns = list(self.config.features) if self.config.features else ["text"]
|
||||
pa_table = pa.Table.from_pandas(df, preserve_index=False)
|
||||
yield Key(shard_idx, 0), self._cast_table(pa_table)
|
||||
|
||||
# If the files are agent traces (one row = one file except for hermes which can have multiple sessions per file)
|
||||
elif is_agent_traces:
|
||||
trace_file = Path(file)
|
||||
trace = trace_file.read_text(encoding="utf-8")
|
||||
lines = trace.splitlines()
|
||||
file_path = original_files[shard_idx]
|
||||
if self.base_path is not None and file_path.startswith(self.base_path):
|
||||
file_path = os.path.relpath(file_path, self.base_path)
|
||||
training_examples = convert_traces_to_training_data(trace_file)
|
||||
examples = []
|
||||
for i, training_example in enumerate(training_examples):
|
||||
if training_example["metadata"]["trace_type"] == "hermes":
|
||||
timestamp = ujson_loads(lines[i])["started_at"]
|
||||
milliseconds = timestamp if timestamp <= 10_000_000_000 else timestamp * 1_000
|
||||
sent_at = (
|
||||
datetime.fromtimestamp(milliseconds, tz=timezone.utc)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z")
|
||||
)
|
||||
bonus_fields = {
|
||||
"harness": training_example["metadata"]["trace_type"],
|
||||
"session_id": training_example["metadata"]["session_id"],
|
||||
"sent_at": sent_at,
|
||||
"num_user_messages": training_example["metadata"]["turn_count"],
|
||||
"num_tool_calls": training_example["metadata"]["tool_call_count"],
|
||||
"trace": lines[i],
|
||||
}
|
||||
else:
|
||||
harness, session_id, prompt, sent_at, num_user_messages, num_tool_calls = (
|
||||
parse_traces_info(lines)
|
||||
)
|
||||
bonus_fields = {
|
||||
"harness": harness,
|
||||
"session_id": session_id,
|
||||
"prompt": prompt,
|
||||
"sent_at": sent_at,
|
||||
"num_user_messages": num_user_messages,
|
||||
"num_tool_calls": num_tool_calls,
|
||||
"trace": trace,
|
||||
}
|
||||
example = {
|
||||
**dict.fromkeys(AGENT_TRACES_FEATURES),
|
||||
**training_example,
|
||||
**bonus_fields,
|
||||
"file_path": file_path,
|
||||
}
|
||||
for json_field_path in json_field_paths:
|
||||
example = json_encode_field(example, json_field_path)
|
||||
examples.append(example)
|
||||
pa_table = pa.Table.from_pylist(examples)
|
||||
yield Key(shard_idx, 0), self._cast_table(pa_table)
|
||||
|
||||
# If the file has one json object per line
|
||||
else:
|
||||
with open(file, "rb") as f:
|
||||
batch_idx = 0
|
||||
# Use block_size equal to the chunk size divided by 32 to leverage multithreading
|
||||
# Set a default minimum value of 16kB if the chunk size is really small
|
||||
block_size = max(self.config.chunksize // 32, 16 << 10)
|
||||
encoding_errors = (
|
||||
self.config.encoding_errors if self.config.encoding_errors is not None else "strict"
|
||||
)
|
||||
while True:
|
||||
batch = f.read(self.config.chunksize)
|
||||
if not batch:
|
||||
break
|
||||
# A leading UTF-8 BOM makes the ujson pre-scan below raise
|
||||
# ValueError, silently skipping mixed-struct detection, while
|
||||
# PyArrow tolerates the BOM -- so the inferred schema differed
|
||||
# depending on whether the file started with a BOM. Strip it so
|
||||
# both see the same bytes (a BOM only appears at the start of the file).
|
||||
if shard_idx == 0 and batch_idx == 0 and batch.startswith(codecs.BOM_UTF8):
|
||||
batch = batch[len(codecs.BOM_UTF8) :]
|
||||
if batch.startswith(b"["):
|
||||
if not allow_full_read:
|
||||
raise FullReadDisallowed()
|
||||
else:
|
||||
# convert to JSON Lines
|
||||
full_data = batch + f.read()
|
||||
if b"{" in batch[:100].split(b'"', 1)[0]: # list of objects
|
||||
batch = "\n".join(ujson_dumps(x) for x in ujson_loads(full_data)).encode()
|
||||
else: # list of strings
|
||||
batch = "\n".join(
|
||||
ujson_dumps({"text": x}) for x in ujson_loads(full_data)
|
||||
).encode()
|
||||
# Finish current line
|
||||
try:
|
||||
batch += f.readline()
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
batch += readline(f)
|
||||
# PyArrow only accepts utf-8 encoded bytes
|
||||
if self.config.encoding != "utf-8":
|
||||
batch = batch.decode(self.config.encoding, errors=encoding_errors).encode("utf-8")
|
||||
# On first batch we check for lists of objects with arbitrary fields
|
||||
if (
|
||||
shard_idx == 0
|
||||
and batch_idx == 0
|
||||
and self.info.features is None
|
||||
and self.config.on_mixed_types == "use_json"
|
||||
):
|
||||
try:
|
||||
examples = [ujson_loads(line) for line in batch.splitlines()]
|
||||
except ValueError:
|
||||
# the file is likely not JSON Lines and may contain one single multi-line JSON object
|
||||
pass
|
||||
else:
|
||||
json_field_paths += find_mixed_struct_types_field_paths(examples)
|
||||
# Re-encode JSON fields
|
||||
original_batch = batch
|
||||
if json_field_paths:
|
||||
examples = [ujson_loads(line) for line in batch.splitlines()]
|
||||
for json_field_path in json_field_paths:
|
||||
examples = [json_encode_field(example, json_field_path) for example in examples]
|
||||
batch = "\n".join(ujson_dumps(example) for example in examples).encode()
|
||||
# Disable parallelism if block size is ~ len(batch) to avoid segfault
|
||||
block_size = len(batch) if len(batch) // 8 > block_size else block_size
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
pa_table = paj.read_json(
|
||||
io.BytesIO(batch), read_options=paj.ReadOptions(block_size=block_size)
|
||||
)
|
||||
break
|
||||
except (pa.ArrowInvalid, pa.ArrowNotImplementedError) as e:
|
||||
if batch.startswith(b"["): # paj.read_json only supports json lines
|
||||
raise
|
||||
elif self.config.on_mixed_types == "use_json" and (
|
||||
isinstance(e, pa.ArrowInvalid)
|
||||
and "JSON parse error: Column(" in str(e)
|
||||
and ") changed from" in str(e)
|
||||
):
|
||||
json_field_path = get_json_field_path_from_pyarrow_json_error(str(e))
|
||||
insert_json_field_path(json_field_paths, json_field_path)
|
||||
batch = json_encode_fields_in_json_lines(original_batch, json_field_paths)
|
||||
elif (
|
||||
"straddling" in str(e) or "JSON conversion to" in str(e)
|
||||
) and block_size < len(batch):
|
||||
# Increase the block size in case it was too small.
|
||||
# The block size will be reset for the next file.
|
||||
# this is needed in case of "stradding" or for some JSON conversions (see https://github.com/huggingface/datasets/issues/2799)
|
||||
logger.debug(
|
||||
f"Batch of {len(batch)} bytes couldn't be parsed with block_size={block_size}. Retrying with block_size={block_size * 2}."
|
||||
)
|
||||
block_size *= 2
|
||||
else:
|
||||
raise
|
||||
except pa.ArrowInvalid as e:
|
||||
if not allow_full_read:
|
||||
raise FullReadDisallowed()
|
||||
try:
|
||||
with open(
|
||||
file, encoding=self.config.encoding, errors=self.config.encoding_errors
|
||||
) as f:
|
||||
df = pandas_read_json(f)
|
||||
except ValueError:
|
||||
logger.error(f"Failed to load JSON from file '{file}' with error {type(e)}: {e}")
|
||||
raise e
|
||||
if df.columns.tolist() == [0]:
|
||||
df.columns = list(self.config.features) if self.config.features else ["text"]
|
||||
try:
|
||||
pa_table = pa.Table.from_pandas(df, preserve_index=False)
|
||||
except pa.ArrowInvalid as e:
|
||||
logger.error(
|
||||
f"Failed to convert pandas DataFrame to Arrow Table from file '{file}' with error {type(e)}: {e}"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Failed to convert pandas DataFrame to Arrow Table from file {file}."
|
||||
) from None
|
||||
yield Key(shard_idx, 0), self._cast_table(pa_table)
|
||||
break
|
||||
yield (
|
||||
Key(shard_idx, batch_idx),
|
||||
self._cast_table(pa_table, json_field_paths=json_field_paths),
|
||||
)
|
||||
batch_idx += 1
|
||||
|
||||
|
||||
AGENT_TRACES_TYPES_VALUES = {
|
||||
"claude_code": ["user", "assistant", "system"],
|
||||
"pi": ["session", "message"],
|
||||
"codex": ["session_meta", "turn_context", "response_item", "event_msg"],
|
||||
# droid message events share pi's "message" type, but droid traces always start with a session_start event
|
||||
"droid": ["session_start"],
|
||||
}
|
||||
AGENT_TRACES_TYPE_TO_HARNESS = {}
|
||||
for _harness, _trace_types in AGENT_TRACES_TYPES_VALUES.items():
|
||||
for _trace_type in _trace_types:
|
||||
AGENT_TRACES_TYPE_TO_HARNESS[_trace_type] = _harness
|
||||
|
||||
|
||||
AGENT_TRACES_FEATURES_MARKERS = {
|
||||
"claude_code_or_pi_or_openclaw": datasets.Features(
|
||||
{
|
||||
"type": lambda f: f == Value("string"),
|
||||
"message": lambda f: f == datasets.Json(),
|
||||
}
|
||||
),
|
||||
"codex": datasets.Features(
|
||||
{
|
||||
"type": lambda f: f == Value("string"),
|
||||
"payload": lambda f: f == datasets.Json(),
|
||||
}
|
||||
),
|
||||
"hermes": datasets.Features(
|
||||
{
|
||||
"id": lambda f: f == Value("string"),
|
||||
"source": lambda f: f == datasets.Value("string"),
|
||||
"model": lambda f: f == datasets.Value("string"),
|
||||
"system_prompt": lambda f: f == datasets.Value("string"),
|
||||
"messages": lambda f: isinstance(f, (datasets.List, datasets.Json)),
|
||||
}
|
||||
),
|
||||
"droid": datasets.Features(
|
||||
{
|
||||
"type": lambda f: f == Value("string"),
|
||||
"id": lambda f: f == Value("string"),
|
||||
"version": lambda f: f == Value("int64"),
|
||||
"cwd": lambda f: f == Value("string"),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
AGENT_TRACES_FEATURES = datasets.Features(
|
||||
{
|
||||
# basic features
|
||||
"harness": Value("string"),
|
||||
"session_id": Value("string"),
|
||||
# teich features
|
||||
"prompt": Value("string"),
|
||||
"messages": List(datasets.Json()),
|
||||
"tools": List(datasets.Json()),
|
||||
"metadata": datasets.Json(),
|
||||
# bonus features
|
||||
"sent_at": Value("string"),
|
||||
"num_user_messages": Value("int64"),
|
||||
"num_tool_calls": Value("int64"),
|
||||
"trace": datasets.Json(),
|
||||
"file_path": Value("string"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def has_agent_traces_markers(features: datasets.Features) -> bool:
|
||||
for agent_traces_features_marker in AGENT_TRACES_FEATURES_MARKERS.values():
|
||||
if all(feature_marker(features.get(key)) for key, feature_marker in agent_traces_features_marker.items()):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_traces_info(
|
||||
trace_events: list[str],
|
||||
) -> tuple[Optional[str], Optional[str], Optional[str], Optional[str], int, int]:
|
||||
harness, session_id, prompt, sent_at = None, None, None, None
|
||||
# prompt/sent_at describe the first user message; the counters summarize the whole trace file.
|
||||
# Codex response_item user messages can include context files (for example AGENTS.md), so only event_msg
|
||||
# user_message records are treated as Codex user messages.
|
||||
num_user_messages = 0
|
||||
num_tool_calls = 0
|
||||
for event in trace_events:
|
||||
decoded_event = ujson_loads(event)
|
||||
if harness is None:
|
||||
if "type" in decoded_event and isinstance(decoded_event["type"], str):
|
||||
harness = AGENT_TRACES_TYPE_TO_HARNESS.get(decoded_event["type"])
|
||||
if session_id is None:
|
||||
session_id = get_session_id(decoded_event)
|
||||
if (
|
||||
session_id is not None
|
||||
and decoded_event.get("type") == "session"
|
||||
and isinstance(decoded_event.get("cwd"), str)
|
||||
and "/.openclaw/" in decoded_event["cwd"]
|
||||
):
|
||||
harness = "openclaw"
|
||||
user_prompt = get_user_prompt(decoded_event)
|
||||
if user_prompt is not None:
|
||||
num_user_messages += 1
|
||||
if prompt is None:
|
||||
prompt = user_prompt
|
||||
sent_at = get_trace_event_timestamp(decoded_event)
|
||||
num_tool_calls += get_tool_call_count(decoded_event)
|
||||
return harness, session_id, prompt, sent_at, num_user_messages, num_tool_calls
|
||||
|
||||
|
||||
def get_session_id(trace: dict) -> Optional[str]:
|
||||
# claude
|
||||
if isinstance(trace.get("sessionId"), str):
|
||||
return trace["sessionId"]
|
||||
# claude (not sure but this format does exist online)
|
||||
if isinstance(trace.get("session_id"), str):
|
||||
return trace["session_id"]
|
||||
# codex
|
||||
if isinstance(trace.get("payload"), dict) and isinstance(trace["payload"].get("id"), str):
|
||||
return trace["payload"]["id"]
|
||||
# pi / openclaw on "session" (openclaw embeds pi-agent; distinguish via cwd), droid on "session_start"
|
||||
if trace.get("type") in ("session", "session_start") and isinstance(trace.get("id"), str):
|
||||
return trace["id"]
|
||||
return None
|
||||
|
||||
|
||||
def get_user_prompt(trace_event: dict) -> Optional[str]:
|
||||
if trace_event.get("type") == "user" and isinstance(trace_event.get("message"), dict):
|
||||
message = trace_event["message"]
|
||||
if message.get("role") == "user":
|
||||
return get_content_text(message.get("content"))
|
||||
|
||||
if trace_event.get("type") == "message":
|
||||
if isinstance(trace_event.get("message"), dict):
|
||||
message = trace_event["message"]
|
||||
# droid marks injected context as llm_only and local-only notes as user_only, neither is a real user prompt
|
||||
if message.get("role") == "user" and message.get("visibility") not in ("llm_only", "user_only"):
|
||||
return get_content_text(message.get("content"))
|
||||
if trace_event.get("role") == "user":
|
||||
return get_content_text(trace_event.get("content"))
|
||||
|
||||
if trace_event.get("type") == "event_msg" and isinstance(trace_event.get("payload"), dict):
|
||||
payload = trace_event["payload"]
|
||||
if payload.get("type") == "user_message":
|
||||
return get_content_text(payload.get("message"))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_tool_call_count(trace_event: dict) -> int:
|
||||
trace_type = trace_event.get("type")
|
||||
if trace_type == "response_item":
|
||||
payload = trace_event.get("payload")
|
||||
if isinstance(payload, dict) and payload.get("type") == "function_call":
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if trace_type not in {"assistant", "message"} or not isinstance(trace_event.get("message"), dict):
|
||||
return 0
|
||||
message = trace_event["message"]
|
||||
if message.get("role") != "assistant":
|
||||
return 0
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
return 0
|
||||
return sum(
|
||||
1
|
||||
for content_part in content
|
||||
if isinstance(content_part, dict) and content_part.get("type") in {"tool_use", "toolCall"}
|
||||
)
|
||||
|
||||
|
||||
def get_trace_event_timestamp(trace_event: dict) -> Optional[str]:
|
||||
timestamp = trace_event.get("timestamp")
|
||||
if isinstance(timestamp, str):
|
||||
return timestamp
|
||||
return None
|
||||
|
||||
|
||||
def get_content_text(content) -> Optional[str]:
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
content_parts = []
|
||||
for content_part in content:
|
||||
if isinstance(content_part, str):
|
||||
content_parts.append(content_part)
|
||||
elif isinstance(content_part, dict):
|
||||
text = content_part.get("text")
|
||||
if isinstance(text, str):
|
||||
content_parts.append(text)
|
||||
if content_parts:
|
||||
return "\n".join(content_parts)
|
||||
return None
|
||||
@@ -0,0 +1,240 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
import pyarrow as pa
|
||||
from huggingface_hub import HfApi, get_token
|
||||
|
||||
import datasets
|
||||
from datasets import Audio, Image, Video
|
||||
from datasets.builder import Key
|
||||
from datasets.table import table_cast
|
||||
from datasets.utils.file_utils import is_local_path
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import lance
|
||||
import lance.file
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
MAGIC_BYTES_EXTENSION_AND_FEATURE_TYPES = [
|
||||
("1A 45 DF A3", ".mkv", Video()),
|
||||
("66 74 79 70 69 73 6F 6D", ".mp4", Video()),
|
||||
("66 74 79 70 4D 53 4E 56", ".mp4", Video()),
|
||||
("52 49 46 46", ".avi", Video()),
|
||||
("00 00 01 BA", ".mpeg", Video()),
|
||||
("00 00 01 BA", ".mpeg", Video()),
|
||||
("00 00 01 B3", ".mov", Video()),
|
||||
("89 50 4E 47", ".png", Image()),
|
||||
("FF D8", ".jpg", Image()),
|
||||
("49 49", ".tif", Image()),
|
||||
("47 49 46 38", ".gif", Image()),
|
||||
("52 49 46 46", ".wav", Audio()),
|
||||
("49 44 33", ".mp3", Audio()),
|
||||
("66 4C 61 43", ".flac", Audio()),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LanceConfig(datasets.BuilderConfig):
|
||||
"""
|
||||
BuilderConfig for Lance format.
|
||||
|
||||
Args:
|
||||
features: (`Features`, *optional*):
|
||||
Cast the data to `features`.
|
||||
columns: (`List[str]`, *optional*):
|
||||
List of columns to load, the other ones are ignored.
|
||||
batch_size: (`int`, *optional*):
|
||||
Size of the RecordBatches to iterate on. Default to 256.
|
||||
token: (`str`, *optional*):
|
||||
Optional HF token to use to download datasets.
|
||||
"""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
columns: Optional[List[str]] = None
|
||||
batch_size: Optional[int] = 256
|
||||
token: Optional[str] = None
|
||||
|
||||
|
||||
def resolve_dataset_uris(files: List[str]) -> Dict[str, List[str]]:
|
||||
dataset_uris = set()
|
||||
for file_path in files:
|
||||
path = Path(file_path)
|
||||
if path.parent.name in {"_transactions", "_indices", "_versions"}:
|
||||
dataset_root = path.parent.parent
|
||||
dataset_uris.add(str(dataset_root))
|
||||
return list(dataset_uris)
|
||||
|
||||
|
||||
def _fix_hf_uri(uri: str) -> str:
|
||||
# replace the revision tag from hf uri
|
||||
if "@" in uri:
|
||||
matched = re.match(r"(hf://.+?)(@[0-9a-f]+)(/.*)", uri)
|
||||
if matched:
|
||||
uri = matched.group(1) + matched.group(3)
|
||||
return uri
|
||||
|
||||
|
||||
def _fix_local_version_file(uri: str) -> str:
|
||||
# replace symlinks with real files for _version
|
||||
if "/_versions/" in uri and is_local_path(uri):
|
||||
path = Path(uri)
|
||||
if path.is_symlink():
|
||||
data = path.read_bytes()
|
||||
path.unlink()
|
||||
path.write_bytes(data)
|
||||
return uri
|
||||
|
||||
|
||||
class Lance(datasets.ArrowBasedBuilder, datasets.builder._CountableBuilderMixin):
|
||||
BUILDER_CONFIG_CLASS = LanceConfig
|
||||
METADATA_EXTENSIONS = [".idx", ".txn", ".manifest"]
|
||||
METADATA_FILE_NAMES = ["latest_version_hint.json"]
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
import lance
|
||||
import lance.file
|
||||
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
if self.repo_id:
|
||||
api = HfApi(**dl_manager.download_config.storage_options.get("hf", {}))
|
||||
dataset_sha = api.dataset_info(self.repo_id).sha
|
||||
if dataset_sha != self.hash:
|
||||
raise NotImplementedError(
|
||||
f"lance doesn't support loading other revisions than 'main' yet, but got {self.hash}"
|
||||
)
|
||||
data_files = dl_manager.download(self.config.data_files)
|
||||
|
||||
# TODO: remove once Lance supports HF links with revisions
|
||||
data_files = {split: [_fix_hf_uri(file) for file in files] for split, files in data_files.items()}
|
||||
# TODO: remove once Lance supports symlinks for _version files
|
||||
data_files = {split: [_fix_local_version_file(file) for file in files] for split, files in data_files.items()}
|
||||
|
||||
splits: list[datasets.SplitGenerator] = []
|
||||
for split_name, files in data_files.items():
|
||||
protocol = files[0].split("://", 1)[0]
|
||||
storage_options = dict(dl_manager.download_config.storage_options.get(protocol, {}))
|
||||
# lance doesn't allow "token": None for hf and expects a string
|
||||
if protocol == "hf" and storage_options.get("token") is None:
|
||||
storage_options["token"] = get_token()
|
||||
|
||||
lance_dataset_uris = resolve_dataset_uris(files)
|
||||
if lance_dataset_uris:
|
||||
lance_datasets = [lance.dataset(uri, storage_options=storage_options) for uri in lance_dataset_uris]
|
||||
fragments = [frag for lance_dataset in lance_datasets for frag in lance_dataset.get_fragments()]
|
||||
if self.info.features is None:
|
||||
pa_schema = fragments[0]._ds.schema
|
||||
first_row_first_bytes = {}
|
||||
for field in pa_schema:
|
||||
if self.config.columns is not None and field.name not in self.config.columns:
|
||||
continue
|
||||
if pa.types.is_binary(field.type) or pa.types.is_large_binary(field.type):
|
||||
try:
|
||||
first_row_first_bytes[field.name] = (
|
||||
lance_datasets[0].take_blobs(field.name, [0])[0].read(16)
|
||||
)
|
||||
except ValueError:
|
||||
first_row_first_bytes[field.name] = (
|
||||
lance_datasets[0].take([0], [field.name]).to_pylist()[0][field.name][:16]
|
||||
)
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={"fragments": fragments, "lance_files_paths": None, "lance_files": None},
|
||||
)
|
||||
)
|
||||
else:
|
||||
lance_files = [
|
||||
lance.file.LanceFileReader(file, storage_options=storage_options, columns=self.config.columns)
|
||||
for file in files
|
||||
]
|
||||
if self.info.features is None:
|
||||
pa_schema = lance_files[0].metadata().schema
|
||||
first_row_first_bytes = {
|
||||
field_name: value[:16]
|
||||
for field_name, value in lance_files[0].take_rows([0]).to_table().to_pylist()[0].items()
|
||||
if isinstance(value, bytes)
|
||||
}
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={"fragments": None, "lance_files_paths": files, "lance_files": lance_files},
|
||||
)
|
||||
)
|
||||
if self.info.features is None:
|
||||
if self.config.columns:
|
||||
fields = [
|
||||
pa_schema.field(name) for name in self.config.columns if pa_schema.get_field_index(name) != -1
|
||||
]
|
||||
pa_schema = pa.schema(fields)
|
||||
features = datasets.Features.from_arrow_schema(pa_schema)
|
||||
for field_name, first_bytes in first_row_first_bytes.items():
|
||||
for magic_bytes_hex, _, feature_type in MAGIC_BYTES_EXTENSION_AND_FEATURE_TYPES:
|
||||
magic_bytes = bytes.fromhex(magic_bytes_hex)
|
||||
if magic_bytes in first_bytes[: len(magic_bytes) * 2]: # allow some padding
|
||||
features[field_name] = feature_type
|
||||
break
|
||||
self.info.features = features
|
||||
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.info.features is not None:
|
||||
# more expensive cast to support nested features with keys in a different order
|
||||
# allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, self.info.features.arrow_schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(
|
||||
self,
|
||||
fragments: Optional[List["lance.LanceFragment"]],
|
||||
lance_files_paths: Optional[list[str]],
|
||||
lance_files: Optional[List["lance.file.LanceFileReader"]],
|
||||
):
|
||||
if fragments:
|
||||
for fragment in fragments:
|
||||
paths = [data_file.path for data_file in fragment.metadata.data_files()]
|
||||
yield paths[0] if len(paths) == 1 else {"fragment_data_files": paths}
|
||||
else:
|
||||
yield from lance_files_paths
|
||||
|
||||
def _generate_num_examples(
|
||||
self,
|
||||
fragments: Optional[List["lance.LanceFragment"]],
|
||||
lance_files_paths: Optional[list[str]],
|
||||
lance_files: Optional[List["lance.file.LanceFileReader"]],
|
||||
):
|
||||
if fragments:
|
||||
for fragment in fragments:
|
||||
yield fragment.count_rows()
|
||||
else:
|
||||
for lance_file in lance_files:
|
||||
yield lance_file.num_rows()
|
||||
|
||||
def _generate_tables(
|
||||
self,
|
||||
fragments: Optional[List["lance.LanceFragment"]],
|
||||
lance_files_paths: Optional[list[str]],
|
||||
lance_files: Optional[List["lance.file.LanceFileReader"]],
|
||||
):
|
||||
if fragments:
|
||||
for frag_idx, fragment in enumerate(fragments):
|
||||
for batch_idx, batch in enumerate(
|
||||
fragment.to_batches(
|
||||
columns=self.config.columns, batch_size=self.config.batch_size, blob_handling="all_binary"
|
||||
)
|
||||
):
|
||||
table = pa.Table.from_batches([batch])
|
||||
yield Key(frag_idx, batch_idx), self._cast_table(table)
|
||||
else:
|
||||
for file_idx, lance_file in enumerate(lance_files):
|
||||
for batch_idx, batch in enumerate(lance_file.read_all(batch_size=self.config.batch_size).to_batches()):
|
||||
table = pa.Table.from_batches([batch])
|
||||
yield Key(file_idx, batch_idx), self._cast_table(table)
|
||||
@@ -0,0 +1,31 @@
|
||||
import datasets
|
||||
|
||||
from ..folder_based_builder import folder_based_builder
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
class MeshFolderConfig(folder_based_builder.FolderBasedBuilderConfig):
|
||||
"""BuilderConfig for MeshFolder."""
|
||||
|
||||
drop_labels: bool = None
|
||||
drop_metadata: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class MeshFolder(folder_based_builder.FolderBasedBuilder):
|
||||
BASE_FEATURE = datasets.Mesh
|
||||
BASE_COLUMN_NAME = "mesh"
|
||||
BUILDER_CONFIG_CLASS = MeshFolderConfig
|
||||
EXTENSIONS: list[str] # definition at the bottom of the script
|
||||
|
||||
|
||||
MESH_EXTENSIONS = [
|
||||
".glb",
|
||||
".ply",
|
||||
".stl",
|
||||
]
|
||||
MeshFolder.EXTENSIONS = MESH_EXTENSIONS
|
||||
@@ -0,0 +1,23 @@
|
||||
import datasets
|
||||
|
||||
from ..folder_based_builder import folder_based_builder
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
class NiftiFolderConfig(folder_based_builder.FolderBasedBuilderConfig):
|
||||
"""BuilderConfig for NiftiFolder."""
|
||||
|
||||
drop_labels: bool = None
|
||||
drop_metadata: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class NiftiFolder(folder_based_builder.FolderBasedBuilder):
|
||||
BASE_FEATURE = datasets.Nifti
|
||||
BASE_COLUMN_NAME = "nifti"
|
||||
BUILDER_CONFIG_CLASS = NiftiFolderConfig
|
||||
EXTENSIONS: list[str] = [".nii"]
|
||||
@@ -0,0 +1,57 @@
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
@dataclass
|
||||
class PandasConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for Pandas."""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class Pandas(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = PandasConfig
|
||||
|
||||
def _info(self):
|
||||
warnings.warn(
|
||||
"The Pandas builder is deprecated and will be removed in the next major version of datasets.",
|
||||
FutureWarning,
|
||||
)
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""We handle string, list and dicts in datafiles"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
data_files = dl_manager.download(self.config.data_files)
|
||||
splits = []
|
||||
for split_name, files in data_files.items():
|
||||
splits.append(datasets.SplitGenerator(name=split_name, gen_kwargs={"files": files}))
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.config.features is not None:
|
||||
# more expensive cast to support nested features with keys in a different order
|
||||
# allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, self.config.features.arrow_schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, files):
|
||||
yield from files
|
||||
|
||||
def _generate_tables(self, files):
|
||||
for i, file in enumerate(files):
|
||||
with open(file, "rb") as f:
|
||||
pa_table = pa.Table.from_pandas(pd.read_pickle(f))
|
||||
yield Key(i, 0), self._cast_table(pa_table)
|
||||
@@ -0,0 +1,234 @@
|
||||
import gc
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional, Union
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as ds
|
||||
import pyarrow.parquet as pq
|
||||
from packaging import version
|
||||
|
||||
import datasets
|
||||
import datasets.config
|
||||
from datasets.builder import Key
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParquetConfig(datasets.BuilderConfig):
|
||||
"""
|
||||
BuilderConfig for Parquet.
|
||||
|
||||
Args:
|
||||
batch_size (`int`, *optional*):
|
||||
Size of the RecordBatches to iterate on.
|
||||
The default is the row group size (defined by the first row group).
|
||||
columns (`list[str]`, *optional*)
|
||||
List of columns to load, the other ones are ignored.
|
||||
All columns are loaded by default.
|
||||
features: (`Features`, *optional*):
|
||||
Cast the data to `features`.
|
||||
filters (`Union[pyarrow.dataset.Expression, list[tuple], list[list[tuple]]]`, *optional*):
|
||||
Return only the rows matching the filter.
|
||||
If possible the predicate will be pushed down to exploit the partition information
|
||||
or internal metadata found in the data source, e.g. Parquet statistics.
|
||||
Otherwise filters the loaded RecordBatches before yielding them.
|
||||
fragment_scan_options (`pyarrow.dataset.ParquetFragmentScanOptions`, *optional*)
|
||||
Scan-specific options for Parquet fragments.
|
||||
This is especially useful to configure buffering and caching.
|
||||
|
||||
<Added version="4.2.0"/>
|
||||
on_bad_files (`Literal["error", "warn", "skip"]`, *optional*, defaults to "error")
|
||||
Specify what to do upon encountering a bad file (a file that can't be read). Allowed values are :
|
||||
* 'error', raise an Exception when a bad file is encountered.
|
||||
* 'warn', raise a warning when a bad file is encountered and skip that file.
|
||||
* 'skip', skip bad files without raising or warning when they are encountered.
|
||||
|
||||
<Added version="4.2.0"/>
|
||||
|
||||
Example:
|
||||
|
||||
Load a subset of columns:
|
||||
|
||||
```python
|
||||
>>> ds = load_dataset(parquet_dataset_id, columns=["col_0", "col_1"])
|
||||
```
|
||||
|
||||
Stream data and efficiently filter data, possibly skipping entire files or row groups:
|
||||
|
||||
```python
|
||||
>>> filters = [("col_0", "==", 0)]
|
||||
>>> ds = load_dataset(parquet_dataset_id, streaming=True, filters=filters)
|
||||
```
|
||||
|
||||
Increase the minimum request size when streaming from 32MiB (default) to 128MiB and enable prefetching:
|
||||
|
||||
```python
|
||||
>>> import pyarrow
|
||||
>>> import pyarrow.dataset
|
||||
>>> fragment_scan_options = pyarrow.dataset.ParquetFragmentScanOptions(
|
||||
... cache_options=pyarrow.CacheOptions(
|
||||
... prefetch_limit=1,
|
||||
... range_size_limit=128 << 20
|
||||
... ),
|
||||
... )
|
||||
>>> ds = load_dataset(parquet_dataset_id, streaming=True, fragment_scan_options=fragment_scan_options)
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
batch_size: Optional[int] = None
|
||||
columns: Optional[list[str]] = None
|
||||
features: Optional[datasets.Features] = None
|
||||
filters: Optional[Union[ds.Expression, list[tuple], list[list[tuple]]]] = None
|
||||
fragment_scan_options: Optional[ds.ParquetFragmentScanOptions] = None
|
||||
on_bad_files: Literal["error", "warn", "skip"] = "error"
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class Parquet(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = ParquetConfig
|
||||
SLEEP_ON_THREADS_SHUTDOWNS = True # Related to https://github.com/apache/arrow/issues/45214
|
||||
|
||||
def _info(self):
|
||||
if (
|
||||
self.config.columns is not None
|
||||
and self.config.features is not None
|
||||
and set(self.config.columns) != set(self.config.features)
|
||||
):
|
||||
if any(col not in self.config.features for col in self.config.columns):
|
||||
raise ValueError(
|
||||
"The columns and features argument must match, but got ",
|
||||
f"{self.config.columns} and {self.config.features}",
|
||||
)
|
||||
else:
|
||||
features = datasets.Features({col: self.config.features[col] for col in self.config.columns})
|
||||
else:
|
||||
features = self.config.features
|
||||
return datasets.DatasetInfo(features=features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""We handle string, list and dicts in datafiles"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
data_files = dl_manager.download(self.config.data_files)
|
||||
splits = []
|
||||
for split_name, files in data_files.items():
|
||||
# Infer features if they are stored in the arrow schema
|
||||
if self.info.features is None:
|
||||
for file in files:
|
||||
try:
|
||||
with open(file, "rb") as f:
|
||||
self.info.features = datasets.Features.from_arrow_schema(pq.read_schema(f))
|
||||
break
|
||||
except pa.ArrowInvalid as e:
|
||||
if self.config.on_bad_files == "error":
|
||||
logger.error(f"Failed to read schema from '{file}' with error {type(e).__name__}: {e}")
|
||||
raise
|
||||
elif self.config.on_bad_files == "warn":
|
||||
logger.warning(f"Skipping bad schema from '{file}'. {type(e).__name__}: {e}`")
|
||||
else:
|
||||
logger.debug(f"Skipping bad schema from '{file}'. {type(e).__name__}: {e}`")
|
||||
if self.info.features is None:
|
||||
raise ValueError(
|
||||
f"At least one valid data file must be specified, all the data_files are invalid: {self.config.data_files}"
|
||||
)
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name, gen_kwargs={"files": files, "row_groups_list": [None] * len(files)}
|
||||
)
|
||||
)
|
||||
if self.config.columns is not None and set(self.config.columns) != set(self.info.features):
|
||||
self.info.features = datasets.Features(
|
||||
{col: feat for col, feat in self.info.features.items() if col in self.config.columns}
|
||||
)
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.info.features is not None:
|
||||
# more expensive cast to support nested features with keys in a different order
|
||||
# allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, self.info.features.arrow_schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_shards(self, files, row_groups_list):
|
||||
if not row_groups_list:
|
||||
yield from files
|
||||
else:
|
||||
for file, row_groups in zip(files, row_groups_list):
|
||||
yield {
|
||||
"fragment_data_file": file,
|
||||
"fragment_row_groups": row_groups,
|
||||
}
|
||||
|
||||
def _generate_more_gen_kwargs(self, files, row_groups_list):
|
||||
if not row_groups_list or any(row_group is None for row_group in row_groups_list):
|
||||
parquet_file_format = ds.ParquetFileFormat(default_fragment_scan_options=self.config.fragment_scan_options)
|
||||
for file in files:
|
||||
with open(file, "rb") as f:
|
||||
parquet_fragment = parquet_file_format.make_fragment(f)
|
||||
yield {
|
||||
"files": [file] * parquet_fragment.num_row_groups,
|
||||
"row_groups_list": [
|
||||
(row_group_id,) for row_group_id in range(parquet_fragment.num_row_groups)
|
||||
],
|
||||
}
|
||||
else:
|
||||
for file, row_groups in zip(files, row_groups_list):
|
||||
yield {"files": [file], "row_groups_list": [row_groups]}
|
||||
|
||||
def _generate_tables(self, files, row_groups_list):
|
||||
if self.config.features is not None and self.config.columns is not None:
|
||||
if sorted(field.name for field in self.info.features.arrow_schema) != sorted(self.config.columns):
|
||||
raise ValueError(
|
||||
f"Tried to load parquet data with columns '{self.config.columns}' with mismatching features '{self.info.features}'"
|
||||
)
|
||||
filter_expr = (
|
||||
pq.filters_to_expression(self.config.filters)
|
||||
if isinstance(self.config.filters, list)
|
||||
else self.config.filters
|
||||
)
|
||||
parquet_file_format = ds.ParquetFileFormat(default_fragment_scan_options=self.config.fragment_scan_options)
|
||||
for file_idx, (file, row_groups) in enumerate(zip(files, row_groups_list)):
|
||||
try:
|
||||
with open(file, "rb") as f:
|
||||
parquet_fragment = parquet_file_format.make_fragment(f)
|
||||
fragment_is_closed = False
|
||||
try:
|
||||
if row_groups is not None:
|
||||
parquet_fragment = parquet_fragment.subset(row_group_ids=row_groups)
|
||||
if parquet_fragment.row_groups:
|
||||
batch_size = self.config.batch_size or parquet_fragment.row_groups[0].num_rows
|
||||
for batch_idx, record_batch in enumerate(
|
||||
parquet_fragment.to_batches(
|
||||
batch_size=batch_size,
|
||||
columns=self.config.columns,
|
||||
filter=filter_expr,
|
||||
batch_readahead=0,
|
||||
fragment_readahead=0,
|
||||
)
|
||||
):
|
||||
pa_table = pa.Table.from_batches([record_batch])
|
||||
# Uncomment for debugging (will print the Arrow table size and elements)
|
||||
# logger.warning(f"pa_table: {pa_table} num rows: {pa_table.num_rows}")
|
||||
# logger.warning('\n'.join(str(pa_table.slice(i, 1).to_pydict()) for i in range(pa_table.num_rows)))
|
||||
yield Key(file_idx, batch_idx), self._cast_table(pa_table)
|
||||
fragment_is_closed = True
|
||||
finally:
|
||||
# Fix for https://github.com/apache/arrow/issues/45214
|
||||
if not fragment_is_closed and datasets.config.PYARROW_VERSION <= version.parse("24.0.0"):
|
||||
del parquet_fragment
|
||||
gc.collect()
|
||||
except (pa.ArrowInvalid, ValueError) as e:
|
||||
if self.config.on_bad_files == "error":
|
||||
logger.error(f"Failed to read file '{file}' with error {type(e).__name__}: {e}")
|
||||
raise
|
||||
elif self.config.on_bad_files == "warn":
|
||||
logger.warning(f"Skipping bad file '{file}'. {type(e).__name__}: {e}`")
|
||||
else:
|
||||
logger.debug(f"Skipping bad file '{file}'. {type(e).__name__}: {e}`")
|
||||
@@ -0,0 +1,23 @@
|
||||
import datasets
|
||||
|
||||
from ..folder_based_builder import folder_based_builder
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
class PdfFolderConfig(folder_based_builder.FolderBasedBuilderConfig):
|
||||
"""BuilderConfig for ImageFolder."""
|
||||
|
||||
drop_labels: bool = None
|
||||
drop_metadata: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class PdfFolder(folder_based_builder.FolderBasedBuilder):
|
||||
BASE_FEATURE = datasets.Pdf
|
||||
BASE_COLUMN_NAME = "pdf"
|
||||
BUILDER_CONFIG_CLASS = PdfFolderConfig
|
||||
EXTENSIONS: list[str] = [".pdf"]
|
||||
@@ -0,0 +1,367 @@
|
||||
import os
|
||||
import posixpath
|
||||
import uuid
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from itertools import islice
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.arrow_writer import ArrowWriter, ParquetWriter
|
||||
from datasets.config import MAX_SHARD_SIZE
|
||||
from datasets.filesystems import (
|
||||
is_remote_filesystem,
|
||||
rename,
|
||||
)
|
||||
from datasets.iterable_dataset import _BaseExamplesIterable
|
||||
from datasets.utils import experimental
|
||||
from datasets.utils.py_utils import convert_file_size_to_int
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pyspark
|
||||
import pyspark.sql
|
||||
|
||||
|
||||
@dataclass
|
||||
class SparkConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for Spark."""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
def _reorder_dataframe_by_partition(df: "pyspark.sql.DataFrame", new_partition_order: list[int]):
|
||||
df_combined = df.select("*").where(f"part_id = {new_partition_order[0]}")
|
||||
for partition_id in new_partition_order[1:]:
|
||||
partition_df = df.select("*").where(f"part_id = {partition_id}")
|
||||
df_combined = df_combined.union(partition_df)
|
||||
return df_combined
|
||||
|
||||
|
||||
def _generate_iterable_examples(
|
||||
df: "pyspark.sql.DataFrame",
|
||||
partition_order: list[int],
|
||||
state_dict: Optional[dict] = None,
|
||||
):
|
||||
import pyspark
|
||||
|
||||
df_with_partition_id = df.select("*", pyspark.sql.functions.spark_partition_id().alias("part_id"))
|
||||
partition_idx_start = state_dict["partition_idx"] if state_dict else 0
|
||||
partition_df = _reorder_dataframe_by_partition(df_with_partition_id, partition_order[partition_idx_start:])
|
||||
# pipeline next partition in parallel to hide latency
|
||||
rows = partition_df.toLocalIterator(prefetchPartitions=True)
|
||||
curr_partition = None
|
||||
row_id = state_dict["partition_example_idx"] if state_dict else 0
|
||||
for row in islice(rows, row_id, None):
|
||||
row_as_dict = row.asDict()
|
||||
part_id = row_as_dict["part_id"]
|
||||
row_as_dict.pop("part_id")
|
||||
if curr_partition != part_id:
|
||||
if state_dict and curr_partition is not None:
|
||||
state_dict["partition_idx"] += 1
|
||||
curr_partition = part_id
|
||||
row_id = 0
|
||||
if state_dict:
|
||||
state_dict["partition_example_idx"] = row_id + 1
|
||||
yield (part_id, row_id), row_as_dict
|
||||
row_id += 1
|
||||
|
||||
|
||||
class SparkExamplesIterable(_BaseExamplesIterable):
|
||||
def __init__(
|
||||
self,
|
||||
df: "pyspark.sql.DataFrame",
|
||||
partition_order=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.df = df
|
||||
self.partition_order = partition_order or range(self.df.rdd.getNumPartitions())
|
||||
|
||||
def _init_state_dict(self) -> dict:
|
||||
self._state_dict = {"partition_idx": 0, "partition_example_idx": 0}
|
||||
return self._state_dict
|
||||
|
||||
@experimental
|
||||
def load_state_dict(self, state_dict: dict) -> dict:
|
||||
return super().load_state_dict(state_dict)
|
||||
|
||||
def __iter__(self):
|
||||
yield from _generate_iterable_examples(self.df, self.partition_order, self._state_dict)
|
||||
|
||||
def shuffle_data_sources(self, generator: np.random.Generator) -> "SparkExamplesIterable":
|
||||
partition_order = list(range(self.df.rdd.getNumPartitions()))
|
||||
generator.shuffle(partition_order)
|
||||
return SparkExamplesIterable(self.df, partition_order=partition_order)
|
||||
|
||||
def shard_data_sources(self, num_shards: int, index: int, contiguous=True) -> "SparkExamplesIterable":
|
||||
partition_order = self.split_shard_indices_by_worker(num_shards=num_shards, index=index, contiguous=contiguous)
|
||||
return SparkExamplesIterable(self.df, partition_order=partition_order)
|
||||
|
||||
@property
|
||||
def num_shards(self) -> int:
|
||||
return len(self.partition_order)
|
||||
|
||||
|
||||
class Spark(datasets.DatasetBuilder):
|
||||
BUILDER_CONFIG_CLASS = SparkConfig
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
df: "pyspark.sql.DataFrame",
|
||||
cache_dir: str = None,
|
||||
working_dir: str = None,
|
||||
**config_kwargs,
|
||||
):
|
||||
import pyspark
|
||||
|
||||
self._spark = pyspark.sql.SparkSession.builder.getOrCreate()
|
||||
self.df = df
|
||||
self._working_dir = working_dir
|
||||
|
||||
super().__init__(
|
||||
cache_dir=cache_dir,
|
||||
config_name=str(self.df.semanticHash()),
|
||||
**config_kwargs,
|
||||
)
|
||||
|
||||
def _validate_cache_dir(self):
|
||||
# Define this so that we don't reference self in create_cache_and_write_probe, which will result in a pickling
|
||||
# error due to pickling the SparkContext.
|
||||
cache_dir = self._cache_dir
|
||||
|
||||
# Returns the path of the created file.
|
||||
def create_cache_and_write_probe(context):
|
||||
# makedirs with exist_ok will recursively create the directory. It will not throw an error if directories
|
||||
# already exist.
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
probe_file = os.path.join(cache_dir, "fs_test" + uuid.uuid4().hex)
|
||||
# Opening the file in append mode will create a new file unless it already exists, in which case it will not
|
||||
# change the file contents.
|
||||
open(probe_file, "a")
|
||||
return [probe_file]
|
||||
|
||||
if self._spark.conf.get("spark.master", "").startswith("local"):
|
||||
return
|
||||
|
||||
# If the cluster is multi-node, make sure that the user provided a cache_dir and that it is on an NFS
|
||||
# accessible to the driver.
|
||||
# TODO: Stream batches to the driver using ArrowCollectSerializer instead of throwing an error.
|
||||
if self._cache_dir:
|
||||
probe = (
|
||||
self._spark.sparkContext.parallelize(range(1), 1).mapPartitions(create_cache_and_write_probe).collect()
|
||||
)
|
||||
if os.path.isfile(probe[0]):
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
"When using Dataset.from_spark on a multi-node cluster, the driver and all workers should be able to access cache_dir"
|
||||
)
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager: datasets.download.download_manager.DownloadManager):
|
||||
return [datasets.SplitGenerator(name=datasets.Split.TRAIN)]
|
||||
|
||||
def _repartition_df_if_needed(self, max_shard_size):
|
||||
import pyspark
|
||||
|
||||
def get_arrow_batch_size(it):
|
||||
for batch in it:
|
||||
yield pa.RecordBatch.from_pydict({"batch_bytes": [batch.nbytes]})
|
||||
|
||||
df_num_rows = self.df.count()
|
||||
sample_num_rows = df_num_rows if df_num_rows <= 100 else 100
|
||||
# Approximate the size of each row (in Arrow format) by averaging over a max-100-row sample.
|
||||
approx_bytes_per_row = (
|
||||
self.df.limit(sample_num_rows)
|
||||
.repartition(1)
|
||||
.mapInArrow(get_arrow_batch_size, "batch_bytes: long")
|
||||
.agg(pyspark.sql.functions.sum("batch_bytes").alias("sample_bytes"))
|
||||
.collect()[0]
|
||||
.sample_bytes
|
||||
/ sample_num_rows
|
||||
)
|
||||
approx_total_size = approx_bytes_per_row * df_num_rows
|
||||
if approx_total_size > max_shard_size:
|
||||
# Make sure there is at least one row per partition.
|
||||
new_num_partitions = min(df_num_rows, int(approx_total_size / max_shard_size))
|
||||
self.df = self.df.repartition(new_num_partitions)
|
||||
|
||||
def _prepare_split_single(
|
||||
self,
|
||||
fpath: str,
|
||||
file_format: str,
|
||||
max_shard_size: int,
|
||||
) -> Iterable[tuple[int, bool, Union[int, tuple]]]:
|
||||
import pyspark
|
||||
|
||||
writer_class = ParquetWriter if file_format == "parquet" else ArrowWriter
|
||||
working_fpath = os.path.join(self._working_dir, os.path.basename(fpath)) if self._working_dir else fpath
|
||||
embed_local_files = file_format == "parquet"
|
||||
|
||||
# Define these so that we don't reference self in write_arrow, which will result in a pickling error due to
|
||||
# pickling the SparkContext.
|
||||
features = self.config.features
|
||||
writer_batch_size = self._writer_batch_size
|
||||
storage_options = self._fs.storage_options
|
||||
|
||||
def write_arrow(it):
|
||||
# Within the same SparkContext, no two task attempts will share the same attempt ID.
|
||||
task_id = pyspark.TaskContext().taskAttemptId()
|
||||
first_batch = next(it, None)
|
||||
if first_batch is None:
|
||||
# Some partitions might not receive any data.
|
||||
return pa.RecordBatch.from_arrays(
|
||||
[[task_id], [0], [0]],
|
||||
names=["task_id", "num_examples", "num_bytes"],
|
||||
)
|
||||
shard_id = 0
|
||||
writer = writer_class(
|
||||
features=features,
|
||||
path=working_fpath.replace("SSSSS", f"{shard_id:05d}").replace("TTTTT", f"{task_id:05d}"),
|
||||
writer_batch_size=writer_batch_size,
|
||||
storage_options=storage_options,
|
||||
embed_local_files=embed_local_files,
|
||||
)
|
||||
table = pa.Table.from_batches([first_batch])
|
||||
writer.write_table(table)
|
||||
for batch in it:
|
||||
if max_shard_size is not None and writer._num_bytes >= max_shard_size:
|
||||
num_examples, num_bytes = writer.finalize()
|
||||
writer.close()
|
||||
yield pa.RecordBatch.from_arrays(
|
||||
[[task_id], [num_examples], [num_bytes]],
|
||||
names=["task_id", "num_examples", "num_bytes"],
|
||||
)
|
||||
shard_id += 1
|
||||
writer = writer_class(
|
||||
features=writer._features,
|
||||
path=working_fpath.replace("SSSSS", f"{shard_id:05d}").replace("TTTTT", f"{task_id:05d}"),
|
||||
writer_batch_size=writer_batch_size,
|
||||
storage_options=storage_options,
|
||||
embed_local_files=embed_local_files,
|
||||
)
|
||||
table = pa.Table.from_batches([batch])
|
||||
writer.write_table(table)
|
||||
|
||||
if writer._num_bytes > 0:
|
||||
num_examples, num_bytes = writer.finalize()
|
||||
writer.close()
|
||||
yield pa.RecordBatch.from_arrays(
|
||||
[[task_id], [num_examples], [num_bytes]],
|
||||
names=["task_id", "num_examples", "num_bytes"],
|
||||
)
|
||||
|
||||
if working_fpath != fpath:
|
||||
for file in os.listdir(os.path.dirname(working_fpath)):
|
||||
dest = os.path.join(os.path.dirname(fpath), os.path.basename(file))
|
||||
shutil.move(file, dest)
|
||||
|
||||
stats = (
|
||||
self.df.mapInArrow(write_arrow, "task_id: long, num_examples: long, num_bytes: long")
|
||||
.groupBy("task_id")
|
||||
.agg(
|
||||
pyspark.sql.functions.sum("num_examples").alias("total_num_examples"),
|
||||
pyspark.sql.functions.sum("num_bytes").alias("total_num_bytes"),
|
||||
pyspark.sql.functions.count("num_bytes").alias("num_shards"),
|
||||
pyspark.sql.functions.collect_list("num_examples").alias("shard_lengths"),
|
||||
)
|
||||
.collect()
|
||||
)
|
||||
for row in stats:
|
||||
yield row.task_id, (row.total_num_examples, row.total_num_bytes, row.num_shards, row.shard_lengths)
|
||||
|
||||
def _prepare_split(
|
||||
self,
|
||||
split_generator: "datasets.SplitGenerator",
|
||||
file_format: str = "arrow",
|
||||
max_shard_size: Optional[Union[str, int]] = None,
|
||||
num_proc: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
self._validate_cache_dir()
|
||||
|
||||
max_shard_size = convert_file_size_to_int(max_shard_size or MAX_SHARD_SIZE)
|
||||
self._repartition_df_if_needed(max_shard_size)
|
||||
is_local = not is_remote_filesystem(self._fs)
|
||||
path_join = os.path.join if is_local else posixpath.join
|
||||
|
||||
SUFFIX = "-TTTTT-SSSSS-of-NNNNN"
|
||||
fname = f"{self.name}-{split_generator.name}{SUFFIX}.{file_format}"
|
||||
fpath = path_join(self._output_dir, fname)
|
||||
|
||||
total_num_examples = 0
|
||||
total_num_bytes = 0
|
||||
total_shards = 0
|
||||
task_id_and_num_shards = []
|
||||
all_shard_lengths = []
|
||||
|
||||
for task_id, content in self._prepare_split_single(fpath, file_format, max_shard_size):
|
||||
(
|
||||
num_examples,
|
||||
num_bytes,
|
||||
num_shards,
|
||||
shard_lengths,
|
||||
) = content
|
||||
if num_bytes > 0:
|
||||
total_num_examples += num_examples
|
||||
total_num_bytes += num_bytes
|
||||
total_shards += num_shards
|
||||
task_id_and_num_shards.append((task_id, num_shards))
|
||||
all_shard_lengths.extend(shard_lengths)
|
||||
|
||||
split_generator.split_info.num_examples = total_num_examples
|
||||
split_generator.split_info.num_bytes = total_num_bytes
|
||||
|
||||
# should rename everything at the end
|
||||
logger.debug(f"Renaming {total_shards} shards.")
|
||||
if total_shards > 1:
|
||||
split_generator.split_info.shard_lengths = all_shard_lengths
|
||||
|
||||
# Define fs outside of _rename_shard so that we don't reference self in the function, which will result in a
|
||||
# pickling error due to pickling the SparkContext.
|
||||
fs = self._fs
|
||||
|
||||
# use the -SSSSS-of-NNNNN pattern
|
||||
def _rename_shard(
|
||||
task_id: int,
|
||||
shard_id: int,
|
||||
global_shard_id: int,
|
||||
):
|
||||
rename(
|
||||
fs,
|
||||
fpath.replace("SSSSS", f"{shard_id:05d}").replace("TTTTT", f"{task_id:05d}"),
|
||||
fpath.replace("TTTTT-SSSSS", f"{global_shard_id:05d}").replace("NNNNN", f"{total_shards:05d}"),
|
||||
)
|
||||
|
||||
args = []
|
||||
global_shard_id = 0
|
||||
for i in range(len(task_id_and_num_shards)):
|
||||
task_id, num_shards = task_id_and_num_shards[i]
|
||||
for shard_id in range(num_shards):
|
||||
args.append([task_id, shard_id, global_shard_id])
|
||||
global_shard_id += 1
|
||||
self._spark.sparkContext.parallelize(args, len(args)).map(lambda args: _rename_shard(*args)).collect()
|
||||
else:
|
||||
# don't use any pattern
|
||||
shard_id = 0
|
||||
task_id = task_id_and_num_shards[0][0]
|
||||
self._rename(
|
||||
fpath.replace("SSSSS", f"{shard_id:05d}").replace("TTTTT", f"{task_id:05d}"),
|
||||
fpath.replace(SUFFIX, ""),
|
||||
)
|
||||
|
||||
def _get_examples_iterable_for_split(
|
||||
self,
|
||||
split_generator: "datasets.SplitGenerator",
|
||||
) -> SparkExamplesIterable:
|
||||
return SparkExamplesIterable(self.df)
|
||||
@@ -0,0 +1,120 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
import datasets.config
|
||||
from datasets.builder import Key
|
||||
from datasets.features.features import require_storage_cast
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sqlite3
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SqlConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for SQL."""
|
||||
|
||||
sql: Union[str, "sqlalchemy.sql.Selectable"] = None
|
||||
con: Union[str, "sqlalchemy.engine.Connection", "sqlalchemy.engine.Engine", "sqlite3.Connection"] = None
|
||||
index_col: Optional[Union[str, list[str]]] = None
|
||||
coerce_float: bool = True
|
||||
params: Optional[Union[list, tuple, dict]] = None
|
||||
parse_dates: Optional[Union[list, dict]] = None
|
||||
columns: Optional[list[str]] = None
|
||||
chunksize: Optional[int] = 10_000
|
||||
features: Optional[datasets.Features] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.sql is None:
|
||||
raise ValueError("sql must be specified")
|
||||
if self.con is None:
|
||||
raise ValueError("con must be specified")
|
||||
|
||||
def create_config_id(
|
||||
self,
|
||||
config_kwargs: dict,
|
||||
custom_features: Optional[datasets.Features] = None,
|
||||
) -> str:
|
||||
config_kwargs = config_kwargs.copy()
|
||||
# We need to stringify the Selectable object to make its hash deterministic
|
||||
|
||||
# The process of stringifying is explained here: http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html
|
||||
sql = config_kwargs["sql"]
|
||||
if not isinstance(sql, str):
|
||||
if datasets.config.SQLALCHEMY_AVAILABLE and "sqlalchemy" in sys.modules:
|
||||
import sqlalchemy
|
||||
|
||||
if isinstance(sql, sqlalchemy.sql.Selectable):
|
||||
engine = sqlalchemy.create_engine(config_kwargs["con"].split("://")[0] + "://")
|
||||
sql_str = str(sql.compile(dialect=engine.dialect))
|
||||
config_kwargs["sql"] = sql_str
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Supported types for 'sql' are string and sqlalchemy.sql.Selectable but got {type(sql)}: {sql}"
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Supported types for 'sql' are string and sqlalchemy.sql.Selectable but got {type(sql)}: {sql}"
|
||||
)
|
||||
con = config_kwargs["con"]
|
||||
if not isinstance(con, str):
|
||||
config_kwargs["con"] = id(con)
|
||||
logger.info(
|
||||
f"SQL connection 'con' of type {type(con)} couldn't be hashed properly. To enable hashing, specify 'con' as URI string instead."
|
||||
)
|
||||
|
||||
return super().create_config_id(config_kwargs, custom_features=custom_features)
|
||||
|
||||
@property
|
||||
def pd_read_sql_kwargs(self):
|
||||
pd_read_sql_kwargs = {
|
||||
"index_col": self.index_col,
|
||||
"columns": self.columns,
|
||||
"params": self.params,
|
||||
"coerce_float": self.coerce_float,
|
||||
"parse_dates": self.parse_dates,
|
||||
}
|
||||
return pd_read_sql_kwargs
|
||||
|
||||
|
||||
class Sql(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = SqlConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
return [datasets.SplitGenerator(name=datasets.Split.TRAIN, gen_kwargs={})]
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.config.features is not None:
|
||||
schema = self.config.features.arrow_schema
|
||||
if all(not require_storage_cast(feature) for feature in self.config.features.values()):
|
||||
# cheaper cast
|
||||
pa_table = pa.Table.from_arrays([pa_table[field.name] for field in schema], schema=schema)
|
||||
else:
|
||||
# more expensive cast; allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, schema)
|
||||
return pa_table
|
||||
|
||||
def _generate_tables(self):
|
||||
chunksize = self.config.chunksize
|
||||
sql_reader = pd.read_sql(
|
||||
self.config.sql, self.config.con, chunksize=chunksize, **self.config.pd_read_sql_kwargs
|
||||
)
|
||||
sql_reader = [sql_reader] if chunksize is None else sql_reader
|
||||
for chunk_idx, df in enumerate(sql_reader):
|
||||
pa_table = pa.Table.from_pandas(df)
|
||||
yield Key(0, chunk_idx), self._cast_table(pa_table)
|
||||
@@ -0,0 +1,137 @@
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.features.features import require_storage_cast
|
||||
from datasets.table import table_cast
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for text files.
|
||||
|
||||
Args:
|
||||
features: (`Features`, *optional*):
|
||||
Cast the data to `features`.
|
||||
encoding: (`str`, defaults to "utf-8"):
|
||||
Encoding to decode the file.
|
||||
encoding_errors: (`str`, *optional*):
|
||||
Argument to define what to do in case of encoding error.
|
||||
This is the same as the `error` argument in `open()`.
|
||||
chunksize: (`Features`, *optional*, defaults to "10MB"):
|
||||
Chunk size to read the data.
|
||||
keep_linebreaks: (`bool`, defaults to False):
|
||||
Whether to keep line breaks.
|
||||
sample_by (`Literal["line", "paragraph", "document"]`, defaults to "line"):
|
||||
Whether to load data per line, praragraph or document.
|
||||
By default one row in the dataset = one line.
|
||||
"""
|
||||
|
||||
features: Optional[datasets.Features] = None
|
||||
encoding: str = "utf-8"
|
||||
encoding_errors: Optional[str] = None
|
||||
chunksize: int = 10 << 20 # 10MB
|
||||
keep_linebreaks: bool = False
|
||||
sample_by: Literal["line", "paragraph", "document"] = "line"
|
||||
|
||||
|
||||
class Text(datasets.ArrowBasedBuilder):
|
||||
BUILDER_CONFIG_CLASS = TextConfig
|
||||
|
||||
def _info(self):
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
"""The `data_files` kwarg in load_dataset() can be a str, List[str], Dict[str,str], or Dict[str,List[str]].
|
||||
|
||||
If str or List[str], then the dataset returns only the 'train' split.
|
||||
If dict, then keys should be from the `datasets.Split` enum.
|
||||
"""
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
base_data_files = dl_manager.download(self.config.data_files)
|
||||
extracted_data_files = dl_manager.extract(base_data_files)
|
||||
splits = []
|
||||
for split_name, files in extracted_data_files.items():
|
||||
files_iterables = [dl_manager.iter_files(file) for file in files]
|
||||
splits.append(
|
||||
datasets.SplitGenerator(
|
||||
name=split_name,
|
||||
gen_kwargs={"files_iterables": files_iterables, "base_files": base_data_files[split_name]},
|
||||
)
|
||||
)
|
||||
return splits
|
||||
|
||||
def _cast_table(self, pa_table: pa.Table) -> pa.Table:
|
||||
if self.config.features is not None:
|
||||
schema = self.config.features.arrow_schema
|
||||
if all(not require_storage_cast(feature) for feature in self.config.features.values()):
|
||||
# cheaper cast
|
||||
pa_table = pa_table.cast(schema)
|
||||
else:
|
||||
# more expensive cast; allows str <-> int/float or str to Audio for example
|
||||
pa_table = table_cast(pa_table, schema)
|
||||
return pa_table
|
||||
else:
|
||||
return pa_table.cast(pa.schema({"text": pa.string()}))
|
||||
|
||||
def _generate_shards(self, base_files, files_iterables):
|
||||
yield from base_files
|
||||
|
||||
def _generate_tables(self, base_files, files_iterables):
|
||||
pa_table_names = list(self.config.features) if self.config.features is not None else ["text"]
|
||||
for shard_idx, files_iterable in enumerate(files_iterables):
|
||||
for file in files_iterable:
|
||||
# open in text mode, by default translates universal newlines ("\n", "\r\n" and "\r") into "\n"
|
||||
with open(file, encoding=self.config.encoding, errors=self.config.encoding_errors) as f:
|
||||
if self.config.sample_by == "line":
|
||||
batch_idx = 0
|
||||
while True:
|
||||
batch = f.read(self.config.chunksize)
|
||||
if not batch:
|
||||
break
|
||||
batch += f.readline() # finish current line
|
||||
# StringIO.readlines, by default splits only on "\n" (and keeps line breaks)
|
||||
batch = StringIO(batch).readlines()
|
||||
if not self.config.keep_linebreaks:
|
||||
batch = [line.rstrip("\n") for line in batch]
|
||||
pa_table = pa.Table.from_arrays([pa.array(batch)], names=pa_table_names)
|
||||
# Uncomment for debugging (will print the Arrow table size and elements)
|
||||
# logger.warning(f"pa_table: {pa_table} num rows: {pa_table.num_rows}")
|
||||
# logger.warning('\n'.join(str(pa_table.slice(i, 1).to_pydict()) for i in range(pa_table.num_rows)))
|
||||
yield Key(shard_idx, batch_idx), self._cast_table(pa_table)
|
||||
batch_idx += 1
|
||||
elif self.config.sample_by == "paragraph":
|
||||
batch_idx = 0
|
||||
batch = ""
|
||||
while True:
|
||||
new_batch = f.read(self.config.chunksize)
|
||||
if not new_batch:
|
||||
break
|
||||
batch += new_batch
|
||||
batch += f.readline() # finish current line
|
||||
batch = batch.split("\n\n")
|
||||
pa_table = pa.Table.from_arrays(
|
||||
[pa.array([example for example in batch[:-1] if example])], names=pa_table_names
|
||||
)
|
||||
# Uncomment for debugging (will print the Arrow table size and elements)
|
||||
# logger.warning(f"pa_table: {pa_table} num rows: {pa_table.num_rows}")
|
||||
# logger.warning('\n'.join(str(pa_table.slice(i, 1).to_pydict()) for i in range(pa_table.num_rows)))
|
||||
yield Key(shard_idx, batch_idx), self._cast_table(pa_table)
|
||||
batch_idx += 1
|
||||
batch = batch[-1]
|
||||
if batch:
|
||||
pa_table = pa.Table.from_arrays([pa.array([batch])], names=pa_table_names)
|
||||
yield (shard_idx, batch_idx), self._cast_table(pa_table)
|
||||
elif self.config.sample_by == "document":
|
||||
text = f.read()
|
||||
pa_table = pa.Table.from_arrays([pa.array([text])], names=pa_table_names)
|
||||
yield Key(shard_idx, 0), self._cast_table(pa_table)
|
||||
@@ -0,0 +1,773 @@
|
||||
"""TsFile (table model) packaged builder — per-device wide format.
|
||||
|
||||
Each output row corresponds to a single device (identified by its TAG values).
|
||||
The ``time`` column and every FIELD column are Arrow ``list<...>`` columns
|
||||
holding the entire time series for that device. When the same device is
|
||||
present in multiple TsFiles within a split, its data is merged across files
|
||||
and the resulting lists are sorted in ascending time order.
|
||||
|
||||
Output schema layout::
|
||||
|
||||
<tag1>: string
|
||||
<tag2>: string (one column per TAG)
|
||||
...
|
||||
time: list<timestamp[unit, tz]>
|
||||
<field1>: list<original_type> (one column per FIELD)
|
||||
<field2>: list<original_type>
|
||||
...
|
||||
|
||||
Reading model
|
||||
-------------
|
||||
Data is fetched **per device** via ``TsFileReader.query_table`` with a
|
||||
push-down ``tag_filter``. For each split the builder:
|
||||
|
||||
1. Opens every input file once, calls ``get_all_devices`` to enumerate the
|
||||
``(tag-tuple) → [files]`` index across all shards.
|
||||
2. Iterates the index in stable order. For each device, streams Arrow
|
||||
batches from every contributing file, concatenates and sorts by time,
|
||||
and emits one wide row.
|
||||
|
||||
Peak memory is bounded by **one device's** total payload across the split,
|
||||
not by the split's total size.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as _dt
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
|
||||
import datasets
|
||||
from datasets.builder import Key
|
||||
from datasets.table import table_cast
|
||||
from datasets.utils.tqdm import tqdm
|
||||
|
||||
|
||||
logger = datasets.utils.logging.get_logger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _arrow_type(ts_dtype, *, unit: str, tz: Optional[str]) -> pa.DataType:
|
||||
"""Map a tsfile ``TSDataType`` to its Arrow representation."""
|
||||
from tsfile.constants import TSDataType
|
||||
|
||||
return {
|
||||
TSDataType.BOOLEAN: pa.bool_(),
|
||||
TSDataType.INT32: pa.int32(),
|
||||
TSDataType.INT64: pa.int64(),
|
||||
TSDataType.FLOAT: pa.float32(),
|
||||
TSDataType.DOUBLE: pa.float64(),
|
||||
TSDataType.TEXT: pa.string(),
|
||||
TSDataType.STRING: pa.string(),
|
||||
TSDataType.TIMESTAMP: pa.timestamp(unit, tz=tz),
|
||||
TSDataType.DATE: pa.date32(),
|
||||
TSDataType.BLOB: pa.binary(),
|
||||
}.get(ts_dtype, pa.string())
|
||||
|
||||
|
||||
def _promote_tsdatatype(a, b):
|
||||
"""Return the widest of two ``TSDataType`` values.
|
||||
|
||||
Mirrors IoTDB's ``ALTER COLUMN ... SET DATA TYPE`` rules:
|
||||
|
||||
- ``INT32 → INT64 → DOUBLE``
|
||||
- ``INT32 → FLOAT → DOUBLE``
|
||||
|
||||
``INT64`` and ``FLOAT`` cannot widen losslessly into either, so their
|
||||
join is ``DOUBLE``. Non-numeric or otherwise unrelated pairs raise.
|
||||
"""
|
||||
if a == b:
|
||||
return a
|
||||
|
||||
from tsfile.constants import TSDataType
|
||||
|
||||
table = {
|
||||
(TSDataType.INT32, TSDataType.INT64): TSDataType.INT64,
|
||||
(TSDataType.INT32, TSDataType.FLOAT): TSDataType.FLOAT,
|
||||
(TSDataType.INT32, TSDataType.DOUBLE): TSDataType.DOUBLE,
|
||||
(TSDataType.INT64, TSDataType.FLOAT): TSDataType.DOUBLE,
|
||||
(TSDataType.INT64, TSDataType.DOUBLE): TSDataType.DOUBLE,
|
||||
(TSDataType.FLOAT, TSDataType.DOUBLE): TSDataType.DOUBLE,
|
||||
}
|
||||
if (a, b) in table:
|
||||
return table[(a, b)]
|
||||
if (b, a) in table:
|
||||
return table[(b, a)]
|
||||
raise ValueError(
|
||||
f"Incompatible column types across files: {a.name} vs {b.name}. "
|
||||
"Only numeric widening (INT32→INT64→DOUBLE, INT32→FLOAT→DOUBLE) is supported."
|
||||
)
|
||||
|
||||
|
||||
def _to_epoch(value: Any, unit: str) -> int:
|
||||
"""Coerce a timestamp boundary to an integer epoch in ``unit``.
|
||||
|
||||
Accepts ``int`` (raw epoch in ``unit``), ``datetime``/``date``,
|
||||
ISO-8601 ``str``, or any ``pa.Scalar`` of timestamp type.
|
||||
"""
|
||||
if isinstance(value, bool): # bool is a subclass of int; reject explicitly
|
||||
raise TypeError(f"start_time/end_time must be a timestamp, got bool: {value!r}")
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
try:
|
||||
# Normalize the various input shapes into something pa.scalar() can absorb
|
||||
# under a `timestamp[unit]` target type.
|
||||
if isinstance(value, _dt.datetime):
|
||||
if value.tzinfo is not None:
|
||||
value = value.astimezone(_dt.timezone.utc).replace(tzinfo=None)
|
||||
elif isinstance(value, _dt.date):
|
||||
value = _dt.datetime(value.year, value.month, value.day)
|
||||
elif isinstance(value, str):
|
||||
value = _dt.datetime.fromisoformat(value)
|
||||
return pa.scalar(value, type=pa.timestamp(unit)).value
|
||||
except (pa.ArrowInvalid, pa.ArrowTypeError, TypeError, ValueError) as e:
|
||||
raise TypeError(
|
||||
f"start_time/end_time must be a datetime, date, pa.TimestampScalar, "
|
||||
f"ISO-8601 str, or int epoch; got {type(value).__name__}: {value!r}"
|
||||
) from e
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class TsFileConfig(datasets.BuilderConfig):
|
||||
"""BuilderConfig for TsFile (table model) — per-device wide format.
|
||||
|
||||
Args:
|
||||
table_name (`str`, *optional*):
|
||||
Name of the table to read. When unset, the first table found in
|
||||
the first valid file is used. Lookups are case-insensitive.
|
||||
columns (`list[str]`, *optional*):
|
||||
Subset of FIELD columns to keep. TAG columns and the TIME column
|
||||
are *always* returned (they identify the device / its timeline
|
||||
and cannot be excluded). Names that refer to TAG or TIME columns,
|
||||
or to fields absent from every file, resolve quietly: TAGs/TIME
|
||||
are emitted as usual, and never-seen fields become all-null list
|
||||
columns. When unset, all FIELDs are returned.
|
||||
start_time, end_time (`datetime`, `date`, `pa.TimestampScalar`, ISO-8601 `str`, or `int`, *optional*):
|
||||
Inclusive timestamp range. Either bound may be omitted.
|
||||
``datetime`` values are taken in their own tz (UTC if naive);
|
||||
``int`` is interpreted as a raw epoch in ``timestamp_unit``.
|
||||
input_batch_size (`int`, *optional*, defaults to 65_536):
|
||||
Maximum number of rows fetched per Arrow batch from
|
||||
``TsFileReader.query_table``. Controls peak memory while
|
||||
streaming a single device.
|
||||
output_batch_size (`int`, *optional*, defaults to 32):
|
||||
Number of devices (output dataset rows) packed into each Arrow
|
||||
record batch yielded to the writer. Also the granularity at
|
||||
which the dataset progress bar advances; smaller values give
|
||||
more responsive feedback on slow per-device reads, larger ones
|
||||
reduce per-batch overhead.
|
||||
features (`Features`, *optional*):
|
||||
Final Features schema. When provided, the metadata scan over
|
||||
input files is skipped.
|
||||
on_bad_files (`Literal["error", "warn", "skip"]`, *optional*, defaults to "error"):
|
||||
What to do if a file cannot be opened or lacks the requested table.
|
||||
timestamp_unit (`Literal["s", "ms", "us", "ns"]`, *optional*, defaults to "ms"):
|
||||
Time unit for the timestamp column. IoTDB defaults to milliseconds.
|
||||
timestamp_tz (`str`, *optional*):
|
||||
Time zone for the timestamp column. ``None`` means timezone-naive.
|
||||
"""
|
||||
|
||||
table_name: Optional[str] = None
|
||||
columns: Optional[list[str]] = None
|
||||
start_time: Optional[Any] = None
|
||||
end_time: Optional[Any] = None
|
||||
input_batch_size: int = 65_536
|
||||
output_batch_size: int = 32
|
||||
features: Optional[datasets.Features] = None
|
||||
on_bad_files: Literal["error", "warn", "skip"] = "error"
|
||||
timestamp_unit: Literal["s", "ms", "us", "ns"] = "ms"
|
||||
timestamp_tz: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.input_batch_size is None or self.input_batch_size <= 0:
|
||||
raise ValueError(f"`input_batch_size` must be a positive integer, got {self.input_batch_size}")
|
||||
if self.output_batch_size is None or self.output_batch_size <= 0:
|
||||
raise ValueError(f"`output_batch_size` must be a positive integer, got {self.output_batch_size}")
|
||||
if self.columns is not None and len(self.columns) == 0:
|
||||
raise ValueError("`columns` must be a non-empty list when provided.")
|
||||
if self.timestamp_unit not in ("s", "ms", "us", "ns"):
|
||||
raise ValueError(f"`timestamp_unit` must be one of 's', 'ms', 'us', 'ns', got {self.timestamp_unit!r}")
|
||||
if self.on_bad_files not in ("error", "warn", "skip"):
|
||||
raise ValueError(f"`on_bad_files` must be one of 'error', 'warn', 'skip', got {self.on_bad_files!r}")
|
||||
if self.start_time is not None:
|
||||
self.start_time = _to_epoch(self.start_time, self.timestamp_unit)
|
||||
if self.end_time is not None:
|
||||
self.end_time = _to_epoch(self.end_time, self.timestamp_unit)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal sentinels
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _SkipSplit(Exception):
|
||||
"""Raised internally to abort emitting a split entirely."""
|
||||
|
||||
|
||||
class _MissingTableError(ValueError):
|
||||
def __init__(self, table: Optional[str], available):
|
||||
super().__init__(f"Table {table!r} not found in file. Available tables: {available}")
|
||||
|
||||
|
||||
_TSFILE_MAGIC = b"TsFile"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Builder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TsFile(datasets.ArrowBasedBuilder):
|
||||
"""Per-device wide-format builder for TsFile (table model)."""
|
||||
|
||||
BUILDER_CONFIG_CLASS = TsFileConfig
|
||||
|
||||
# ----- builder hooks ------------------------------------------------
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._table: Optional[str] = None
|
||||
self._time_col: str = "time"
|
||||
self._tag_cols: list[str] = []
|
||||
self._field_inner: dict[str, pa.DataType] = {}
|
||||
self._requested_fields: Optional[list[str]] = None # lowercased
|
||||
|
||||
def _info(self):
|
||||
if (
|
||||
self.config.columns is not None
|
||||
and self.config.features is not None
|
||||
and not set(self.config.columns).issubset(set(self.config.features))
|
||||
):
|
||||
raise ValueError(
|
||||
"Every entry in `columns` must also appear in `features`, but got "
|
||||
f"columns={self.config.columns} and features={list(self.config.features)}"
|
||||
)
|
||||
return datasets.DatasetInfo(features=self.config.features)
|
||||
|
||||
def _split_generators(self, dl_manager):
|
||||
if not self.config.data_files:
|
||||
raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
|
||||
dl_manager.download_config.extract_on_the_fly = True
|
||||
data_files = dl_manager.download(self.config.data_files)
|
||||
|
||||
# Lowercase user-facing names to match tsfile's case-insensitive convention.
|
||||
self._table = self.config.table_name.lower() if self.config.table_name else None
|
||||
self._requested_fields = [c.lower() for c in self.config.columns] if self.config.columns else None
|
||||
|
||||
all_files = [f for files in data_files.values() for f in files]
|
||||
scan = self._scan_metadata(all_files)
|
||||
if scan is None:
|
||||
raise ValueError(
|
||||
"Could not infer schema from any of the provided files. "
|
||||
"Set `features` explicitly or check the input files."
|
||||
)
|
||||
self._table = scan["table"]
|
||||
self._time_col = scan["time_col"]
|
||||
self._tag_cols = scan["tag_cols"]
|
||||
self._field_inner = scan["field_inner"]
|
||||
|
||||
if self.info.features is None:
|
||||
self.info.features = self._build_features()
|
||||
|
||||
return [
|
||||
datasets.SplitGenerator(name=split, gen_kwargs={"files": list(files)})
|
||||
for split, files in data_files.items()
|
||||
]
|
||||
|
||||
def _generate_shards(self, files):
|
||||
yield from files
|
||||
|
||||
def _generate_tables(self, files):
|
||||
target_schema = self.info.features.arrow_schema
|
||||
try:
|
||||
yield from self._fold_split(files, target_schema)
|
||||
except _SkipSplit:
|
||||
return
|
||||
|
||||
# ----- metadata scan ------------------------------------------------
|
||||
|
||||
def _scan_metadata(self, files) -> Optional[dict]:
|
||||
"""Walk every file and unify table name, TAG columns, FIELD types."""
|
||||
from tsfile.constants import TIME_COLUMN, ColumnCategory
|
||||
|
||||
wanted_table = self._table
|
||||
wanted_fields = set(self._requested_fields) if self._requested_fields is not None else None
|
||||
|
||||
table: Optional[str] = wanted_table
|
||||
time_col: Optional[str] = None
|
||||
tag_cols: list[str] = []
|
||||
tag_seen: set[str] = set()
|
||||
# Per-field widest TSDataType seen so far (we map to Arrow at the end).
|
||||
field_widest: dict = {}
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
with self._open_reader(file) as reader:
|
||||
schemas = self._schemas_by_lc(reader)
|
||||
self._require_table_model(file, schemas)
|
||||
if table is None:
|
||||
table = next(iter(schemas))
|
||||
if table not in schemas:
|
||||
raise _MissingTableError(table, list(schemas))
|
||||
for col in schemas[table].get_columns():
|
||||
name = col.get_column_name()
|
||||
cat = col.get_category()
|
||||
ts_dtype = col.get_data_type()
|
||||
if cat == ColumnCategory.TIME:
|
||||
time_col = name
|
||||
elif cat == ColumnCategory.TAG:
|
||||
if name not in tag_seen:
|
||||
tag_seen.add(name)
|
||||
tag_cols.append(name)
|
||||
else: # FIELD
|
||||
if wanted_fields is not None and name not in wanted_fields:
|
||||
continue
|
||||
prev = field_widest.get(name)
|
||||
field_widest[name] = ts_dtype if prev is None else _promote_tsdatatype(prev, ts_dtype)
|
||||
except Exception as e:
|
||||
if self._should_reraise(file, e):
|
||||
raise
|
||||
continue
|
||||
|
||||
if table is None:
|
||||
return None
|
||||
|
||||
unit = self.config.timestamp_unit
|
||||
tz = self.config.timestamp_tz
|
||||
|
||||
if self._requested_fields is not None:
|
||||
# Honor user order; silently drop names that turned out to be TAGs
|
||||
# or the TIME column (TAGs are emitted as their own scalar columns
|
||||
# and TIME is always emitted as a list column — neither may also
|
||||
# appear as a list-typed field, which would collide on schema name).
|
||||
reserved = tag_seen | {time_col} if time_col is not None else tag_seen
|
||||
field_inner: dict[str, pa.DataType] = {}
|
||||
for name in self._requested_fields:
|
||||
if name in reserved:
|
||||
continue
|
||||
ts_dtype = field_widest.get(name)
|
||||
if ts_dtype is not None:
|
||||
field_inner[name] = _arrow_type(ts_dtype, unit=unit, tz=tz)
|
||||
else:
|
||||
# Field never appeared in any file — keep as a nullable
|
||||
# float64 list, fully filled with nulls at read time.
|
||||
field_inner[name] = pa.float64()
|
||||
else:
|
||||
field_inner = {n: _arrow_type(d, unit=unit, tz=tz) for n, d in field_widest.items()}
|
||||
|
||||
return {
|
||||
"table": table,
|
||||
"time_col": time_col or TIME_COLUMN,
|
||||
"tag_cols": tag_cols,
|
||||
"field_inner": field_inner,
|
||||
}
|
||||
|
||||
def _build_features(self) -> datasets.Features:
|
||||
unit = self.config.timestamp_unit
|
||||
tz = self.config.timestamp_tz
|
||||
fields: list[pa.Field] = [pa.field(t, pa.string()) for t in self._tag_cols]
|
||||
fields.append(pa.field(self._time_col, pa.list_(pa.timestamp(unit, tz=tz))))
|
||||
for name, inner in self._field_inner.items():
|
||||
fields.append(pa.field(name, pa.list_(inner)))
|
||||
return datasets.Features.from_arrow_schema(pa.schema(fields))
|
||||
|
||||
# ----- per-split folding -------------------------------------------
|
||||
|
||||
def _fold_split(self, files, target_schema: pa.Schema):
|
||||
"""Stream every device in this split via per-device tag-filter pushdown.
|
||||
|
||||
Open one ``TsFileReader`` per file, build a cross-file device index
|
||||
keyed by ``(tag-tuple)``, then iterate devices in stable order. For
|
||||
each device, ``query_table(tag_filter=...)`` reads only that device's
|
||||
rows from each contributing file, so peak memory is bounded by one
|
||||
device's payload across the split — never the split's total size.
|
||||
"""
|
||||
if self._table is None:
|
||||
raise _SkipSplit
|
||||
|
||||
readers: dict[str, Any] = {}
|
||||
try:
|
||||
for file in files:
|
||||
try:
|
||||
readers[file] = self._open_reader(file)
|
||||
except Exception as e:
|
||||
if self._should_reraise(file, e):
|
||||
raise
|
||||
continue
|
||||
|
||||
device_index, file_meta = self._build_device_index(readers)
|
||||
if not device_index:
|
||||
return
|
||||
|
||||
yield from self._iter_device_batches(device_index, file_meta, readers, target_schema)
|
||||
finally:
|
||||
for reader in readers.values():
|
||||
try:
|
||||
reader.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _build_device_index(self, readers: dict):
|
||||
"""Walk every open reader and build the cross-file device index.
|
||||
|
||||
Returns ``(device_index, file_meta)``:
|
||||
|
||||
- ``device_index``: list of ``(device_key, [file_path, ...])`` pairs
|
||||
in stable first-seen order. ``device_key`` is a tuple aligned to
|
||||
``self._tag_cols`` (the unified tag-column order).
|
||||
- ``file_meta``: maps each readable file to its per-file context
|
||||
(``tag_cols``, ``field_cols``, ``time_col``).
|
||||
"""
|
||||
from tsfile.constants import ColumnCategory
|
||||
|
||||
device_to_files: dict[tuple, list[str]] = {}
|
||||
device_order: list[tuple] = []
|
||||
file_meta: dict[str, dict] = {}
|
||||
# ``self._table`` was lowercased either by user-input normalization in
|
||||
# ``_split_generators`` or by ``_schemas_by_lc`` during auto-detect.
|
||||
table_lc = self._table
|
||||
|
||||
files_iter = tqdm(
|
||||
readers.items(),
|
||||
total=len(readers),
|
||||
desc="Indexing TsFile devices",
|
||||
unit="file",
|
||||
)
|
||||
|
||||
for file, reader in files_iter:
|
||||
try:
|
||||
schemas = self._schemas_by_lc(reader)
|
||||
self._require_table_model(file, schemas)
|
||||
if table_lc not in schemas:
|
||||
raise _MissingTableError(table_lc, list(schemas))
|
||||
schema = schemas[table_lc]
|
||||
|
||||
file_tag_cols: list[str] = []
|
||||
file_field_cols: set[str] = set()
|
||||
time_col = self._time_col
|
||||
for col in schema.get_columns():
|
||||
name = col.get_column_name()
|
||||
cat = col.get_category()
|
||||
if cat == ColumnCategory.TIME:
|
||||
time_col = name
|
||||
elif cat == ColumnCategory.TAG:
|
||||
file_tag_cols.append(name)
|
||||
elif cat == ColumnCategory.FIELD:
|
||||
file_field_cols.add(name)
|
||||
|
||||
file_meta[file] = {
|
||||
"tag_cols": file_tag_cols,
|
||||
"field_cols": file_field_cols,
|
||||
"time_col": time_col,
|
||||
}
|
||||
|
||||
for device in reader.get_all_devices():
|
||||
if device.table_name is None or device.table_name.lower() != table_lc:
|
||||
continue
|
||||
file_tag_values = list(device.segments[1 : 1 + len(file_tag_cols)])
|
||||
file_tag_dict = dict(zip(file_tag_cols, file_tag_values))
|
||||
unified_key = tuple(file_tag_dict.get(c) for c in self._tag_cols)
|
||||
if any(v is None for v in unified_key):
|
||||
raise ValueError(
|
||||
f"Device in file '{file}' has missing tag values: "
|
||||
f"{dict(zip(self._tag_cols, unified_key))}. "
|
||||
"Schema-evolution devices with NULL tag values are not "
|
||||
"supported because tsfile lacks an IS NULL tag filter."
|
||||
)
|
||||
if unified_key not in device_to_files:
|
||||
device_to_files[unified_key] = []
|
||||
device_order.append(unified_key)
|
||||
device_to_files[unified_key].append(file)
|
||||
except Exception as e:
|
||||
if self._should_reraise(file, e):
|
||||
raise
|
||||
file_meta.pop(file, None)
|
||||
continue
|
||||
|
||||
device_index = [(key, device_to_files[key]) for key in device_order]
|
||||
return device_index, file_meta
|
||||
|
||||
def _iter_device_batches(self, device_index, file_meta, readers, target_schema: pa.Schema):
|
||||
"""Materialize devices in order and emit packed Arrow tables."""
|
||||
field_names = list(self._field_inner.keys())
|
||||
rows: list[dict] = []
|
||||
batch_idx = 0
|
||||
for device_key, contributing_files in device_index:
|
||||
time_chunks: list[np.ndarray] = []
|
||||
field_chunks: dict[str, list] = {f: [] for f in field_names}
|
||||
for file in contributing_files:
|
||||
if file not in file_meta:
|
||||
continue
|
||||
try:
|
||||
ts_arr, vals = self._read_device_from_file(readers[file], file_meta[file], device_key)
|
||||
except Exception as e:
|
||||
if self._should_reraise(file, e):
|
||||
raise
|
||||
continue
|
||||
if len(ts_arr) == 0:
|
||||
continue
|
||||
time_chunks.append(ts_arr)
|
||||
for f in field_names:
|
||||
field_chunks[f].append(vals.get(f)) # None → all-null contribution
|
||||
if not time_chunks:
|
||||
continue # device produced no rows in time range → skip
|
||||
|
||||
row = self._finalize_device(device_key, time_chunks, field_chunks, field_names)
|
||||
rows.append(row)
|
||||
if len(rows) >= self.config.output_batch_size:
|
||||
yield Key(0, batch_idx), self._rows_to_table(rows, target_schema)
|
||||
rows = []
|
||||
batch_idx += 1
|
||||
if rows:
|
||||
yield Key(0, batch_idx), self._rows_to_table(rows, target_schema)
|
||||
|
||||
def _read_device_from_file(self, reader, meta: dict, device_key: tuple) -> tuple:
|
||||
"""Stream one device's rows from one file via ``query_table`` pushdown.
|
||||
|
||||
Returns ``(timestamps, {field_name: values})``. The dict only includes
|
||||
field columns that this file owns *and* that the builder requested;
|
||||
callers fill missing fields with all-null contributions.
|
||||
"""
|
||||
from tsfile import tag_eq
|
||||
|
||||
file_tag_cols: list[str] = meta["tag_cols"]
|
||||
file_field_cols: set[str] = meta["field_cols"]
|
||||
time_col: str = meta["time_col"]
|
||||
|
||||
# Build the tag filter only over this file's tag columns. The unified
|
||||
# device key carries one value per builder tag; map back by name.
|
||||
unified_to_value = dict(zip(self._tag_cols, device_key))
|
||||
tag_filter = None
|
||||
for c in file_tag_cols:
|
||||
v = unified_to_value.get(c)
|
||||
if v is None:
|
||||
# Caught earlier in _build_device_index; defensive guard.
|
||||
return np.array([], dtype=np.int64), {}
|
||||
expr = tag_eq(c, str(v))
|
||||
tag_filter = expr if tag_filter is None else tag_filter & expr
|
||||
|
||||
# Project: requested fields ∩ this file's fields. ``query_table``
|
||||
# always returns the time column, but it requires at least one
|
||||
# non-time column — fall back to any owned field if the user's
|
||||
# selection has nothing in this file.
|
||||
requested = list(self._field_inner.keys())
|
||||
fields_to_query = [f for f in requested if f in file_field_cols]
|
||||
fallback_only = False
|
||||
if not fields_to_query:
|
||||
if not file_field_cols:
|
||||
return np.array([], dtype=np.int64), {}
|
||||
fields_to_query = [next(iter(file_field_cols))]
|
||||
fallback_only = True
|
||||
|
||||
kwargs: dict = {"tag_filter": tag_filter, "batch_size": self.config.input_batch_size}
|
||||
if self.config.start_time is not None:
|
||||
kwargs["start_time"] = self.config.start_time
|
||||
if self.config.end_time is not None:
|
||||
kwargs["end_time"] = self.config.end_time
|
||||
|
||||
ts_parts: list[np.ndarray] = []
|
||||
field_parts: dict[str, list] = {f: [] for f in fields_to_query}
|
||||
with reader.query_table(self._table, fields_to_query, **kwargs) as rs:
|
||||
while True:
|
||||
batch = rs.read_arrow_batch()
|
||||
if batch is None:
|
||||
break
|
||||
if batch.num_rows == 0:
|
||||
continue
|
||||
ts_parts.append(np.asarray(batch.column(time_col).to_numpy(), dtype=np.int64))
|
||||
for f in fields_to_query:
|
||||
col = batch.column(f)
|
||||
# tsfile's arrow reader tags TIMESTAMP / DATE field columns
|
||||
# with a fixed unit (e.g. ``timestamp[ns]``) regardless of
|
||||
# the value's original write unit. Reinterpret as raw
|
||||
# int64/int32 ticks so the downstream
|
||||
# ``pa.array(type=timestamp[<our unit>])`` treats them as
|
||||
# ticks in the unit declared by our schema, instead of
|
||||
# cross-unit casting (which would raise on data loss).
|
||||
if pa.types.is_timestamp(col.type):
|
||||
field_parts[f].append(col.cast(pa.int64()).to_numpy(zero_copy_only=False))
|
||||
elif pa.types.is_date(col.type):
|
||||
field_parts[f].append(col.cast(pa.int32()).to_numpy(zero_copy_only=False))
|
||||
else:
|
||||
field_parts[f].append(col.to_numpy(zero_copy_only=False))
|
||||
|
||||
if not ts_parts:
|
||||
return np.array([], dtype=np.int64), {}
|
||||
ts_full = np.concatenate(ts_parts) if len(ts_parts) > 1 else ts_parts[0]
|
||||
vals_full = {f: (np.concatenate(parts) if len(parts) > 1 else parts[0]) for f, parts in field_parts.items()}
|
||||
|
||||
# Defensive boundary mask: native query paths may emit rows just
|
||||
# outside the requested window in some chunk-boundary cases.
|
||||
if self.config.start_time is not None or self.config.end_time is not None:
|
||||
lo = self.config.start_time if self.config.start_time is not None else np.iinfo(np.int64).min
|
||||
hi = self.config.end_time if self.config.end_time is not None else np.iinfo(np.int64).max
|
||||
mask = (ts_full >= lo) & (ts_full <= hi)
|
||||
if not mask.all():
|
||||
ts_full = ts_full[mask]
|
||||
vals_full = {f: arr[mask] for f, arr in vals_full.items()}
|
||||
|
||||
# Drop the fallback "pick one" column from the user-visible payload.
|
||||
if fallback_only:
|
||||
vals_full = {}
|
||||
return ts_full, vals_full
|
||||
|
||||
def _finalize_device(
|
||||
self,
|
||||
device_key: tuple,
|
||||
time_chunks: list,
|
||||
field_chunks: dict,
|
||||
field_names: list[str],
|
||||
) -> dict:
|
||||
"""Concatenate per-file chunks, sort by time, and return one row.
|
||||
|
||||
Raises ``ValueError`` if the same timestamp appears more than once
|
||||
for a device (within or across files) — tsfile's per-device timeline
|
||||
is required to be unique-by-timestamp.
|
||||
"""
|
||||
time_arr = np.concatenate(time_chunks) if time_chunks else np.array([], dtype=np.int64)
|
||||
n_total = len(time_arr)
|
||||
|
||||
if n_total > 0:
|
||||
sort_idx = np.argsort(time_arr, kind="stable")
|
||||
time_sorted = time_arr[sort_idx]
|
||||
if n_total > 1:
|
||||
dup_mask = time_sorted[1:] == time_sorted[:-1]
|
||||
if dup_mask.any():
|
||||
dup_ts = int(time_sorted[1:][dup_mask][0])
|
||||
raise ValueError(
|
||||
f"Duplicate timestamp {dup_ts} for device "
|
||||
f"{dict(zip(self._tag_cols, device_key))}. "
|
||||
"Cross-file or within-file duplicate timestamps are not supported."
|
||||
)
|
||||
else:
|
||||
sort_idx = None
|
||||
time_sorted = time_arr
|
||||
|
||||
row: dict = {}
|
||||
for tag_name, tag_val in zip(self._tag_cols, device_key):
|
||||
row[tag_name] = None if tag_val is None else str(tag_val)
|
||||
row[self._time_col] = time_sorted
|
||||
|
||||
for fname in field_names:
|
||||
chunks = field_chunks.get(fname, [])
|
||||
materialized: list = []
|
||||
for tchunk, fchunk in zip(time_chunks, chunks):
|
||||
if fchunk is None:
|
||||
materialized.append(np.full(len(tchunk), None, dtype=object))
|
||||
else:
|
||||
materialized.append(fchunk)
|
||||
arr = np.concatenate(materialized) if materialized else np.array([], dtype=object)
|
||||
if sort_idx is not None and len(arr) == n_total:
|
||||
arr = arr[sort_idx]
|
||||
row[fname] = arr
|
||||
return row
|
||||
|
||||
# ----- arrow assembly ----------------------------------------------
|
||||
|
||||
def _rows_to_table(self, rows: list[dict], target_schema: pa.Schema) -> pa.Table:
|
||||
"""Convert a batch of row dicts into an Arrow table matching ``target_schema``."""
|
||||
arrays: list[pa.Array] = []
|
||||
for f in target_schema:
|
||||
values = [r[f.name] for r in rows]
|
||||
if pa.types.is_list(f.type):
|
||||
arrays.append(self._build_list_array(values, f.type))
|
||||
else:
|
||||
arrays.append(pa.array(values, type=f.type))
|
||||
pa_table = pa.Table.from_arrays(arrays, names=[f.name for f in target_schema])
|
||||
return table_cast(pa_table, target_schema)
|
||||
|
||||
@staticmethod
|
||||
def _build_list_array(values: list, list_type: pa.ListType) -> pa.ListArray:
|
||||
"""Build a ``ListArray`` from a list of per-row 1D arrays / sequences."""
|
||||
inner_type = list_type.value_type
|
||||
offsets = [0]
|
||||
flat_chunks: list = []
|
||||
total = 0
|
||||
for v in values:
|
||||
if v is None:
|
||||
length = 0
|
||||
else:
|
||||
length = len(v)
|
||||
flat_chunks.append(v)
|
||||
total += length
|
||||
offsets.append(total)
|
||||
|
||||
if flat_chunks:
|
||||
try:
|
||||
flat = np.concatenate([np.asarray(c) for c in flat_chunks])
|
||||
except ValueError:
|
||||
# Heterogeneous shapes / dtypes → fall back to a Python list.
|
||||
flat = []
|
||||
for c in flat_chunks:
|
||||
flat.extend(list(c))
|
||||
flat_arr = pa.array(flat, type=inner_type, from_pandas=True)
|
||||
else:
|
||||
flat_arr = pa.array([], type=inner_type)
|
||||
|
||||
return pa.ListArray.from_arrays(pa.array(offsets, type=pa.int32()), flat_arr)
|
||||
|
||||
# ----- file / error handling ---------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _open_reader(file: str):
|
||||
"""Open a file as a ``TsFileReader`` after verifying its magic header.
|
||||
|
||||
The C library's ``TsFileReader`` constructor silently returns an
|
||||
invalid handle for non-tsfile inputs, and any subsequent call on it
|
||||
segfaults. The 6-byte ``TsFile`` magic header is checked first to
|
||||
bail out cleanly.
|
||||
"""
|
||||
from tsfile import TsFileReader
|
||||
|
||||
try:
|
||||
with open(file, "rb") as fh:
|
||||
header = fh.read(len(_TSFILE_MAGIC))
|
||||
except OSError as e:
|
||||
raise ValueError(f"Cannot open file {file!r}: {e}") from e
|
||||
if header != _TSFILE_MAGIC:
|
||||
raise ValueError(f"File {file!r} is not a valid TsFile (bad magic header).")
|
||||
return TsFileReader(file)
|
||||
|
||||
@staticmethod
|
||||
def _require_table_model(file: str, schemas) -> None:
|
||||
if not schemas:
|
||||
raise ValueError(
|
||||
f"File {file!r} is a tree-model TsFile, which is not supported. "
|
||||
"Only table-model TsFiles can be loaded."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _schemas_by_lc(reader) -> dict:
|
||||
"""Return ``get_all_table_schemas()`` keyed by lowercased table name.
|
||||
|
||||
TsFile / IoTDB treat table names case-insensitively, but the Python
|
||||
binding's ``get_all_table_schemas()`` returns a dict keyed by whatever
|
||||
casing the file was written with. Lowercasing the keys here lets all
|
||||
downstream lookups use a single canonical form.
|
||||
"""
|
||||
return {name.lower(): schema for name, schema in reader.get_all_table_schemas().items()}
|
||||
|
||||
def _should_reraise(self, file: str, exc: BaseException) -> bool:
|
||||
"""Apply ``on_bad_files`` policy. Returns True iff the caller should re-raise."""
|
||||
mode = self.config.on_bad_files
|
||||
if mode == "error":
|
||||
logger.error(f"Failed to read file '{file}' with error {type(exc).__name__}: {exc}")
|
||||
return True
|
||||
if mode == "warn":
|
||||
logger.warning(f"Skipping bad file '{file}'. {type(exc).__name__}: {exc}")
|
||||
else:
|
||||
logger.debug(f"Skipping bad file '{file}'. {type(exc).__name__}: {exc}")
|
||||
return False
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user