373 lines
12 KiB
Python
373 lines
12 KiB
Python
import logging
|
|
import os
|
|
import posixpath
|
|
import urllib.parse
|
|
from functools import partial
|
|
from unittest.mock import patch
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
from pyarrow.fs import LocalFileSystem
|
|
from pytest_lazy_fixtures import lf as lazy_fixture
|
|
|
|
from ray.data.datasource import (
|
|
BaseFileMetadataProvider,
|
|
DefaultFileMetadataProvider,
|
|
FileMetadataProvider,
|
|
)
|
|
from ray.data.datasource.file_based_datasource import (
|
|
FILE_SIZE_FETCH_PARALLELIZATION_THRESHOLD,
|
|
)
|
|
from ray.data.datasource.file_meta_provider import (
|
|
_get_file_infos_common_path_prefix,
|
|
_get_file_infos_parallel,
|
|
_get_file_infos_serial,
|
|
)
|
|
from ray.data.datasource.path_util import (
|
|
_resolve_paths_and_filesystem,
|
|
_unwrap_protocol,
|
|
)
|
|
from ray.data.tests.conftest import * # noqa
|
|
from ray.data.tests.test_partitioning import PathPartitionEncoder
|
|
from ray.tests.conftest import * # noqa
|
|
|
|
|
|
def df_to_csv(dataframe, path, **kwargs):
|
|
dataframe.to_csv(path, **kwargs)
|
|
|
|
|
|
def _get_file_sizes_bytes(paths, fs):
|
|
from pyarrow.fs import FileType
|
|
|
|
file_sizes = []
|
|
for path in paths:
|
|
file_info = fs.get_file_info(path)
|
|
if file_info.type == FileType.File:
|
|
file_sizes.append(file_info.size)
|
|
else:
|
|
raise FileNotFoundError(path)
|
|
return file_sizes
|
|
|
|
|
|
def test_file_metadata_providers_not_implemented():
|
|
meta_provider = FileMetadataProvider()
|
|
with pytest.raises(NotImplementedError):
|
|
meta_provider(["/foo/bar.csv"])
|
|
meta_provider = BaseFileMetadataProvider()
|
|
with pytest.raises(NotImplementedError):
|
|
meta_provider(["/foo/bar.csv"], rows_per_file=None, file_sizes=[None])
|
|
with pytest.raises(NotImplementedError):
|
|
meta_provider.expand_paths(["/foo/bar.csv"], None)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fs,data_path,endpoint_url",
|
|
[
|
|
(None, lazy_fixture("local_path"), None),
|
|
(lazy_fixture("local_fs"), lazy_fixture("local_path"), None),
|
|
(lazy_fixture("s3_fs"), lazy_fixture("s3_path"), lazy_fixture("s3_server")),
|
|
(
|
|
lazy_fixture("s3_fs_with_space"),
|
|
lazy_fixture("s3_path_with_space"),
|
|
lazy_fixture("s3_server"),
|
|
), # Path contains space.
|
|
(
|
|
lazy_fixture("s3_fs_with_special_chars"),
|
|
lazy_fixture("s3_path_with_special_chars"),
|
|
lazy_fixture("s3_server"),
|
|
),
|
|
],
|
|
)
|
|
def test_default_file_metadata_provider(
|
|
propagate_logs, caplog, fs, data_path, endpoint_url
|
|
):
|
|
storage_options = (
|
|
{}
|
|
if endpoint_url is None
|
|
else dict(client_kwargs=dict(endpoint_url=endpoint_url))
|
|
)
|
|
|
|
path_module = os.path if urllib.parse.urlparse(data_path).scheme else posixpath
|
|
path1 = path_module.join(data_path, "test1.csv")
|
|
path2 = path_module.join(data_path, "test2.csv")
|
|
paths = [path1, path2]
|
|
paths, fs = _resolve_paths_and_filesystem(paths, fs)
|
|
|
|
df1 = pd.DataFrame({"one": [1, 2, 3], "two": ["a", "b", "c"]})
|
|
df1.to_csv(path1, index=False, storage_options=storage_options)
|
|
df2 = pd.DataFrame({"one": [4, 5, 6], "two": ["e", "f", "g"]})
|
|
df2.to_csv(path2, index=False, storage_options=storage_options)
|
|
|
|
meta_provider = DefaultFileMetadataProvider()
|
|
with caplog.at_level(logging.WARNING), patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_serial",
|
|
wraps=_get_file_infos_serial,
|
|
) as mock_get:
|
|
file_paths, file_sizes = map(list, zip(*meta_provider.expand_paths(paths, fs)))
|
|
mock_get.assert_called_once_with(paths, fs, False)
|
|
# No warning should be logged.
|
|
assert len(caplog.text) == 0
|
|
assert file_paths == paths
|
|
expected_file_sizes = _get_file_sizes_bytes(paths, fs)
|
|
assert file_sizes == expected_file_sizes
|
|
|
|
meta = meta_provider(
|
|
paths,
|
|
rows_per_file=3,
|
|
file_sizes=file_sizes,
|
|
)
|
|
assert meta.size_bytes == sum(expected_file_sizes)
|
|
assert meta.num_rows == 6
|
|
assert len(paths) == 2
|
|
assert all(path in meta.input_files for path in paths)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fs,data_path,endpoint_url",
|
|
[
|
|
(lazy_fixture("local_fs"), lazy_fixture("local_path"), None),
|
|
(lazy_fixture("s3_fs"), lazy_fixture("s3_path"), lazy_fixture("s3_server")),
|
|
],
|
|
)
|
|
def test_default_metadata_provider_ignore_missing(fs, data_path, endpoint_url):
|
|
storage_options = (
|
|
{}
|
|
if endpoint_url is None
|
|
else dict(client_kwargs=dict(endpoint_url=endpoint_url))
|
|
)
|
|
|
|
path1 = os.path.join(data_path, "test1.csv")
|
|
path2 = os.path.join(data_path, "test2.csv")
|
|
paths = [path1, path2]
|
|
paths_with_missing = paths + [os.path.join(data_path, "missing.csv")]
|
|
paths_with_missing, fs = _resolve_paths_and_filesystem(paths_with_missing, fs)
|
|
|
|
df1 = pd.DataFrame({"one": [1, 2, 3], "two": ["a", "b", "c"]})
|
|
df1.to_csv(path1, index=False, storage_options=storage_options)
|
|
df2 = pd.DataFrame({"one": [4, 5, 6], "two": ["e", "f", "g"]})
|
|
df2.to_csv(path2, index=False, storage_options=storage_options)
|
|
|
|
meta_provider = DefaultFileMetadataProvider()
|
|
file_paths, _ = map(
|
|
list,
|
|
zip(
|
|
*meta_provider.expand_paths(
|
|
paths_with_missing, fs, ignore_missing_paths=True
|
|
)
|
|
),
|
|
)
|
|
|
|
assert len(file_paths) == 2
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fs,data_path,endpoint_url",
|
|
[
|
|
(lazy_fixture("local_fs"), lazy_fixture("local_path"), None),
|
|
(lazy_fixture("s3_fs"), lazy_fixture("s3_path"), lazy_fixture("s3_server")),
|
|
],
|
|
)
|
|
def test_default_file_metadata_provider_many_files_basic(
|
|
propagate_logs,
|
|
caplog,
|
|
fs,
|
|
data_path,
|
|
endpoint_url,
|
|
):
|
|
if endpoint_url is None:
|
|
storage_options = {}
|
|
else:
|
|
storage_options = dict(client_kwargs=dict(endpoint_url=endpoint_url))
|
|
|
|
paths = []
|
|
dfs = []
|
|
num_dfs = 4 * FILE_SIZE_FETCH_PARALLELIZATION_THRESHOLD
|
|
for i in range(num_dfs):
|
|
df = pd.DataFrame({"one": list(range(i * 3, (i + 1) * 3))})
|
|
dfs.append(df)
|
|
path = os.path.join(data_path, f"test_{i}.csv")
|
|
if i % 4 == 0:
|
|
# Append same path multiple times to test duplicated paths.
|
|
paths.extend([path, path, path])
|
|
else:
|
|
paths.append(path)
|
|
df.to_csv(path, index=False, storage_options=storage_options)
|
|
paths, fs = _resolve_paths_and_filesystem(paths, fs)
|
|
|
|
meta_provider = DefaultFileMetadataProvider()
|
|
if isinstance(fs, LocalFileSystem):
|
|
patcher = patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_serial",
|
|
wraps=_get_file_infos_serial,
|
|
)
|
|
else:
|
|
patcher = patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_common_path_prefix",
|
|
wraps=_get_file_infos_common_path_prefix,
|
|
)
|
|
with caplog.at_level(logging.WARNING), patcher as mock_get:
|
|
file_paths, file_sizes = map(list, zip(*meta_provider.expand_paths(paths, fs)))
|
|
if isinstance(fs, LocalFileSystem):
|
|
mock_get.assert_called_once_with(paths, fs, False)
|
|
else:
|
|
mock_get.assert_called_once_with(paths, _unwrap_protocol(data_path), fs, False)
|
|
# No warning should be logged.
|
|
assert len(caplog.text) == 0
|
|
assert file_paths == paths
|
|
expected_file_sizes = _get_file_sizes_bytes(paths, fs)
|
|
assert file_sizes == expected_file_sizes
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fs,data_path,endpoint_url",
|
|
[
|
|
(lazy_fixture("local_fs"), lazy_fixture("local_path"), None),
|
|
(lazy_fixture("s3_fs"), lazy_fixture("s3_path"), lazy_fixture("s3_server")),
|
|
],
|
|
)
|
|
def test_default_file_metadata_provider_many_files_partitioned(
|
|
propagate_logs,
|
|
caplog,
|
|
fs,
|
|
data_path,
|
|
endpoint_url,
|
|
write_partitioned_df,
|
|
):
|
|
if endpoint_url is None:
|
|
storage_options = {}
|
|
else:
|
|
storage_options = dict(client_kwargs=dict(endpoint_url=endpoint_url))
|
|
|
|
partition_keys = ["one"]
|
|
partition_path_encoder = PathPartitionEncoder.of(
|
|
base_dir=data_path,
|
|
field_names=partition_keys,
|
|
filesystem=fs,
|
|
)
|
|
paths = []
|
|
dfs = []
|
|
num_dfs = FILE_SIZE_FETCH_PARALLELIZATION_THRESHOLD
|
|
for i in range(num_dfs):
|
|
df = pd.DataFrame(
|
|
{"one": [1, 1, 1, 3, 3, 3], "two": list(range(6 * i, 6 * (i + 1)))}
|
|
)
|
|
df_paths = write_partitioned_df(
|
|
df,
|
|
partition_keys,
|
|
partition_path_encoder,
|
|
partial(df_to_csv, storage_options=storage_options, index=False),
|
|
file_name_suffix=i,
|
|
)
|
|
dfs.append(df)
|
|
paths.extend(df_paths)
|
|
paths, fs = _resolve_paths_and_filesystem(paths, fs)
|
|
partitioning = partition_path_encoder.scheme
|
|
|
|
meta_provider = DefaultFileMetadataProvider()
|
|
if isinstance(fs, LocalFileSystem):
|
|
patcher = patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_serial",
|
|
wraps=_get_file_infos_serial,
|
|
)
|
|
else:
|
|
patcher = patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_common_path_prefix",
|
|
wraps=_get_file_infos_common_path_prefix,
|
|
)
|
|
with caplog.at_level(logging.WARNING), patcher as mock_get:
|
|
file_paths, file_sizes = map(
|
|
list, zip(*meta_provider.expand_paths(paths, fs, partitioning))
|
|
)
|
|
if isinstance(fs, LocalFileSystem):
|
|
mock_get.assert_called_once_with(paths, fs, False)
|
|
else:
|
|
mock_get.assert_called_once_with(
|
|
paths, _unwrap_protocol(partitioning.base_dir), fs, False
|
|
)
|
|
assert len(caplog.text) == 0
|
|
assert file_paths == paths
|
|
expected_file_sizes = _get_file_sizes_bytes(paths, fs)
|
|
assert file_sizes == expected_file_sizes
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fs,data_path,endpoint_url",
|
|
[
|
|
(lazy_fixture("local_fs"), lazy_fixture("local_path"), None),
|
|
(lazy_fixture("s3_fs"), lazy_fixture("s3_path"), lazy_fixture("s3_server")),
|
|
],
|
|
)
|
|
def test_default_file_metadata_provider_many_files_diff_dirs(
|
|
ray_start_regular,
|
|
propagate_logs,
|
|
caplog,
|
|
fs,
|
|
data_path,
|
|
endpoint_url,
|
|
):
|
|
if endpoint_url is None:
|
|
storage_options = {}
|
|
else:
|
|
storage_options = dict(client_kwargs=dict(endpoint_url=endpoint_url))
|
|
|
|
dir1 = os.path.join(data_path, "dir1")
|
|
dir2 = os.path.join(data_path, "dir2")
|
|
if fs is None:
|
|
os.mkdir(dir1)
|
|
os.mkdir(dir2)
|
|
else:
|
|
fs.create_dir(_unwrap_protocol(dir1))
|
|
fs.create_dir(_unwrap_protocol(dir2))
|
|
|
|
paths = []
|
|
dfs = []
|
|
num_dfs = 2 * FILE_SIZE_FETCH_PARALLELIZATION_THRESHOLD
|
|
for i, dir_path in enumerate([dir1, dir2]):
|
|
for j in range(num_dfs * i, num_dfs * (i + 1)):
|
|
df = pd.DataFrame({"one": list(range(3 * j, 3 * (j + 1)))})
|
|
dfs.append(df)
|
|
path = os.path.join(dir_path, f"test_{j}.csv")
|
|
paths.append(path)
|
|
df.to_csv(path, index=False, storage_options=storage_options)
|
|
paths, fs = _resolve_paths_and_filesystem(paths, fs)
|
|
|
|
meta_provider = DefaultFileMetadataProvider()
|
|
if isinstance(fs, LocalFileSystem):
|
|
patcher = patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_serial",
|
|
wraps=_get_file_infos_serial,
|
|
)
|
|
else:
|
|
patcher = patch(
|
|
"ray.data.datasource.file_meta_provider._get_file_infos_parallel",
|
|
wraps=_get_file_infos_parallel,
|
|
)
|
|
with caplog.at_level(logging.WARNING), patcher as mock_get:
|
|
file_paths, file_sizes = map(list, zip(*meta_provider.expand_paths(paths, fs)))
|
|
|
|
mock_get.assert_called_once_with(paths, fs, False)
|
|
if isinstance(fs, LocalFileSystem):
|
|
# No warning should be logged.
|
|
assert len(caplog.text) == 0
|
|
else:
|
|
# Many files with different directories on cloud storage should log warning.
|
|
assert "common parent directory" in caplog.text
|
|
assert file_paths == paths
|
|
expected_file_sizes = _get_file_sizes_bytes(paths, fs)
|
|
assert file_sizes == expected_file_sizes
|
|
|
|
# Many directories should not trigger error.
|
|
if isinstance(fs, LocalFileSystem):
|
|
dir_paths = [dir1, dir2] * num_dfs
|
|
with caplog.at_level(logging.WARNING), patcher as mock_get:
|
|
file_paths, file_sizes = map(
|
|
list, zip(*meta_provider.expand_paths(dir_paths, fs))
|
|
)
|
|
assert len(file_paths) == len(paths) * num_dfs
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|