diff --git a/src/india_api/internal/config/env.py b/src/india_api/internal/config/env.py index f2dff8f..4021d2b 100644 --- a/src/india_api/internal/config/env.py +++ b/src/india_api/internal/config/env.py @@ -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 @@ -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. - - 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): @@ -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 @@ -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 diff --git a/src/india_api/internal/inputs/utils.py b/src/india_api/internal/inputs/utils.py index aaa69f1..06f7637 100644 --- a/src/india_api/internal/inputs/utils.py +++ b/src/india_api/internal/inputs/utils.py @@ -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) \ No newline at end of file + 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 diff --git a/src/india_api/internal/service/server.py b/src/india_api/internal/service/server.py index b87f1de..66ed95c 100644 --- a/src/india_api/internal/service/server.py +++ b/src/india_api/internal/service/server.py @@ -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 @@ -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(