generated from NOAA-OWP/owp-open-source-project-template
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
31 changed files
with
1,889 additions
and
1,375 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# Dockerfile.Notebook | ||
|
||
This document describes the Docker setup for running JupyterLab with mounted volumes for development and analysis. | ||
|
||
## Container Overview | ||
|
||
The container provides a JupyterLab environment with: | ||
- Python environment for data analysis | ||
- Web interface accessible via port 8000 | ||
|
||
This container is a great way to run examples and integrated tests | ||
|
||
## Docker Configuration | ||
|
||
### Dockerfile | ||
The Dockerfile sets up: | ||
- Base Python environment | ||
- JupyterLab installation | ||
- Volume mount points for data and code | ||
- Port 8000 exposed for web interface | ||
- Working directory configuration | ||
|
||
### Getting Started | ||
|
||
Build: | ||
```bash | ||
docker build -t troute-notebook -f docker/Dockerfile.notebook . | ||
``` | ||
|
||
Run: | ||
```bash | ||
docker run -p 8000:8000 troute-notebook | ||
``` | ||
|
||
Then, take the URL from the output and put that into your browser. An example one is below: | ||
``` | ||
http://127.0.0.1:8000/lab?token=<token> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
FROM rockylinux:9.2 as rocky-base | ||
RUN yum install -y epel-release | ||
RUN yum install -y netcdf netcdf-fortran netcdf-fortran-devel netcdf-openmpi | ||
|
||
RUN yum install -y git cmake python python-devel pip | ||
|
||
WORKDIR "/t-route/" | ||
|
||
COPY . /t-route/ | ||
|
||
RUN ln -s /usr/lib64/gfortran/modules/netcdf.mod /usr/include/openmpi-x86_64/netcdf.mod | ||
|
||
ENV VIRTUAL_ENV=/opt/venv | ||
RUN python3 -m venv $VIRTUAL_ENV | ||
|
||
# Equivalent to source /opt/venv/bin/activate | ||
ENV PATH="$VIRTUAL_ENV/bin:$PATH" | ||
|
||
RUN python -m pip install . | ||
RUN python -m pip install .[jupyter] | ||
RUN python -m pip install .[test] | ||
|
||
RUN ./compiler.sh no-e | ||
|
||
EXPOSE 8000 | ||
|
||
# increase max open files soft limit | ||
RUN ulimit -n 10000 | ||
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8000", "--no-browser", "--allow-root"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
[project] | ||
name = "troute_project" | ||
authors = [ | ||
{name = "DongHa Kim", email = "dongha.kim@noaa.gov"}, | ||
{name = "Sean Horvath", email = "sean.horvath@noaa.gov"}, | ||
{name = "Amin Torabi", email = "amin.torabi@noaa.gov"}, | ||
{name = "Zach Jurgen", email = "jurgen.zach@noaa.gov"}, | ||
{name = "Austin Raney", email = "austin.raney@noaa.gov"}, | ||
] | ||
dynamic = ["version", "dependencies"] | ||
|
||
[tool.setuptools.dynamic] | ||
dependencies = {file = ["requirements.txt"]} | ||
|
||
[project.optional-dependencies] | ||
test = [ | ||
"pytest==8.3.2", | ||
"bmipy==2.0.0", | ||
] | ||
|
||
jupyter = [ | ||
"contextily==1.6.0", | ||
"matplotlib>=3.7.0,<3.8.0", # More stable version range | ||
"ipykernel>=6.29.0,<7.0.0", | ||
"jupyterlab>=3.6.7,<4.0.0", | ||
"xarray>=2024.1.1", | ||
"matplotlib-inline>=0.1.6" # Add explicit version | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import os | ||
|
||
from contextlib import contextmanager | ||
from pathlib import Path | ||
|
||
|
||
@contextmanager | ||
def temporarily_change_dir(path: Path): | ||
"""Temporarily changes the current working directory | ||
This context manager changes the current working directory to the specified path, | ||
yields control back to the caller, and then changes back to the original directory | ||
when exiting the context | ||
Parameters | ||
---------- | ||
path : Path | ||
The path to temporarily change the current working directory to | ||
Yields | ||
------ | ||
None | ||
""" | ||
original_cwd = Path.cwd() | ||
if original_cwd != path: | ||
os.chdir(path) | ||
try: | ||
yield | ||
finally: | ||
if original_cwd != path: | ||
os.chdir(original_cwd) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from pathlib import Path | ||
from typing import Any, Dict, List, Tuple | ||
|
||
import pytest | ||
import yaml | ||
from _pytest.fixtures import FixtureRequest | ||
|
||
|
||
def find_config_files() -> List[Path]: | ||
"""Finds all `.yaml` configuration files within specified directories | ||
Returns | ||
------- | ||
List[Path] | ||
A list of Path objects pointing to each valid configuration | ||
""" | ||
test_dir = Path(__file__).parents[3] / "test" # Searching for the t-route/test dir | ||
target_dirs = ["LowerColorado_TX", "LowerColorado_TX_v4", "LowerColorado_TX_HYFeatures_v22", "unit_test_hyfeature"] | ||
files = [] | ||
for dir_name in target_dirs: | ||
files.extend(list((test_dir / dir_name).glob("*.yaml"))) | ||
return files | ||
|
||
|
||
@pytest.fixture(params=find_config_files()) | ||
def config_data(request: FixtureRequest) -> Tuple[Path, Dict[str, Any]]: | ||
"""A fixture for loading yaml files into python dictionary mappings | ||
Parameters | ||
---------- | ||
request : FixtureRequest | ||
The pytest request object, containing the current parameter value | ||
Returns | ||
------- | ||
Tuple[Path, Dict[str, Any]] | ||
A tuple containing the path to the YAML file and the loaded data as a dictionary | ||
""" | ||
data = yaml.load(request.param.read_text(), Loader=yaml.Loader) | ||
return request.param, data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,98 @@ | ||
import pytest | ||
|
||
import yaml | ||
from pathlib import Path | ||
from typing import List | ||
from test import temporarily_change_dir | ||
from typing import Any, Dict, Tuple | ||
|
||
import pytest | ||
from pydantic import ValidationError | ||
from troute.config import Config | ||
|
||
TEST_DIR = Path(__file__).parent | ||
ROOT_TEST_DIR = TEST_DIR / "../../../test" | ||
|
||
def test_config_validation(config_data: Tuple[Path, Dict[str, Any]]) -> None: | ||
"""Validates all config files contained within the `test/` folder | ||
Parameters | ||
---------- | ||
config_data : Tuple[Path, Dict[str, Any]] | ||
A tuple containing the path to the config file and the parsed config data | ||
- The first element is a Path object pointing to the config file | ||
- The second element is a dictionary containing the parsed config yaml file data. | ||
Raises | ||
------ | ||
pytest.fail | ||
If a ValidationError occurs during Config creation, this function will | ||
call pytest.fail with a detailed error message showing the config file that fails | ||
Notes | ||
----- | ||
This test function uses the `temporarily_change_dir` context manager to | ||
change the working directory before attempting to create the Config object | ||
""" | ||
path, data = config_data | ||
with temporarily_change_dir(path.parent): | ||
try: | ||
Config(**data) | ||
except ValidationError as e: | ||
error_details = "\n".join( | ||
f"{' -> '.join(map(str, err['loc']))}: {err['msg']}" | ||
for err in e.errors() | ||
) | ||
pytest.fail(f"Validation failed for {path}:\n{error_details}") | ||
|
||
|
||
def test_strict_config_validation(config_data: Tuple[Path, Dict[str, Any]]) -> None: | ||
"""Validates all config files contained within the `test/` folder via strict handling | ||
Parameters | ||
---------- | ||
config_data : Tuple[Path, Dict[str, Any]] | ||
A tuple containing the path to the config file and the parsed config data | ||
- The first element is a Path object pointing to the config file | ||
- The second element is a dictionary containing the parsed config yaml file data. | ||
Raises | ||
------ | ||
pytest.fail | ||
If a ValidationError occurs during Config creation, this function will | ||
call pytest.fail with a detailed error message showing the config file that fails | ||
def config_files() -> List[Path]: | ||
files = list(ROOT_TEST_DIR.glob("*/*.yaml")) | ||
return files | ||
Notes | ||
----- | ||
- This test function uses the `temporarily_change_dir` context manager to | ||
change the working directory before attempting to create the Config object | ||
- If this code runs into a "value_error.path.not_exists" error, this is either because: | ||
1. there is a relative path in the config | ||
2. the file doesn't exist | ||
Thus, we will make the relative path absolute and retry the validation. If that fails, we | ||
know the file does not existt | ||
""" | ||
path, data = config_data | ||
parent_path = path.parent | ||
try: | ||
with temporarily_change_dir(path.parent): | ||
Config.with_strict_mode(**data) | ||
except ValidationError as e: | ||
for error in e.errors(): | ||
if error["type"] == "value_error.path.not_exists": | ||
keys = error["loc"] | ||
invalid_path = error["ctx"]["path"] | ||
corrected_path = Path(parent_path, invalid_path).__str__() | ||
|
||
# Ensuring the code path exists before changing relative path to absolute | ||
if Path(corrected_path).exists(): | ||
current = data | ||
for key in keys[:-1]: | ||
current = current.setdefault(key, {}) | ||
current[keys[-1]] = corrected_path | ||
else: | ||
pytest.fail(f"Path does not exist: {corrected_path}") | ||
|
||
@pytest.mark.parametrize("file", config_files()) | ||
def test_naive_deserialization(file: Path): | ||
data = yaml.load(file.read_text(), Loader=yaml.Loader) | ||
Config(**data) | ||
try: | ||
with temporarily_change_dir(path.parent): | ||
Config.with_strict_mode(**data) | ||
except ValidationError as e: | ||
error_details = "\n".join( | ||
f"{' -> '.join(map(str, err['loc']))}: {err['msg']}" | ||
for err in e.errors() | ||
) | ||
pytest.fail(f"Validation failed for {path}:\n{error_details}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import os | ||
|
||
from contextlib import contextmanager | ||
from pathlib import Path | ||
|
||
|
||
@contextmanager | ||
def temporarily_change_dir(path: Path): | ||
"""Temporarily changes the current working directory | ||
This context manager changes the current working directory to the specified path, | ||
yields control back to the caller, and then changes back to the original directory | ||
when exiting the context | ||
Parameters | ||
---------- | ||
path : Path | ||
The path to temporarily change the current working directory to | ||
Yields | ||
------ | ||
None | ||
""" | ||
original_cwd = Path.cwd() | ||
if original_cwd != path: | ||
os.chdir(path) | ||
try: | ||
yield | ||
finally: | ||
if original_cwd != path: | ||
os.chdir(original_cwd) |
Oops, something went wrong.