Skip to content

Commit

Permalink
added general LowerColorado Test case for API
Browse files Browse the repository at this point in the history
  • Loading branch information
taddyb committed Oct 9, 2024
1 parent 7e5dc45 commit 4fc70fa
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 56 deletions.
3 changes: 3 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ services:
- type: bind
source: ${CORE_VOLUME_SOURCE}
target: ${CORE_VOLUME_TARGET}
- type: bind
source: ./test
target: /t-route/test
command: sh -c ". /t-route/.venv/bin/activate && uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"
healthcheck:
test: curl --fail -I http://localhost:${PORT}/health || exit 1
Expand Down
56 changes: 46 additions & 10 deletions doc/api/api_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ T-Route is used in many contexts for hydrological river routing:
- Scientific Python
- Replace and Route (RnR)

In the latest PR for RnR, there is a requirement to run T-Route as a service. This service requires an easy way to dynamically create config files, restart flow from Initial Conditions, and run T-Route. To satisfy this requirement, a FastAPI endpoint was created in `/src/app` along with code to dynamically create t-route endpoints.
In the latest PR for RnR (https://github.com/NOAA-OWP/hydrovis/pull/865), there is an requirement to run T-Route as a service. This service requires an easy way to dynamically create config files, restart flow from Initial Conditions, and run T-Route. To satisfy this requirement, a FastAPI endpoint was created in `/src/app` along with code to dynamically create t-route endpoints.

## Why use shared volumes?

Expand All @@ -27,7 +27,7 @@ Since T-Route is running in a docker container, there has to be a connection bet
## Quickstart
1. From the Root directory, run:
```shell
docker compose up
docker compose --env-file ./compose.env up
```

This will start the T-Route container and run the API on localhost:8004. To view the API spec and swagger docs, visit localhost:8004/docs
Expand Down Expand Up @@ -68,21 +68,57 @@ and an internal 500 error if there is something wrong.

## Building and pushing to a container registry

the `build.sh` script is created to simplify the process of pushing the T-route image to a docker container registry. Below is the URL for the pushed container:
To ensure Replace and Route is using the correct version of T-Route, it is recommended a docker container be built, and then pushed to a registry (Dockerhub, GitHub Container Registry, etc). To do this manually for the GitHub container registry, the following commands should be used within a terminal.

```shell
ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG}
docker login --username ${GH_USERNAME} --password ${GH_TOKEN} ghcr.io
```
To run that script, the following ENV variables have to be set:
- ${GH_USERNAME}
- This command will log the user into the GitHub container registry using their credentials

```shell
docker build -t ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG} -f Dockerfile.troute_api
```
- This command builds the T-Route API container using a defined version `${TAG}`

```shell
docker push ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG}
```
- This commands pushes the built T-Route API container to the NOAA-OWP/t-route container registry


The following env variables are used:
- `${GH_USERNAME}`
- your github username
- ${GH_TOKEN}
- `${GH_TOKEN}`
- your github access token
- ${TAG}
- `${TAG} `
- the version tag
- ex: 0.0.2

If you want to build this off a forked version, change the container registry to your user accounts container registry.
If you want to build this off a forked version, change the container registry (`/NOAA-OWP/t-route/`) to your user accounts container registry.

## Testing:

To test this container, follow the steps within `test/api/README.md`
### Testing the RnR Extension:
The following folder contains data files that are to be used to test the T-Route FastAPI code within src/app

To use these files, follow the steps below:

1. Copy the `./test/api/test_compose.yaml` file in the base project dir (`./`)
2. Run `docker compose -f test_compose.yaml up`
3. visit `localhost:8000/docs` in your browser
4. Enter the following parameters into the `/api/v1/flow_routing/v4` endpoint
- lid=CAGM7
- feature_id=2930769
- hy_id=1074884
- initial_start=0
- start_time=2024-08-24T00:00:00
- num_forecast_days=5
5. Click execute
6. A Status 201 code means the run ran, and test/api/data/troute_output will be populated in the `{lid}/` folder

### Testing the LowerColorado test cases:
1. Run the compose.yaml file from the base dir using: `docker compose --env-file ./compose.env up --build`
2. visit `localhost:8000/docs` in your browser
3. Execute the `/api/v1/flow_routing/v4/tests/LowerColorado` endpoint using the default parameter file path
4. A Status 201 code means the run ran, and the defined yaml output will be populated
73 changes: 66 additions & 7 deletions src/app/api/routes/troute_v4.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"""Author: Tadd Bindas"""
import json
from datetime import datetime
from pathlib import Path
from typing import Annotated

from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
import yaml
from fastapi import APIRouter, Depends, HTTPException
from nwm_routing import main_v04 as t_route
from pydantic import conint
from troute.config import Config

from app.api.services.initialization import (create_initial_start_file,
create_params, edit_yaml)
from app.core.settings import Settings
from app.api.services.utils import update_test_paths_with_prefix
from app.core import get_settings
from app.schemas import TRouteStatus
from app.core.settings import Settings
from app.schemas import HttpStatusCode, TestStatus, TRouteStatus

router = APIRouter()

Expand Down Expand Up @@ -57,15 +60,71 @@ async def get_gauge_data(
try:
t_route(["-f", yaml_file_path.__str__()])
except Exception as e:
JSONResponse(
raise HTTPException(
status_code=500,
content={"message": e},
detail=str(e),
)

yaml_file_path.unlink()

return TRouteStatus(
status_code=HttpStatusCode.CREATED,
message="T-Route run successfully",
lid=lid,
feature_id=feature_id,
)


@router.post("/tests/LowerColorado", response_model=TestStatus)
async def run_lower_colorado_tests(
settings: Annotated[Settings, Depends(get_settings)],
config_path: str = "test/LowerColorado_TX_v4/test_AnA_V4_HYFeature.yaml",
) -> TRouteStatus:
"""An API call for running the LowerColorado T-Route test using a predefined config file
Parameters:
----------
config_path: str
Path to the YAML configuration file for the test
Returns:
--------
TRouteStatus
The status of the T-Route run
"""
base = "/t-route"
path_to_test_dir = Path(f"{base}/{config_path}").parent
yaml_path = Path(f"{base}/{config_path}")

with open(yaml_path) as custom_file:
data = yaml.load(custom_file, Loader=yaml.SafeLoader)

# Updating paths to work in docker
data = update_test_paths_with_prefix(data, path_to_test_dir, settings.lower_colorado_paths_to_update)

troute_configuration = Config.with_strict_mode(**data)

tmp_yaml = path_to_test_dir / "tmp.yaml"

dict_ = json.loads(troute_configuration.json())

# converting timeslice back to string (Weird pydantic 1.10 workaround)
dict_["compute_parameters"]["restart_parameters"]["start_datetime"] = data["compute_parameters"]["restart_parameters"]["start_datetime"]

with open(tmp_yaml, 'w') as file:
yaml.dump(dict_, file)

try:
t_route(["-f", tmp_yaml.__str__()])
return TestStatus(
status_code=HttpStatusCode.CREATED,
message="T-Route run successfully using defined configuration",
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e),
)
finally:
if tmp_yaml.exists():
tmp_yaml.unlink()
33 changes: 33 additions & 0 deletions src/app/api/services/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pathlib import Path
from typing import Dict, List


def update_test_paths_with_prefix(data: Dict[str, str], prefix: Path, paths_to_update: List[List[str]]) -> Dict[str, str]:
"""Update specific paths inside of a config dictionary with the given prefix, if they exist.
Parameters:
-----------
data: Dict[str, str]
The data dictionary read from the yaml config
prefix: Path
The path prefix we want to append
paths_to_update: List[str]
The list of paths to update from the config
Returns:
--------
Dict[str, str]
The updated data dictionary
"""

for keys in paths_to_update:
current = data
for key in keys[:-1]:
if key in current:
current = current[key]
else:
break
if keys[-1] in current:
current[keys[-1]] = (prefix / current[keys[-1]]).__str__()

return data
1 change: 1 addition & 0 deletions src/app/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from app.core.settings import Settings


def get_settings() -> Settings:
"""Instantiating the Settings object for FastAPI
Expand Down
36 changes: 28 additions & 8 deletions src/app/core/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Author: Tadd Bindas"""

from pathlib import Path
from typing import List

from pydantic import BaseSettings

Expand All @@ -9,7 +8,7 @@ class Settings(BaseSettings):
"""
Configuration settings for the application.
This class uses Pydantic's BaseSettings to manage configuration,
This class uses Pydantic's BaseSettings to manage any variables,
allowing for easy integration with environment variables and
configuration files.
Expand All @@ -29,11 +28,8 @@ class Settings(BaseSettings):
The regex string for finding restart files
geofile_path: str
The path to the docker folders where the geopackage is located in the shared volume
Notes
-----
The configuration is initially read from a 'config.ini' file and can be
overridden by environment variables.
paths_to_update: str
The entries in a config that have relative paths. Used for changig in services/utils.py
"""

api_v1_str: str = "/api/v1"
Expand All @@ -43,3 +39,27 @@ class Settings(BaseSettings):
restart_path: str = "/t-route/data/troute_restart/{}/"
restart_file: str = "HYDRO_RST_{}_DOMAIN1"
geofile_path: str = "/t-route/data/rfc_geopackage_data/{}/downstream.gpkg"
lower_colorado_paths_to_update: List[List[str]] = [
["network_topology_parameters", "supernetwork_parameters", "geo_file_path"],
["compute_parameters", "hybrid_parameters", "diffusive_domain"],
["compute_parameters", "hybrid_parameters", "topobathy_domain"],
["compute_parameters", "hybrid_parameters", "coastal_boundary_domain"],
["compute_parameters", "forcing_parameters", "qlat_input_folder"],
["compute_parameters", "data_assimilation_parameters", "usace_timeslices_folder"],
["compute_parameters", "data_assimilation_parameters", "usgs_timeslices_folder"],
["compute_parameters", "data_assimilation_parameters", "canada_timeslices_folder"],
["compute_parameters", "data_assimilation_parameters", "LakeOntario_outflow"],
["compute_parameters", "data_assimilation_parameters", "reservoir_da", "reservoir_rfc_da", "reservoir_rfc_forecasts_time_series_path"]
]
lower_colorado_paths_to_update: List[List[str]] = [
["network_topology_parameters", "supernetwork_parameters", "geo_file_path"],
["compute_parameters", "hybrid_parameters", "diffusive_domain"],
["compute_parameters", "hybrid_parameters", "topobathy_domain"],
["compute_parameters", "hybrid_parameters", "coastal_boundary_domain"],
["compute_parameters", "forcing_parameters", "qlat_input_folder"],
["compute_parameters", "data_assimilation_parameters", "usace_timeslices_folder"],
["compute_parameters", "data_assimilation_parameters", "usgs_timeslices_folder"],
["compute_parameters", "data_assimilation_parameters", "canada_timeslices_folder"],
["compute_parameters", "data_assimilation_parameters", "LakeOntario_outflow"],
["compute_parameters", "data_assimilation_parameters", "reservoir_da", "reservoir_rfc_da", "reservoir_rfc_forecasts_time_series_path"]
]
40 changes: 33 additions & 7 deletions src/app/schemas.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
"""Author: Tadd Bindas"""
from enum import IntEnum

from pydantic import BaseModel


class HttpStatusCode(IntEnum):
OK = 200
CREATED = 201
ACCEPTED = 202
BAD_REQUEST = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
INTERNAL_SERVER_ERROR = 500


class TRouteStatus(BaseModel):
"""
A schema to define successful t-route output
"""A schema to define successful t-route output
Attributes
----------
Attributes:
-----------
status_code: HttpStatusCode
The HTTP status code output from the code run
message: str
The
The output message from T-Route
lid : str
The location ID belonging to the point being routed
feature_id : str
The COMID, or hf_id, belonging to the point being routed
"""

status_code: HttpStatusCode
message: str
lid: str
feature_id: str


class TestStatus(BaseModel):
"""A schema for output from t-route test cases
Attributes:
-----------
status_code: HttpStatusCode
The HTTP status code output from the code run
message: str
The output message from T-Route
"""
status_code: HttpStatusCode
message: str
22 changes: 0 additions & 22 deletions test/api/README.md

This file was deleted.

3 changes: 1 addition & 2 deletions test/api/test_compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,4 @@ services:
interval: 30s
timeout: 5s
retries: 3
start_period: 5s

start_period: 5s

0 comments on commit 4fc70fa

Please sign in to comment.