189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
import json
|
|
import os
|
|
from unittest import mock
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from mlflow.data.dataset_source_registry import get_dataset_source_from_json, resolve_dataset_source
|
|
from mlflow.data.http_dataset_source import HTTPDatasetSource
|
|
from mlflow.exceptions import MlflowException
|
|
from mlflow.utils.os import is_windows
|
|
from mlflow.utils.rest_utils import cloud_storage_http_request
|
|
|
|
|
|
def test_source_to_and_from_json():
|
|
url = "http://mywebsite.com/path/to/my/dataset.txt"
|
|
source = HTTPDatasetSource(url)
|
|
assert source.to_json() == json.dumps({"url": url})
|
|
|
|
reloaded_source = get_dataset_source_from_json(
|
|
source.to_json(), source_type=source._get_source_type()
|
|
)
|
|
assert isinstance(reloaded_source, HTTPDatasetSource)
|
|
assert type(source) == type(reloaded_source)
|
|
assert source.url == reloaded_source.url == url
|
|
|
|
|
|
def test_http_dataset_source_is_registered_and_resolvable():
|
|
source1 = resolve_dataset_source(
|
|
"http://mywebsite.com/path/to/my/dataset.txt", candidate_sources=[HTTPDatasetSource]
|
|
)
|
|
assert isinstance(source1, HTTPDatasetSource)
|
|
assert source1.url == "http://mywebsite.com/path/to/my/dataset.txt"
|
|
|
|
source2 = resolve_dataset_source(
|
|
"https://otherwebsite.net", candidate_sources=[HTTPDatasetSource]
|
|
)
|
|
assert isinstance(source2, HTTPDatasetSource)
|
|
assert source2.url == "https://otherwebsite.net"
|
|
|
|
with pytest.raises(MlflowException, match="Could not find a source information resolver"):
|
|
resolve_dataset_source("s3://mybucket", candidate_sources=[HTTPDatasetSource])
|
|
|
|
with pytest.raises(MlflowException, match="Could not find a source information resolver"):
|
|
resolve_dataset_source("otherscheme://mybucket", candidate_sources=[HTTPDatasetSource])
|
|
|
|
with pytest.raises(MlflowException, match="Could not find a source information resolver"):
|
|
resolve_dataset_source("htp://mybucket", candidate_sources=[HTTPDatasetSource])
|
|
|
|
|
|
def test_source_load(tmp_path):
|
|
source1 = HTTPDatasetSource(
|
|
"https://raw.githubusercontent.com/mlflow/mlflow/master/tests/datasets/winequality-red.csv"
|
|
)
|
|
|
|
loaded1 = source1.load()
|
|
parsed1 = pd.read_csv(loaded1, sep=";")
|
|
# Verify that the expected data was downloaded by checking for an expected column and asserting
|
|
# that several rows are present
|
|
assert "fixed acidity" in parsed1.columns
|
|
assert len(parsed1) > 10
|
|
|
|
loaded2 = source1.load(dst_path=tmp_path)
|
|
assert loaded2 == str(tmp_path / "winequality-red.csv")
|
|
parsed2 = pd.read_csv(loaded2, sep=";")
|
|
# Verify that the expected data was downloaded by checking for an expected column and asserting
|
|
# that several rows are present
|
|
assert "fixed acidity" in parsed2.columns
|
|
assert len(parsed1) > 10
|
|
|
|
source2 = HTTPDatasetSource(
|
|
"https://raw.githubusercontent.com/mlflow/mlflow/master/tests/datasets/winequality-red.csv#foo?query=param"
|
|
)
|
|
loaded3 = source2.load(dst_path=tmp_path)
|
|
assert loaded3 == str(tmp_path / "winequality-red.csv")
|
|
parsed3 = pd.read_csv(loaded3, sep=";")
|
|
assert "fixed acidity" in parsed3.columns
|
|
assert len(parsed1) > 10
|
|
|
|
source3 = HTTPDatasetSource("https://github.com/")
|
|
loaded4 = source3.load()
|
|
assert os.path.exists(loaded4)
|
|
assert os.path.basename(loaded4) == "dataset_source"
|
|
|
|
source4 = HTTPDatasetSource("https://github.com")
|
|
loaded5 = source4.load()
|
|
assert os.path.exists(loaded5)
|
|
assert os.path.basename(loaded5) == "dataset_source"
|
|
|
|
def cloud_storage_http_request_with_fast_fail(*args, **kwargs):
|
|
kwargs["max_retries"] = 1
|
|
kwargs["timeout"] = 5
|
|
return cloud_storage_http_request(*args, **kwargs)
|
|
|
|
source5 = HTTPDatasetSource("https://nonexistentwebsitebuiltbythemlflowteam112312.com")
|
|
with (
|
|
mock.patch(
|
|
"mlflow.data.http_dataset_source.cloud_storage_http_request",
|
|
side_effect=cloud_storage_http_request_with_fast_fail,
|
|
),
|
|
pytest.raises(Exception, match="Max retries exceeded with url"),
|
|
):
|
|
source5.load()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attachment_filename", "expected_filename"),
|
|
[
|
|
("testfile.txt", "testfile.txt"),
|
|
('"testfile.txt"', "testfile.txt"),
|
|
("'testfile.txt'", "testfile.txt"),
|
|
(None, "winequality-red.csv"),
|
|
],
|
|
)
|
|
def test_source_load_with_content_disposition_header(attachment_filename, expected_filename):
|
|
def download_with_mock_content_disposition_headers(*args, **kwargs):
|
|
response = cloud_storage_http_request(*args, **kwargs)
|
|
if attachment_filename is not None:
|
|
response.headers["Content-Disposition"] = f"attachment; filename={attachment_filename}"
|
|
else:
|
|
response.headers["Content-Disposition"] = "attachment"
|
|
return response
|
|
|
|
with mock.patch(
|
|
"mlflow.data.http_dataset_source.cloud_storage_http_request",
|
|
side_effect=download_with_mock_content_disposition_headers,
|
|
):
|
|
source = HTTPDatasetSource(
|
|
"https://raw.githubusercontent.com/mlflow/mlflow/master/tests/datasets/winequality-red.csv"
|
|
)
|
|
source.load()
|
|
loaded = source.load()
|
|
assert os.path.exists(loaded)
|
|
assert os.path.basename(loaded) == expected_filename
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"filename",
|
|
[
|
|
"/foo/bar.txt",
|
|
"./foo/bar.txt",
|
|
"../foo/bar.txt",
|
|
"foo/bar.txt",
|
|
],
|
|
)
|
|
def test_source_load_with_content_disposition_header_invalid_filename(filename):
|
|
def download_with_mock_content_disposition_headers(*args, **kwargs):
|
|
response = cloud_storage_http_request(*args, **kwargs)
|
|
response.headers["Content-Disposition"] = f"attachment; filename={filename}"
|
|
return response
|
|
|
|
with mock.patch(
|
|
"mlflow.data.http_dataset_source.cloud_storage_http_request",
|
|
side_effect=download_with_mock_content_disposition_headers,
|
|
):
|
|
source = HTTPDatasetSource(
|
|
"https://raw.githubusercontent.com/mlflow/mlflow/master/tests/datasets/winequality-red.csv"
|
|
)
|
|
|
|
with pytest.raises(MlflowException, match="Invalid filename in Content-Disposition header"):
|
|
source.load()
|
|
|
|
|
|
@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
|
|
@pytest.mark.parametrize(
|
|
"filename",
|
|
[
|
|
r"..\..\poc.txt",
|
|
r"Users\User\poc.txt",
|
|
],
|
|
)
|
|
def test_source_load_with_content_disposition_header_invalid_filename_windows(filename):
|
|
def download_with_mock_content_disposition_headers(*args, **kwargs):
|
|
response = cloud_storage_http_request(*args, **kwargs)
|
|
response.headers = {"Content-Disposition": f"attachment; filename={filename}"}
|
|
return response
|
|
|
|
with mock.patch(
|
|
"mlflow.data.http_dataset_source.cloud_storage_http_request",
|
|
side_effect=download_with_mock_content_disposition_headers,
|
|
):
|
|
source = HTTPDatasetSource(
|
|
"https://raw.githubusercontent.com/mlflow/mlflow/master/tests/datasets/winequality-red.csv"
|
|
)
|
|
|
|
# Expect an MlflowException for invalid filenames
|
|
with pytest.raises(MlflowException, match="Invalid filename in Content-Disposition header"):
|
|
source.load()
|