Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reading from real DB #10

Merged
merged 4 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
# * Produce JUnit XML report
- name: Run unit tests
run: |
.venv/bin/python3 -m xmlrunner discover -s src/india_api -p "test_*.py" --output-file ut-report.xml
.venv/bin/python3 -m pytest src/india_api --cov -s src/india_api --cov-report=xml

# Create test summary to be visualised on the job summary screen on GitHub
# * Runs even if previous steps fail
Expand Down
42 changes: 17 additions & 25 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
# Build a virtualenv using venv
# * Install required compilation tools for wheels via apt
FROM debian:12-slim AS build
RUN apt -qq update && apt -qq install -y python3-venv gcc libpython3-dev && \
python3 -m venv /venv && \
/venv/bin/pip install --upgrade -q pip setuptools wheel
FROM python:3.11-slim

# Install packages into the virtualenv as a separate step
# * Only re-execute this step when the requirements files change
FROM build AS install-deps
# install requirements
RUN apt-get clean
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libatlas-base-dev \
libgdal-dev \
gfortran

# Copy required files.
WORKDIR /app
COPY pyproject.toml pyproject.toml
RUN /venv/bin/pip install -q . --no-cache-dir --no-binary=india_api

# Build binary for the package
# * The package is versioned via setuptools_git_versioning
# hence the .git directory is required
# * The README.md is required for the long description
FROM install-deps AS build-app
COPY src src
COPY .git .git
COPY README.md README.md
RUN /venv/bin/pip install .
RUN ls /venv/bin

# Copy the virtualenv into a distroless image
# * These are small images that only contain the runtime dependencies
FROM gcr.io/distroless/python3-debian12
COPY --from=build-app /venv /venv
# set working directory
WORKDIR /app

# Install python requirements.
RUN pip install .

# health check and entrypoint
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1
ENTRYPOINT ["/venv/bin/india-api"]

ENTRYPOINT ["india-api"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ and create a new environment with your favorite environment manager.
Install all the dependencies with

```
pip install -e .[all]
pip install -e ".[all]"
```

You can run the service with the command `india-api`.
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ authors = [
classifiers = ["Programming Language :: Python :: 3"]
dependencies = [
"fastapi >= 0.105.0",
"pvsite-datamodel >= 1.0.10",
"pytz >= 2023.3",
"structlog >= 23.2.0",
"uvicorn >= 0.24.0",
Expand All @@ -26,6 +27,9 @@ dependencies = [
[project.optional-dependencies]
test = [
"unittest-xml-reporting == 3.2.0",
"pytest >= 8.0.0",
"pytest-cov >= 4.1.0",
"testcontainers >= 3.7.1",
]
lint = [
"mypy >= 1.7.1",
Expand Down
6 changes: 6 additions & 0 deletions src/india_api/cmd/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
cfg = Config()

match cfg.SOURCE:
case "indiadb":
if cfg.DB_URL == "" or cfg.DB_URL == None:
raise OSError(f"DB_URL env var is required using db source: {cfg.SOURCE}")

def get_db_client_override() -> internal.DatabaseInterface:
return internal.inputs.indiadb.Client(cfg.DB_URL)
case "dummydb":

def get_db_client_override() -> internal.DatabaseInterface:
Expand Down
2 changes: 2 additions & 0 deletions src/india_api/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Packages internal to the service."""

from .models import (
ActualPower,
DatabaseInterface,
PredictedPower,
)
Expand All @@ -11,6 +12,7 @@
)

__all__ = [
"ActualPower",
"PredictedPower",
"DatabaseInterface",
"inputs",
Expand Down
3 changes: 2 additions & 1 deletion src/india_api/internal/config/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,6 @@ def __init__(self) -> None:
class Config(EnvParser):
"""Config for the application."""

SOURCE: str = "dummydb"
SOURCE: str = "indiadb"
DB_URL: str = ""
PORT: int = 8000
3 changes: 2 additions & 1 deletion src/india_api/internal/inputs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import dummydb
from . import dummydb, indiadb

__all__ = [
"indiadb",
"dummydb",
]
1 change: 1 addition & 0 deletions src/india_api/internal/inputs/dummydb/test_dummydb.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

client = Client()


class TestDummyDatabase(unittest.TestCase):
def test_get_predicted_wind_yields_for_location(self) -> None:
locID = "testID"
Expand Down
5 changes: 5 additions & 0 deletions src/india_api/internal/inputs/indiadb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Defines sources of data for the API."""

from .client import Client

__all__ = ["Client"]
169 changes: 169 additions & 0 deletions src/india_api/internal/inputs/indiadb/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""India DB client that conforms to the DatabaseInterface."""
import datetime as dt
import logging

from pvsite_datamodel import DatabaseConnection
from pvsite_datamodel.read import (
get_sites_by_country,
get_latest_forecast_values_by_site,
get_pv_generation_by_sites,
)
from pvsite_datamodel.sqlmodels import SiteAssetType, ForecastValueSQL
from sqlalchemy.orm import Session

from india_api import internal

log = logging.getLogger(__name__)


class Client(internal.DatabaseInterface):
"""Defines India DB client that conforms to the DatabaseInterface."""

session: Session = None

def __init__(self, database_url: str) -> None:
"""Initialize the client with a SQLAlchemy database connection and session."""

self.connection = DatabaseConnection(url=database_url, echo=False)

def _get_session(self):
"""Allows for overriding the default session (useful for testing)"""
if self.session is None:
return self.connection.get_session()
else:
return self.session

def get_predicted_yields_for_location(
self,
location: str,
asset_type: SiteAssetType,
) -> list[internal.PredictedPower]:
"""Gets the predicted yields for a location."""

# Get the window
start, end = _getWindow()

# get site uuid
with self._get_session() as session:
sites = get_sites_by_country(session, country="india")

# just select wind site
sites = [s for s in sites if s.asset_type == asset_type]
site = sites[0]

# read actual generations
values = get_latest_forecast_values_by_site(
session, site_uuids=[site.site_uuid], start_utc=start
)
forecast_values: [ForecastValueSQL] = values[site.site_uuid]

# convert ForecastValueSQL to PredictedPower
values = [
internal.PredictedPower(
PowerKW=value.forecast_power_kw, Time=value.start_utc.astimezone(dt.UTC)
)
for value in forecast_values
]

return values

def get_generation_for_location(
self,
location: str,
asset_type: SiteAssetType,
) -> [internal.PredictedPower]:
"""Gets the predicted yields for a location."""

# Get the window
start, end = _getWindow()

# get site uuid
with self._get_session() as session:
sites = get_sites_by_country(session, country="india")

# just select wind site
sites = [site for site in sites if site.asset_type == asset_type]
site = sites[0]

# read actual generations
values = get_pv_generation_by_sites(
session=session, site_uuids=[site.site_uuid], start_utc=start, end_utc=end
)

# convert from GenerationSQL to PredictedPower
values = [
internal.ActualPower(
PowerKW=value.generation_power_kw, Time=value.start_utc.astimezone(dt.UTC)
)
for value in values
]

return values

def get_predicted_solar_yields_for_location(
self,
location: str,
) -> [internal.PredictedPower]:
"""
Gets the predicted solar yields for a location.

Args:
location: The location to get the predicted solar yields for.
"""

return self.get_predicted_yields_for_location(
location=location, asset_type=SiteAssetType.pv
)

def get_predicted_wind_yields_for_location(
self,
location: str,
) -> list[internal.PredictedPower]:
"""
Gets the predicted wind yields for a location.

Args:
location: The location to get the predicted wind yields for.
"""

return self.get_predicted_yields_for_location(
location=location, asset_type=SiteAssetType.wind
)

def get_actual_solar_yields_for_location(self, location: str) -> list[internal.PredictedPower]:
"""Gets the actual solar yields for a location."""

return self.get_generation_for_location(location=location, asset_type=SiteAssetType.pv)

def get_actual_wind_yields_for_location(self, location: str) -> list[internal.PredictedPower]:
"""Gets the actual wind yields for a location."""

return self.get_generation_for_location(location=location, asset_type=SiteAssetType.wind)

def get_wind_regions(self) -> list[str]:
"""Gets the valid wind regions."""
return ["ruvnl"]

def get_solar_regions(self) -> list[str]:
"""Gets the valid solar regions."""
return ["ruvnl"]


def _getWindow() -> tuple[dt.datetime, dt.datetime]:
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is duplicated from a different model, should we refactor tor remove duplciate code. Happy if its put as an issue and dealt with later

"""Returns the start and end of the window for timeseries data."""

# Window start is the beginning of the day two days ago
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
Loading
Loading