Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/adapt/contracts/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
returning a dataset to the context.
"""

import contextlib

import numpy as np
import xarray as xr

Expand Down Expand Up @@ -41,10 +43,8 @@ def assert_time_normalized(ds: xr.Dataset) -> None:
tv = raw.flat[0] if isinstance(raw, np.ndarray) and raw.ndim > 0 else raw
# unwrap numpy scalar wrapper if needed
if hasattr(tv, "item"):
try:
with contextlib.suppress(Exception):
tv = tv.item()
except Exception:
pass
module = getattr(type(tv), "__module__", "")
require(
not module.startswith("cftime"),
Expand Down
Empty file added src/adapt/utils/__init__.py
Empty file.
33 changes: 33 additions & 0 deletions src/adapt/utils/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Time normalization helpers shared across ADAPT modules."""

import contextlib
from datetime import UTC, datetime

import numpy as np


def normalize_time_scalar(time_val):
"""Normalize xarray/cftime/numpy time representations to a scalar."""
tv = time_val
while isinstance(tv, np.ndarray) and tv.size == 1:
tv = tv.reshape(-1)[0]
if isinstance(tv, np.ndarray):
tv = tv.reshape(-1)[0]

if hasattr(tv, "item"):
with contextlib.suppress(TypeError, ValueError):
tv = tv.item()

if getattr(type(tv), "__module__", "").startswith("cftime"):
tv = datetime(
int(tv.year),
int(tv.month),
int(tv.day),
int(tv.hour),
int(tv.minute),
int(tv.second),
int(getattr(tv, "microsecond", 0) or 0),
tzinfo=UTC,
)

return tv
Empty file added tests/__init__.py
Empty file.
49 changes: 49 additions & 0 deletions tests/cli/test_config_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os
import shutil
from argparse import Namespace
from pathlib import Path

from adapt.cli import _config_cmd


def test_adapt_config_handles_deleted_cwd(tmp_path, monkeypatch):
# Create and chdir into a temp directory, then delete it to simulate stale cwd.
cwd = tmp_path / "gone"
cwd.mkdir()
os.chdir(cwd)
shutil.rmtree(cwd)

home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))

# No output arg: must fail loudly (cannot resolve ./config.yaml).
args = Namespace(output=None)
try:
_config_cmd(args)
except FileNotFoundError as e:
assert "Current working directory no longer exists" in str(e)
else:
raise AssertionError(
"Expected FileNotFoundError when cwd is missing and no output is provided"
)

# Absolute output path should still work even when cwd is missing.
os.chdir(home)
out = Path(home) / "config.yaml"
args2 = Namespace(output=str(out))
_config_cmd(args2)
assert out.exists()
text = out.read_text()
assert f'base_dir: "{str(home)}"' in text


def test_adapt_config_sets_base_dir_to_output_parent(tmp_path):
out_dir = tmp_path / "nested"
out_path = out_dir / "my_config.yaml"
args = Namespace(output=str(out_path))
_config_cmd(args)

assert out_path.exists()
text = out_path.read_text()
assert f'base_dir: "{str(out_dir)}"' in text
Empty file added tests/configuration/__init__.py
Empty file.
115 changes: 115 additions & 0 deletions tests/configuration/test_cli_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Tests for CLIConfig schema and conversion to internal overrides."""

from adapt.configuration.schemas.cli import CLIConfig


def test_cli_to_internal_overrides_with_mode():
"""Test CLI config conversion with mode override."""
cli = CLIConfig(mode="historical")
overrides = cli.to_internal_overrides()
assert overrides["mode"] == "historical"


def test_cli_to_internal_overrides_with_realtime_mode():
"""Test CLI config conversion with realtime mode."""
cli = CLIConfig(mode="realtime")
overrides = cli.to_internal_overrides()
assert overrides["mode"] == "realtime"


def test_cli_to_internal_overrides_with_radar_id():
"""Test CLI config conversion with radar_id override."""
cli = CLIConfig(radar="KMOB")
overrides = cli.to_internal_overrides()
assert overrides["downloader"]["radar"] == "KMOB"


def test_cli_to_internal_overrides_with_log_level():
"""Test CLI config conversion with log_level override."""
cli = CLIConfig(log_level="DEBUG")
overrides = cli.to_internal_overrides()
assert overrides["logging"]["level"] == "DEBUG"


def test_cli_to_internal_overrides_with_multiple_fields():
"""Test CLI config conversion with multiple overrides."""
cli = CLIConfig(mode="historical", radar="KHTX", log_level="INFO")
overrides = cli.to_internal_overrides()
assert overrides["mode"] == "historical"
assert overrides["downloader"]["radar"] == "KHTX"
assert overrides["logging"]["level"] == "INFO"


def test_cli_to_internal_overrides_empty():
"""Test CLI config conversion with no overrides."""
cli = CLIConfig()
overrides = cli.to_internal_overrides()
assert overrides == {}


def test_cli_config_accepts_base_dir():
"""Test that base_dir is accepted and in overrides."""
cli = CLIConfig(base_dir="/path/to/output")
assert cli.base_dir == "/path/to/output"
overrides = cli.to_internal_overrides()
assert overrides["base_dir"] == "/path/to/output"

def test_cli_config_all_log_levels():
"""Test all valid log levels."""
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
cli = CLIConfig(log_level=level)
overrides = cli.to_internal_overrides()
assert overrides["logging"]["level"] == level


def test_cli_infers_historical_mode_from_start_time():
"""CLI automatically sets mode=historical if start_time provided without explicit mode."""
cli = CLIConfig(start_time="2024-01-01T00:00:00Z")
assert cli.mode == "historical"
overrides = cli.to_internal_overrides()
assert overrides["mode"] == "historical"


def test_cli_infers_historical_mode_from_end_time():
"""CLI automatically sets mode=historical if end_time provided without explicit mode."""
cli = CLIConfig(end_time="2024-01-01T23:59:59Z")
assert cli.mode == "historical"
overrides = cli.to_internal_overrides()
assert overrides["mode"] == "historical"


def test_cli_infers_historical_mode_from_both_times():
"""CLI automatically sets mode=historical if both times provided without explicit mode."""
cli = CLIConfig(
start_time="2024-01-01T00:00:00Z",
end_time="2024-01-01T23:59:59Z"
)
assert cli.mode == "historical"
overrides = cli.to_internal_overrides()
assert overrides["mode"] == "historical"
assert overrides["downloader"]["start_time"] == "2024-01-01T00:00:00Z"
assert overrides["downloader"]["end_time"] == "2024-01-01T23:59:59Z"


def test_cli_explicit_mode_overrides_time_inference():
"""Explicit mode in CLI is not overridden by time inference."""
cli = CLIConfig(
mode="realtime",
start_time="2024-01-01T00:00:00Z"
)
# Explicit mode should be respected
assert cli.mode == "realtime"


def test_cli_time_fields_in_overrides():
"""Test that start_time and end_time are included in overrides."""
cli = CLIConfig(
start_time="2024-01-01T00:00:00Z",
end_time="2024-01-01T23:59:59Z",
radar="KMOB"
)
overrides = cli.to_internal_overrides()
assert overrides["downloader"]["start_time"] == "2024-01-01T00:00:00Z"
assert overrides["downloader"]["end_time"] == "2024-01-01T23:59:59Z"
assert overrides["downloader"]["radar"] == "KMOB"

78 changes: 78 additions & 0 deletions tests/configuration/test_cli_precedence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from adapt.configuration.schemas.cli import CLIConfig
from adapt.configuration.schemas.param import ParamConfig
from adapt.configuration.schemas.resolve import resolve_config
from adapt.configuration.schemas.user import UserConfig


def test_cli_overrides_do_not_mutate_user():
user = UserConfig.model_validate({"RADAR_ID": "KABC", "MODE": "realtime", "BASE_DIR": "/tmp"})

cli = CLIConfig.model_validate({"radar": "KHTX"})

internal = resolve_config(ParamConfig(), user, cli)

# CLI should take precedence
assert internal.downloader.radar == "KHTX"

# But the original user model should remain unchanged
assert user.radar == "KABC"


def test_cli_minimal_overrides_radar_id():
"""CLI radar_id override should work correctly."""
user = UserConfig(base_dir="/tmp", radar="KABC")
cli = CLIConfig(radar="KHTX")

config = resolve_config(ParamConfig(), user, cli)

assert config.downloader.radar == "KHTX" # CLI wins
assert config.base_dir == "/tmp" # User value preserved


def test_cli_minimal_overrides_mode():
"""CLI mode override should work correctly."""
user = UserConfig(
base_dir="/tmp",
radar="KABC",
mode="realtime",
start_time="2024-01-01T00:00:00Z",
end_time="2024-01-01T12:00:00Z"
)
cli = CLIConfig(mode="historical")

config = resolve_config(ParamConfig(), user, cli)

assert config.mode == "historical" # CLI wins
assert config.downloader.radar == "KABC" # User value preserved
# Historical mode validation should pass since start/end times provided


def test_cli_precedence_no_user_config():
"""CLI should work even without UserConfig."""
cli = CLIConfig(radar="KHTX", mode="realtime")

# This will need minimal UserConfig for required fields
user = UserConfig(base_dir="/tmp")
config = resolve_config(ParamConfig(), user, cli)

assert config.downloader.radar == "KHTX"
assert config.mode == "realtime"


def test_cli_only_overrides_specified_fields():
"""CLI should only override fields that are explicitly set."""
user = UserConfig(
base_dir="/tmp",
radar="KABC",
mode="realtime",
threshold=35
)

# CLI only sets radar_id
cli = CLIConfig(radar="KHTX")

config = resolve_config(ParamConfig(), user, cli)

assert config.downloader.radar == "KHTX" # CLI override
assert config.mode == "realtime" # User value preserved
assert config.segmenter.threshold == 35.0 # User value preserved
Loading
Loading