Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fc04af3
add interpolation flag to model config
sameersk2k Jul 7, 2026
935fd95
add spatial module to handle grid lookup
sameersk2k Jul 7, 2026
03c64cd
remove large pkl files and replace them with npz
sameersk2k Jul 7, 2026
7b5acad
modify lookup to support npz
sameersk2k Jul 7, 2026
062b63b
lookup registration in wdc on startup & adf mods
sameersk2k Jul 7, 2026
0f59489
modify config manager to return data instead of file path
sameersk2k Jul 7, 2026
dcdf5f3
deprecate legacy router and controllers
sameersk2k Jul 7, 2026
68feac9
add wind processing func in util & modify height val for interpolation
sameersk2k Jul 7, 2026
3e31ef7
remove package download from dockerfile and update requirements.txt
sameersk2k Jul 7, 2026
0d5117a
remove the package downloading from dockerfile
sameersk2k Jul 7, 2026
10c4383
make format
sameersk2k Jul 7, 2026
f2b6e36
modify tests to work without package approach
sameersk2k Jul 7, 2026
cd944e3
hub-heights: change interpolation field to boolean
RLiNREL Jul 20, 2026
14268f5
hub-height: add textfield, simplify banner, add notes
RLiNREL Jul 20, 2026
5e7d7ac
Loss-assumption: . -> : text adjustment for consistency
RLiNREL Jul 20, 2026
291c5e5
make-fmt
RLiNREL Jul 20, 2026
d274fe9
api: update api description, delete legacy controllers, imports
RLiNREL Jul 21, 2026
c431b68
api: update backend doc
RLiNREL Jul 21, 2026
684527b
api-test: update controller tests
RLiNREL Jul 21, 2026
f09cede
make format
RLiNREL Jul 21, 2026
e7b8b76
docs: add CHANGELOG, migration doc
RLiNREL Jul 21, 2026
f29eca4
api-doc: update to full api path
RLiNREL Jul 21, 2026
1be74bd
undo deprecating of legacy router/controllers
sameersk2k Jul 22, 2026
67159c2
fix legacy controllers
sameersk2k Jul 24, 2026
76782ea
add neutral power law as default vertical interpolation
sameersk2k Jul 24, 2026
0df8cb2
add spatial manager and global spatial manager
sameersk2k Jul 24, 2026
3e9e9dd
remove n_nearest from athena fetcher and make grid points use spatial…
sameersk2k Jul 24, 2026
bc3788d
add context manager to npz file
sameersk2k Jul 27, 2026
cb559cf
add thread locking to _cache_df
sameersk2k Jul 27, 2026
e8b0392
add circuit breaker to athena
sameersk2k Jul 27, 2026
e13fec9
add flag to stop reinitializing spatial on every init_spatial
sameersk2k Jul 27, 2026
bfa5bba
Merge pull request #290 from NatLabRockies/chore/sunset-legacy-api-289
RLiNREL Jul 27, 2026
d3f1ad9
Merge pull request #288 from NatLabRockies/feat/custom-height-ui-281
RLiNREL Jul 27, 2026
407fc00
patch: update old nrel links to nlr
RLiNREL Jul 30, 2026
bb04df1
fix/initialized flag not set to True
sameersk2k Jul 30, 2026
df20e38
decouple athena query logic from athena data fetcher
sameersk2k Jul 30, 2026
6da9cb4
merge develop into feat/migrate-windwatts-data-to-backend
sameersk2k Jul 30, 2026
93e922b
fix GridPoints test
sameersk2k Jul 31, 2026
1a69efe
Merge pull request #286 from NatLabRockies/feat/migrate-windwatts-dat…
RLiNREL Jul 31, 2026
2753aed
CORS-CSP: clean nrel traces from config
RLiNREL Jul 31, 2026
f29a6e8
fix: allow _ AWS source key to - key; update sample config
RLiNREL Aug 3, 2026
a21d8e9
patch: move longer _ALT_ATHENA_TABLE_NAME before _ATHENA_TABLE_NAME, …
RLiNREL Aug 3, 2026
f209445
make format
RLiNREL Aug 3, 2026
d71dbbb
Merge pull request #298 from NatLabRockies/hot-fix/config-key-update
RLiNREL Aug 3, 2026
7edc1af
Merge pull request #296 from NatLabRockies/chore/clean-nrel-traces-287
RLiNREL Aug 3, 2026
b940538
power-curve: NLR 20kW ref
RLiNREL Aug 3, 2026
624f729
20kw-pc: update range to 30-50m according to table 3
RLiNREL Aug 3, 2026
986f406
Merge pull request #300 from NatLabRockies/feat/15-20kW-NLR-ref-299
RLiNREL Aug 3, 2026
8dec3c3
fix: return month as int in monthly windspeed response
sameersk2k Aug 4, 2026
7d224a0
make format
sameersk2k Aug 4, 2026
64aa214
Merge pull request #301 from NatLabRockies/hot-fix/monthly-windspeed-…
RLiNREL Aug 4, 2026
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Changelog

All notable changes to this project will be documented in this file.

## [2.0.0] - 2026-07-20

### Breaking Changes

- **Removed legacy API endpoints** (`/wtk/*` and `/era5/*`). These routes were deprecated in v1 and have now been removed.
- Migrate to the unified `/v1/{model}/` endpoints. See the [Migration Guide](docs/06-migration.md).

### Changed

- API version bumped from `1.0.0` to `2.0.0`.
- Removed orphaned controller files: `wtk_data_controller.py`, `era5_data_controller.py`.

---

## [1.0.0] - Initial release

- Introduced unified `/v1/{model}/` API endpoints.
- Legacy model-specific routes (`/wtk/*`, `/era5/*`) marked deprecated.
- Supported models: `era5-quantiles`, `era5-timeseries`, `wtk-timeseries`, `ensemble-quantiles`.
12 changes: 12 additions & 0 deletions docs/03-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,15 @@ pytest
## API Documentation

When the app is running, visit `/docs` (e.g., `http://localhost:8080/docs`) to see the auto-generated Swagger UI.

### API Endpoints

All endpoints are under the `/api/v1/{model}/` prefix.

| Endpoint | Description |
|---|---|
| `GET /api/v1/{model}/windspeed` | Wind speed data |
| `GET /api/v1/{model}/production` | Energy production estimates |
| `GET /api/v1/{model}/timeseries` | Raw timeseries downloads |

**Supported models**: `era5-quantiles`, `era5-timeseries`, `wtk-timeseries`, `ensemble-quantiles`
34 changes: 34 additions & 0 deletions docs/06-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# API Migration Guide: Legacy --> v1

The legacy model-specific endpoints (`/wtk/*`, `/era5/*`) were removed in API v2.0.0. All functionality is available through the unified v1 API.

For full endpoint details and parameters, see the **interactive API docs** at `/api/docs` when the app is running.

## Route Structure

```
Legacy: /api/wtk/<endpoint> → /api/v1/wtk-timeseries/<endpoint>
Legacy: /api/era5/<endpoint> → /api/v1/era5-quantiles/<endpoint>
```

## Endpoint Mapping

| Legacy | v1 Equivalent |
|---|---|
| `GET /api/wtk/windspeed` | `GET /api/v1/wtk-timeseries/windspeed` |
| `GET /api/wtk/energy-production` | `GET /api/v1/wtk-timeseries/production` |
| `GET /api/wtk/download-csv` | `GET /api/v1/wtk-timeseries/timeseries` |
| `POST /api/wtk/download-csv-batch` | `POST /api/v1/wtk-timeseries/timeseries/batch` |
| `GET /api/wtk/nearest-locations` | `GET /api/v1/wtk-timeseries/grid-points` |
| `GET /api/wtk/available-powercurves` | `GET /api/v1/turbines` |
| `GET /api/era5/windspeed` | `GET /api/v1/era5-quantiles/windspeed` |
| `GET /api/era5/production` | `GET /api/v1/era5-quantiles/production` |
| `GET /api/era5/timeseries` | `GET /api/v1/era5-timeseries/timeseries` |
| `POST /api/era5/timeseries/batch` | `POST /api/v1/era5-timeseries/timeseries/batch` |
| `GET /api/era5/grid-points` | `GET /api/v1/era5-quantiles/grid-points` |
| `GET /api/era5/powercurves` | `GET /api/v1/turbines` |

## Notable Changes

- **Period** — path-based period (e.g. `/windspeed/{avg_type}`) is now a query parameter: `?period=`.
- **Turbine** — the `powercurve` query parameter is deprecated and the renamed and recommended query parameter is `turbine`.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Welcome to the WindWatts documentation.
3. [Backend Guide](03-backend.md) - API development.
4. [Frontend Guide](04-frontend.md) - UI development.
5. [Deployment](05-deployment.md) - Production deployment.
6. [Migration Guide](06-migration.md) - Migrating from legacy API endpoints to v1.

## Contributing

Expand Down
9 changes: 0 additions & 9 deletions windwatts-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,6 @@ RUN apt-get update && apt-get upgrade -y && apt-get install -y \
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Download and install windwatts_data.whl from s3
ARG WINDWATTS_DATA_URL
ARG WINDWATTS_DATA_VERSION=1.0.4
ARG WINDWATTS_DATA_FILE=windwatts_data-${WINDWATTS_DATA_VERSION}-py3-none-any.whl
ENV WINDWATTS_DATA_URL=${WINDWATTS_DATA_URL}
RUN curl -o /tmp/windwatts_data-${WINDWATTS_DATA_VERSION}-py3-none-any.whl ${WINDWATTS_DATA_URL}${WINDWATTS_DATA_FILE} && \
pip install --no-cache-dir /tmp/${WINDWATTS_DATA_FILE} && \
rm /tmp/${WINDWATTS_DATA_FILE}

# Copy the FastAPI application
COPY . .

Expand Down
10 changes: 9 additions & 1 deletion windwatts-api/app/config/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
"schema": "quantile_yearly",
"years": {"full": list(range(2013, 2024)), "sample": [2020, 2021, 2022, 2023]},
"heights": {"windspeed": [30, 40, 50, 60, 80, 100], "winddirection": []},
"interpolation": True,
"grid": "era5",
"grid_info": {
"min_lat": 23.402,
"min_long": -137.725,
Expand All @@ -96,6 +98,8 @@
"windspeed": [40, 60, 80, 100, 120, 140, 160, 200],
"winddirection": [],
},
"interpolation": True,
"grid": "wtk",
"grid_info": {
"min_lat": 7.75129,
"min_long": -179.99918,
Expand All @@ -104,7 +108,7 @@
"spatial_resolution": "2 km",
"temporal_resolution": "1 hour",
},
"links": ["https://www.nrel.gov/grid/wind-toolkit"],
"links": ["https://www.nlr.gov/grid/wind-toolkit"],
"references": [
"Draxl, C., B.M. Hodge, A. Clifton, and J. McCaa. 2015. Overview and Meteorological Validation of the Wind Integration National Dataset Toolkit (Technical Report, NREL/TP-5000-61740). Golden, CO: National Laboratory of the Rockies",
'Draxl, C., B.M. Hodge, A. Clifton, and J. McCaa. 2015. "The Wind Integration National Dataset (WIND) Toolkit." Applied Energy 151: 355366',
Expand All @@ -116,6 +120,8 @@
"schema": "quantile_atemporal",
"years": {"full": list(range(2013, 2024)), "sample": []},
"heights": {"windspeed": [30, 40, 50, 60, 80, 100], "winddirection": []},
"interpolation": True,
"grid": "era5",
"grid_info": {
"min_lat": 23.402,
"min_long": -137.725,
Expand All @@ -137,6 +143,8 @@
"windspeed": [10, 30, 40, 50, 60, 80, 100],
"winddirection": [10, 100],
},
"interpolation": False,
"grid": "era5",
"grid_info": {
"min_lat": 23.402,
"min_long": -137.725,
Expand Down
29 changes: 23 additions & 6 deletions windwatts-api/app/config/sample_windwatts_data_config.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
{
"region_name": "us-east-1",
"bucket_name": "sample-bucket",
"database": "sample_database",
"output_location": "s3://sample-output-location/",
"output_bucket": "sample-output-bucket",
"athena_table_name": "sample_table",
"alt_athena_table_name": "sample_alt_table",
"athena_workgroup": "sample_workgroup"
}
"database": "sample_database",
"athena_workgroup": "sample_workgroup",
"sources": {
"wtk-timeseries": {
"bucket_name": "sample-bucket",
"athena_table_name": "sample_table",
"alt_athena_table_name": "sample_alt_table",
"capabilities": { "avg_types": ["all", "annual", "monthly", "hourly"] }
},
"era5-quantiles": {
"bucket_name": "sample-bucket",
"athena_table_name": "sample_table",
"alt_athena_table_name": "sample_alt_table",
"capabilities": { "avg_types": ["all", "annual"] }
},
"ensemble-quantiles": {
"bucket_name": "sample-bucket",
"athena_table_name": "sample_table",
"alt_athena_table_name": "sample_alt_table",
"capabilities": { "avg_types": ["all"] }
}
}
}
39 changes: 14 additions & 25 deletions windwatts-api/app/config_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import json
import boto3
import tempfile


class ConfigManager:
Expand All @@ -21,45 +20,34 @@ def __init__(
self.local_config_path = local_config_path
self.client = boto3.client("secretsmanager", region_name=region_name)

def get_config(self) -> str:
def get_config(self) -> dict:
"""
Retrieve the secret from AWS Secrets Manager, environment variables, or the local configuration file.
:return: The path to the configuration file.
:return: The dict with config keys.
"""
# Try to retrieve the secret from AWS Secrets Manager
if self.secret_arn:
try:
response = self.client.get_secret_value(SecretId=self.secret_arn)
secret = response["SecretString"]
config_data = json.loads(secret)

# Save the secret to a temporary file
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".json")
temp_file.close()
with open(temp_file.name, "w") as f:
json.dump(config_data, f)
return temp_file.name
return json.loads(response["SecretString"])
except self.client.exceptions.ClientError as e:
print(f"Unable to retrieve secret: {e}")

# Try to retrieve config from environment variables
env_config = self._get_config_from_env()
if env_config:
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".json")
temp_file.close()
with open(temp_file.name, "w") as f:
json.dump(env_config, f)
print("Config loaded from environment variables.")
return temp_file.name
return env_config

# Fallback: return the path to the local configuration file
if self.local_config_path and os.path.exists(self.local_config_path):
print("Local configuration file found.")
return self.local_config_path
else:
raise FileNotFoundError(
"Local configuration file not found and unable to retrieve secret from AWS Secrets Manager or environment variables."
)
with open(self.local_config_path, "r") as f:
print(f"Loaded config from local path '{self.local_config_path}'")
return json.load(f)

raise FileNotFoundError(
"Local configuration file not found and unable to retrieve secret from AWS Secrets Manager or environment variables."
)

def _get_config_from_env(self):
"""
Expand All @@ -76,19 +64,20 @@ def _get_config_from_env(self):
# Scan for all SOURCES_<SOURCE>_FIELD_NAME env vars
sources = {}
prefix = "SOURCES_"
suffixes = ["_BUCKET_NAME", "_ATHENA_TABLE_NAME", "_ALT_ATHENA_TABLE_NAME"]
suffixes = ["_ALT_ATHENA_TABLE_NAME", "_ATHENA_TABLE_NAME", "_BUCKET_NAME"]
env = os.environ
source_fields = {}
for key, value in env.items():
if key.startswith(prefix):
rest = key[len(prefix) :]
for suffix in suffixes:
if rest.endswith(suffix):
source = rest[: -len(suffix)].lower()
source = rest[: -len(suffix)].lower().replace("_", "-")
field = suffix[1:].lower() # e.g. 'bucket_name'
if source not in source_fields:
source_fields[source] = {}
source_fields[source][field] = value
break
# Package the sources with required fields into `sources`
for source, fields in source_fields.items():
if "bucket_name" in fields and "athena_table_name" in fields:
Expand Down
Loading