Skip to content
Open
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
17 changes: 8 additions & 9 deletions src/india_api/internal/config/env.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Config struct for application running."""
import os
from distutils.util import strtobool
try:
from distutils.util import strtobool
except ImportError:
from setuptools._distutils.util import strtobool
from typing import get_type_hints

import structlog
Expand All @@ -22,19 +25,13 @@ def __init__(self) -> None:
If the class field is upper case, parse it into the indicated
type from the environment. Required fields are those set in
the child class without a default value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed those example lines because they are outdated or no longer accurately reflect how the class is used, they might end up confusing readers. Additionally, if the functionality of the EnvParser is already there so , including those examples just becomes unnecessary.

Examples:
>>> MyEnv(EnvParser):
>>> REQUIRED_ENV_VAR: str
>>> OPTIONAL_ENV_VAR: str = "default value"
>>> ignored_var: str = "ignored"
"""
for field, t in get_type_hints(self).items():
# Skip item if not upper case
if not field.isupper():
continue

# Log Error if required field not supplied
# Log error if required field not supplied
default_value = getattr(self, field, None)
match (default_value, os.environ.get(field)):
case (None, None):
Expand All @@ -46,7 +43,7 @@ def __init__(self) -> None:
case (_, _):
# Field is in env
env_value: str | bool = os.environ[field]
# Handle bools seperately as bool("False") == True
# Handle bools separately as bool("False") == True
if t == bool:
env_value = bool(strtobool(os.environ[field]))
# Cast to desired type
Expand All @@ -61,3 +58,5 @@ class Config(EnvParser):
PORT: int = 8000
AUTH0_DOMAIN: str = ""
AUTH0_API_AUDIENCE: str = ""
# Optional configuration for the forecast submission deadline (9:00 AM IST)
FORECAST_DEADLINE_HOUR: int = 9
34 changes: 29 additions & 5 deletions src/india_api/internal/inputs/utils.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import datetime as dt

from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo

def get_window() -> tuple[dt.datetime, dt.datetime]:
"""Returns the start and end of the window for timeseries data."""
# Window start is the beginning of the day two days ago
"""Returns the start and end of the window for timeseries data.

Window start is the beginning of the day two days ago.
Window end is the beginning of the day two days ahead.
"""
# Using UTC for current window calculation
start = (dt.datetime.now(tz=dt.UTC) - dt.timedelta(days=2)).replace(
hour=0,
minute=0,
second=0,
microsecond=0,
)
# Window end is the beginning of the day two days ahead
end = (dt.datetime.now(tz=dt.UTC) + dt.timedelta(days=2)).replace(
hour=0,
minute=0,
second=0,
microsecond=0,
)
return (start, end)
return (start, end)

def is_submission_valid(forecast_target: datetime, submission_time: datetime) -> bool:
"""
Validates whether a forecast submission is made on time.

For a forecast targeting a specific datetime (forecast_target) in IST,
the submission must be made before 9:00 AM IST on the day prior to the forecast date.

Returns True if submission_time is before the deadline, False otherwise.
"""
ist = ZoneInfo("Asia/Kolkata")
# Ensure both times are in IST
forecast_target_ist = forecast_target.astimezone(ist)
submission_time_ist = submission_time.astimezone(ist)

# Deadline is set to 9:00 AM IST on the day before the forecast target date.
deadline_date = forecast_target_ist.date() - timedelta(days=1)
deadline = datetime.combine(deadline_date, time(9, 0, 0), tzinfo=ist)

return submission_time_ist < deadline
53 changes: 25 additions & 28 deletions src/india_api/internal/service/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,24 @@
log = logging.getLogger(__name__)
version = "0.1.52"


tags_metadata = [
{
"name": "API Information",
"description": "General API information,",
"description": "General API information.",
},
{
"name": "Sites",
"description": "A site is a specific point location, for example (52.15, 1.25) in latitude and longitude. "
"Each site will have one source of energy "
"and there is forecast and generation data for each site. ",
"description": (
"A site is a specific point location, for example (52.15, 1.25) in latitude and longitude. "
"Each site will have one source of energy, and there is forecast and generation data for each site."
),
},
# I want to keep this here, as we might add this back in the future
# {
# "name": "Regions",
# "description": "A region is an area of land e.g. Alaska in the USA. "
# "There is forecast and generation data for each region "
# "and there may be different sources of energy in one region.",
# },
# Future addition for regions can be enabled as needed.
]

# Updated API description with forecast submission guidelines
title = "India API"
description = """ API providing OCF Forecast for India.
description = """API providing OCF Forecast for India.

## Regions

Expand All @@ -47,28 +42,30 @@
## Sites

The sites routes are used to get site level forecasts.
A user can
- **/sites**: Get information about your sites
- **/sites/{site_uuid}/forecast**: Get a forecast for a specific site
- **/sites/{site_uuid}/forecast**: Get and post generation for a specific site
A user can:
- **/sites**: Get information about your sites.
- **/sites/{site_uuid}/forecast**: Get a forecast for a specific site.
- **/sites/{site_uuid}/forecast**: Get and post generation for a specific site.

### Forecast Submission Deadline

Day-ahead forecasts must be submitted before 9:00 AM IST on the day prior to the forecast date.
For example, a forecast for **2024-06-03 at 12:00 IST** must be submitted before **2024-06-02 at 09:00 IST**.

### Authentication and Example

If you need an authentication route, please get your access token with the following code.
You'll need a username and password.
```
export AUTH=$(curl --request POST
--url https://nowcasting-pro.eu.auth0.com/oauth/token
--header 'content-type: application/json'
--data '{"client_id":"TODO", "audience":"https://api.nowcasting.io/", "grant_type":"password", "username":"username", "password":"password"}'
)

export AUTH=$(curl --request POST \\
--url https://nowcasting-pro.eu.auth0.com/oauth/token \\
--header 'content-type: application/json' \\
--data '{"client_id":"TODO", "audience":"https://api.nowcasting.io/", "grant_type":"password", "username":"username", "password":"password"}')

export TOKEN=$(echo "${AUTH}" | jq '.access_token' | tr -d '"')
```
You can then use
```
curl -X GET 'https://api.quartz.energy/sites' -H "Authorization: Bearer $TOKEN"
```

You can then use:
curl -X GET 'https://api.quartz.energy/sites' -H "Authorization: Bearer $TOKEN"
"""

server = FastAPI(
Expand Down