feat: consolidate legacy workflows into gyxx-flow
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gyxx_flow.core.layout import DataLayout
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNTIME_ROOT = PROJECT_ROOT / "src" / "gyxx_flow" / "modules"
|
||||
|
||||
|
||||
def _load_path_module(module: str, filename: str, monkeypatch, data_root: Path):
|
||||
monkeypatch.setenv("GYXX_DATA_ROOT", str(data_root))
|
||||
runtime_root = RUNTIME_ROOT / module / "runtime"
|
||||
monkeypatch.setenv("GYXX_MODULE_ROOT", str(runtime_root))
|
||||
path = runtime_root / filename
|
||||
name = f"_gyxx_test_paths_{module}"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
loaded = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = loaded
|
||||
try:
|
||||
spec.loader.exec_module(loaded)
|
||||
finally:
|
||||
sys.modules.pop(name, None)
|
||||
return loaded
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("module", "filename", "names"),
|
||||
[
|
||||
(
|
||||
"content_marketing",
|
||||
"runtime_paths.py",
|
||||
("raw_root", "normalized_root", "curated_root", "exports_root"),
|
||||
),
|
||||
(
|
||||
"product_commerce",
|
||||
"runtime_paths.py",
|
||||
(
|
||||
"RAW_DATA_ROOT",
|
||||
"NORMALIZED_DATA_ROOT",
|
||||
"CURATED_DATA_ROOT",
|
||||
"EXPORTS_DATA_ROOT",
|
||||
),
|
||||
),
|
||||
(
|
||||
"shop_intelligence",
|
||||
"paths.py",
|
||||
("RAW_ROOT", "NORMALIZED_ROOT", "CURATED_ROOT", "EXPORTS_ROOT"),
|
||||
),
|
||||
(
|
||||
"supply_chain",
|
||||
"paths.py",
|
||||
("SHARED_ROOT", "NORMALIZED_ROOT", "CURATED_ROOT", "BACKUP_ROOT"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_all_module_path_boundaries_use_the_canonical_data_tree(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
module: str,
|
||||
filename: str,
|
||||
names: tuple[str, str, str, str],
|
||||
) -> None:
|
||||
data_root = tmp_path / "relocatable-data"
|
||||
loaded = _load_path_module(module, filename, monkeypatch, data_root)
|
||||
|
||||
layers = ("raw", "normalized", "curated", "exports")
|
||||
for name, layer in zip(names, layers, strict=True):
|
||||
assert getattr(loaded, name) == data_root / "data" / layer / module
|
||||
assert not data_root.exists(), "path resolution must not mutate the filesystem"
|
||||
|
||||
|
||||
def test_module_data_paths_cover_collected_and_exported_file_types(tmp_path: Path) -> None:
|
||||
paths = DataLayout(tmp_path).for_module("content_marketing")
|
||||
|
||||
assert paths.raw_path("api", "response.json") == (
|
||||
tmp_path / "data" / "raw" / "content_marketing" / "api" / "response.json"
|
||||
)
|
||||
assert paths.raw_path("downloads", "source.xlsx") == (
|
||||
tmp_path / "data" / "raw" / "content_marketing" / "downloads" / "source.xlsx"
|
||||
)
|
||||
assert paths.raw_path("downloads", "source.xls") == (
|
||||
tmp_path / "data" / "raw" / "content_marketing" / "downloads" / "source.xls"
|
||||
)
|
||||
assert paths.raw_path("downloads", "source.csv") == (
|
||||
tmp_path / "data" / "raw" / "content_marketing" / "downloads" / "source.csv"
|
||||
)
|
||||
assert paths.export_path("reports", "weekly.md") == (
|
||||
tmp_path / "data" / "exports" / "content_marketing" / "reports" / "weekly.md"
|
||||
)
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unsafe", ["", "..", "a/b", "a\\b", "C:\\data"])
|
||||
def test_module_data_paths_reject_unsafe_module_names(tmp_path: Path, unsafe: str) -> None:
|
||||
with pytest.raises(ValueError, match="path segment"):
|
||||
DataLayout(tmp_path).for_module(unsafe)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unsafe", [("..", "escape.json"), ("C:\\", "escape.json")])
|
||||
def test_module_data_paths_reject_paths_outside_the_layer(
|
||||
tmp_path: Path, unsafe: tuple[str, str]
|
||||
) -> None:
|
||||
with pytest.raises(ValueError, match="escapes module data root"):
|
||||
DataLayout(tmp_path).for_module("product_commerce").raw_path(*unsafe)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configured", ["", " "])
|
||||
@pytest.mark.parametrize(
|
||||
("module", "filename", "root_name"),
|
||||
[
|
||||
("content_marketing", "runtime_paths.py", "data_root"),
|
||||
("product_commerce", "runtime_paths.py", "DATA_HOME"),
|
||||
("shop_intelligence", "paths.py", "DATA_ROOT"),
|
||||
("supply_chain", "paths.py", "DATA_ROOT"),
|
||||
],
|
||||
)
|
||||
def test_blank_data_root_uses_the_project_var_default(
|
||||
monkeypatch,
|
||||
configured: str,
|
||||
module: str,
|
||||
filename: str,
|
||||
root_name: str,
|
||||
) -> None:
|
||||
monkeypatch.setenv("GYXX_DATA_ROOT", configured)
|
||||
runtime_root = RUNTIME_ROOT / module / "runtime"
|
||||
monkeypatch.setenv("GYXX_MODULE_ROOT", str(runtime_root))
|
||||
path = runtime_root / filename
|
||||
name = f"_gyxx_test_blank_paths_{module}"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
loaded = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = loaded
|
||||
try:
|
||||
spec.loader.exec_module(loaded)
|
||||
finally:
|
||||
sys.modules.pop(name, None)
|
||||
|
||||
assert getattr(loaded, root_name) == PROJECT_ROOT / "var"
|
||||
Reference in New Issue
Block a user