feat: implement CSV export functionality with customizable columns and formatters
This commit is contained in:
166
tests/test_export_serializers.py
Normal file
166
tests/test_export_serializers.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Iterable
|
||||
|
||||
import pytest
|
||||
|
||||
from services.export_serializers import (
|
||||
CSVExportColumn,
|
||||
CSVExporter,
|
||||
default_formatter,
|
||||
default_project_columns,
|
||||
default_scenario_columns,
|
||||
format_date_iso,
|
||||
format_datetime_utc,
|
||||
format_decimal,
|
||||
stream_projects_to_csv,
|
||||
stream_scenarios_to_csv,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DummyProject:
|
||||
name: str
|
||||
location: str | None = None
|
||||
operation_type: str = "open_pit"
|
||||
description: str | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DummyScenario:
|
||||
project: DummyProject | None
|
||||
name: str
|
||||
status: str = "draft"
|
||||
start_date: date | None = None
|
||||
end_date: date | None = None
|
||||
discount_rate: Decimal | None = None
|
||||
currency: str | None = None
|
||||
primary_resource: str | None = None
|
||||
description: str | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
def collect_csv_bytes(chunks: Iterable[bytes]) -> list[str]:
|
||||
return [chunk.decode("utf-8") for chunk in chunks]
|
||||
|
||||
|
||||
def test_csv_exporter_writes_header_and_rows() -> None:
|
||||
exporter = CSVExporter(
|
||||
[
|
||||
CSVExportColumn("Name", "name"),
|
||||
CSVExportColumn("Location", "location"),
|
||||
]
|
||||
)
|
||||
|
||||
project = DummyProject(name="Alpha", location="Nevada")
|
||||
|
||||
chunks = collect_csv_bytes(exporter.iter_bytes([project]))
|
||||
|
||||
assert chunks[0] == "Name,Location\n"
|
||||
assert chunks[1] == "Alpha,Nevada\n"
|
||||
|
||||
|
||||
def test_csv_exporter_handles_optional_values_and_default_formatter() -> None:
|
||||
exporter = CSVExporter(
|
||||
[
|
||||
CSVExportColumn("Name", "name"),
|
||||
CSVExportColumn("Description", "description"),
|
||||
]
|
||||
)
|
||||
|
||||
project = DummyProject(name="Bravo")
|
||||
|
||||
chunks = collect_csv_bytes(exporter.iter_bytes([project]))
|
||||
|
||||
assert chunks[-1] == "Bravo,\n"
|
||||
|
||||
|
||||
def test_stream_projects_uses_default_columns() -> None:
|
||||
projects = [
|
||||
DummyProject(
|
||||
name="Alpha",
|
||||
location="Nevada",
|
||||
operation_type="open_pit",
|
||||
description="Primary",
|
||||
created_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2025, 1, 2, tzinfo=timezone.utc),
|
||||
)
|
||||
]
|
||||
|
||||
chunks = collect_csv_bytes(stream_projects_to_csv(projects))
|
||||
|
||||
assert chunks[0].startswith("name,location,operation_type")
|
||||
assert any("Alpha" in chunk for chunk in chunks)
|
||||
|
||||
|
||||
def test_stream_scenarios_resolves_project_name_accessor() -> None:
|
||||
project = DummyProject(name="Project X")
|
||||
scenario = DummyScenario(project=project, name="Scenario A")
|
||||
|
||||
chunks = collect_csv_bytes(stream_scenarios_to_csv([scenario]))
|
||||
|
||||
assert "Project X" in chunks[-1]
|
||||
assert "Scenario A" in chunks[-1]
|
||||
|
||||
|
||||
def test_custom_formatter_applies() -> None:
|
||||
def uppercase(value: Any) -> str:
|
||||
return str(value).upper() if value is not None else ""
|
||||
|
||||
exporter = CSVExporter([
|
||||
CSVExportColumn("Name", "name", formatter=uppercase),
|
||||
])
|
||||
|
||||
chunks = collect_csv_bytes(
|
||||
exporter.iter_bytes([DummyProject(name="alpha")]))
|
||||
|
||||
assert chunks[-1] == "ALPHA\n"
|
||||
|
||||
|
||||
def test_default_formatter_handles_multiple_types() -> None:
|
||||
assert default_formatter(None) == ""
|
||||
assert default_formatter(True) == "true"
|
||||
assert default_formatter(False) == "false"
|
||||
assert default_formatter(Decimal("1.234")) == "1.23"
|
||||
assert default_formatter(
|
||||
datetime(2025, 1, 1, tzinfo=timezone.utc)).endswith("Z")
|
||||
assert default_formatter(date(2025, 1, 1)) == "2025-01-01"
|
||||
|
||||
|
||||
def test_format_helpers() -> None:
|
||||
assert format_date_iso(date(2025, 5, 1)) == "2025-05-01"
|
||||
assert format_date_iso("not-a-date") == ""
|
||||
|
||||
ts = datetime(2025, 5, 1, 12, 0, tzinfo=timezone.utc)
|
||||
assert format_datetime_utc(ts) == "2025-05-01T12:00:00Z"
|
||||
|
||||
assert format_datetime_utc("nope") == ""
|
||||
assert format_decimal(None) == ""
|
||||
assert format_decimal(Decimal("12.345")) == "12.35"
|
||||
assert format_decimal(10) == "10.00"
|
||||
|
||||
|
||||
def test_default_project_columns_includes_required_fields() -> None:
|
||||
columns = default_project_columns()
|
||||
headers = [column.header for column in columns]
|
||||
assert headers[:3] == ["name", "location", "operation_type"]
|
||||
|
||||
|
||||
def test_default_scenario_columns_handles_missing_project() -> None:
|
||||
scenario = DummyScenario(project=None, name="Orphan Scenario")
|
||||
|
||||
exporter = CSVExporter(default_scenario_columns())
|
||||
chunks = collect_csv_bytes(exporter.iter_bytes([scenario]))
|
||||
|
||||
assert chunks[-1].startswith(",Orphan Scenario")
|
||||
|
||||
|
||||
def test_csv_exporter_requires_columns() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
CSVExporter([])
|
||||
Reference in New Issue
Block a user