251 lines
7.8 KiB
Python
251 lines
7.8 KiB
Python
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
from sqlalchemy import Connection
|
|
|
|
from enervision_ml.data import (
|
|
OUTPUT_COLUMNS,
|
|
load_from_csv,
|
|
load_from_database,
|
|
load_recent_from_database,
|
|
)
|
|
from tests.conftest import ANCRAGE, insere_dataset, insere_lecture, insere_lectures, insere_site
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def test_load_from_database_returns_the_nine_contract_columns(connexion_ml: Connection) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lectures(connexion_ml, site_id, heures=3, fin=ANCRAGE)
|
|
|
|
frame = load_from_database(connexion_ml)
|
|
|
|
assert list(frame.columns) == OUTPUT_COLUMNS
|
|
|
|
|
|
def test_load_from_database_joins_the_site_attributes_to_every_reading(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml, site_type="factory", capacity_kw=250.0)
|
|
insere_lectures(connexion_ml, site_id, heures=3, fin=ANCRAGE)
|
|
|
|
frame = load_from_database(connexion_ml)
|
|
|
|
mien = frame[frame["site_id"] == site_id]
|
|
assert len(mien) == 3
|
|
assert set(mien["site_type"]) == {"factory"}
|
|
assert set(mien["capacity_kw"]) == {250.0}
|
|
|
|
|
|
def test_load_from_database_deduplicates_two_sources_at_the_same_instant(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
# `uq_reading_source` autorise deux lignes au meme (site_id, timestamp) des que `source`
|
|
# differe : le garde-fou vit dans `mock_api_import.py`, pas dans le schema. Le chargeur ML
|
|
# doit donc imposer lui-meme "une ligne par (site_id, timestamp)", pas la supposer.
|
|
site_id = insere_site(connexion_ml)
|
|
dataset_id = insere_dataset(connexion_ml)
|
|
insere_lecture(
|
|
connexion_ml, site_id, instant=ANCRAGE, consumption_kwh=10.0, source="api_history"
|
|
)
|
|
insere_lecture(
|
|
connexion_ml,
|
|
site_id,
|
|
instant=ANCRAGE,
|
|
consumption_kwh=99.0,
|
|
source="csv",
|
|
dataset_id=dataset_id,
|
|
)
|
|
|
|
frame = load_from_database(connexion_ml)
|
|
|
|
mien = frame[frame["site_id"] == site_id]
|
|
assert len(mien) == 1
|
|
assert mien["consumption_kwh"].iloc[0] == 99.0
|
|
|
|
|
|
def test_load_recent_from_database_prefers_csv_when_two_sources_share_an_instant(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
dataset_id = insere_dataset(connexion_ml)
|
|
insere_lecture(
|
|
connexion_ml, site_id, instant=ANCRAGE, consumption_kwh=10.0, source="api_history"
|
|
)
|
|
insere_lecture(
|
|
connexion_ml,
|
|
site_id,
|
|
instant=ANCRAGE,
|
|
consumption_kwh=99.0,
|
|
source="csv",
|
|
dataset_id=dataset_id,
|
|
)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
|
|
)
|
|
|
|
assert len(frame) == 1
|
|
assert frame["consumption_kwh"].iloc[0] == 99.0
|
|
|
|
|
|
def test_load_recent_from_database_excludes_readings_before_the_since_bound(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lectures(connexion_ml, site_id, heures=5, fin=ANCRAGE)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE - timedelta(hours=2), until=ANCRAGE
|
|
)
|
|
|
|
assert list(frame["timestamp"]) == [
|
|
ANCRAGE - timedelta(hours=2),
|
|
ANCRAGE - timedelta(hours=1),
|
|
ANCRAGE,
|
|
]
|
|
|
|
|
|
def test_load_recent_from_database_includes_a_reading_exactly_at_the_since_bound(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lecture(connexion_ml, site_id, instant=ANCRAGE)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
|
|
)
|
|
|
|
assert len(frame) == 1
|
|
|
|
|
|
def test_load_recent_from_database_keeps_timestamps_timezone_aware(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lecture(connexion_ml, site_id, instant=ANCRAGE)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
|
|
)
|
|
|
|
assert frame["timestamp"].dt.tz is not None
|
|
|
|
|
|
def test_load_recent_from_database_orders_readings_by_site_then_timestamp(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
for decalage in (2, 0, 1):
|
|
insere_lecture(connexion_ml, site_id, instant=ANCRAGE + timedelta(hours=decalage))
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
|
|
)
|
|
|
|
assert list(frame["timestamp"]) == [
|
|
ANCRAGE,
|
|
ANCRAGE + timedelta(hours=1),
|
|
ANCRAGE + timedelta(hours=2),
|
|
]
|
|
|
|
|
|
def test_load_recent_from_database_returns_the_contract_columns_even_without_any_row(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE + timedelta(days=365), until=ANCRAGE + timedelta(days=400)
|
|
)
|
|
|
|
assert frame.empty
|
|
assert list(frame.columns) == OUTPUT_COLUMNS
|
|
|
|
|
|
def test_load_recent_from_database_types_a_fully_null_capacity_kw_as_float64(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml, capacity_kw=None)
|
|
insere_lectures(connexion_ml, site_id, heures=3, fin=ANCRAGE)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE - timedelta(hours=2), until=ANCRAGE
|
|
)
|
|
|
|
assert frame["capacity_kw"].dtype == "float64"
|
|
assert frame["capacity_kw"].isna().all()
|
|
|
|
|
|
def test_load_recent_from_database_types_a_null_is_working_hours_as_float64(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lecture(connexion_ml, site_id, instant=ANCRAGE, is_working_hours=None)
|
|
insere_lecture(
|
|
connexion_ml, site_id, instant=ANCRAGE + timedelta(hours=1), is_working_hours=True
|
|
)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
|
|
)
|
|
|
|
assert frame["is_working_hours"].dtype == "float64"
|
|
assert list(frame["is_working_hours"].isna()) == [True, False]
|
|
|
|
|
|
def test_load_recent_from_database_types_is_working_hours_as_float64_even_without_a_null(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
# Sans cette garantie, le dtype dependrait du contenu de la fenetre lue : `bool` ici, `float64`
|
|
# des qu'une seule lecture est a NULL, et le schema des deux chargeurs cesserait d'etre egal.
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lectures(connexion_ml, site_id, heures=2, fin=ANCRAGE)
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE - timedelta(hours=2), until=ANCRAGE
|
|
)
|
|
|
|
assert frame["is_working_hours"].dtype == "float64"
|
|
|
|
|
|
def test_both_loaders_produce_the_same_columns_in_the_same_order(
|
|
connexion_ml: Connection, tmp_path: Path
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lectures(connexion_ml, site_id, heures=2, fin=ANCRAGE)
|
|
csv_path = tmp_path / "lectures.csv"
|
|
pd.DataFrame(
|
|
{
|
|
"site_id": [site_id],
|
|
"timestamp": [ANCRAGE],
|
|
"consumption_kwh": [50.0],
|
|
"temperature_celsius": [15.0],
|
|
"humidity_percent": [50.0],
|
|
"solar_irradiance_wm2": [0.0],
|
|
"is_working_hours": [True],
|
|
"site_type": ["office"],
|
|
}
|
|
).to_csv(csv_path, index=False)
|
|
|
|
depuis_la_base = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE - timedelta(hours=1), until=ANCRAGE
|
|
)
|
|
depuis_le_csv = load_from_csv(csv_path)
|
|
|
|
assert list(depuis_la_base.columns) == list(depuis_le_csv.columns)
|
|
assert depuis_la_base.dtypes.to_dict() == depuis_le_csv.dtypes.to_dict()
|
|
|
|
|
|
def test_load_recent_from_database_excludes_readings_after_the_until_bound(
|
|
connexion_ml: Connection,
|
|
) -> None:
|
|
site_id = insere_site(connexion_ml)
|
|
insere_lectures(connexion_ml, site_id, heures=5, fin=ANCRAGE + timedelta(hours=4))
|
|
|
|
frame = load_recent_from_database(
|
|
connexion_ml, since=ANCRAGE - timedelta(days=1), until=ANCRAGE
|
|
)
|
|
|
|
assert list(frame["timestamp"]) == [ANCRAGE]
|