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

Enode API Changes #87

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
23e4aa8
Create BaseModels for inverter data
neha-vard Apr 13, 2023
cd99e2e
Test
neha-vard Apr 13, 2023
db19834
Add fake inverter test
anyaparekh Apr 17, 2023
0eddcde
Create httpx auth class
jackypark9852 Apr 14, 2023
e1dadaf
Add enode link URL endpoint (#86)
AndrewLester Apr 23, 2023
3541f2f
[WIP] Add put endpoint for editing site (#88)
ericcccsliu Apr 23, 2023
f491656
Add post inverters for site endpoint
AndrewLester Apr 23, 2023
f4258e3
Add Enode auth test
AndrewLester Apr 24, 2023
ec1898c
Fix format
AndrewLester Apr 24, 2023
6715206
Change update inverters for site from post to put
AndrewLester Apr 25, 2023
d904f3a
Fix all review comments besides blocking client queries
AndrewLester Apr 27, 2023
ab5a5c1
remove redirectresponse class
ericcccsliu Apr 27, 2023
e4c0922
lint and format
ericcccsliu Apr 27, 2023
94c3767
Add client fetch to auth dependency and create new dependency for asy…
AndrewLester Apr 28, 2023
6329cb1
Use a pydantic model for client to serialize it in auth
AndrewLester Apr 29, 2023
af3ca34
Merge remote-tracking branch 'origin/main' into h4i/enode
AndrewLester May 4, 2023
4a04fd5
Add sentry init back and fix tests
AndrewLester May 4, 2023
381f849
Add logs for enode link and site inverters
AndrewLester May 4, 2023
5d622a2
Use httpx-auth to remove Enode auth code
AndrewLester May 4, 2023
e185d93
Fix format
AndrewLester May 4, 2023
246724d
Add test default values for Enode auth
AndrewLester May 16, 2023
7e80ee4
Handle case where no Enode user exists
AndrewLester May 18, 2023
819f55f
Fix import order
AndrewLester May 18, 2023
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
75 changes: 47 additions & 28 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 33 additions & 1 deletion pv_site_api/_db_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,26 @@

import sqlalchemy as sa
import structlog
from fastapi import Depends
from pvsite_datamodel.read.generation import get_pv_generation_by_sites
from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, SiteSQL
from pvsite_datamodel.sqlmodels import (
ClientSQL,
ForecastSQL,
ForecastValueSQL,
InverterSQL,
SiteSQL,
)
from sqlalchemy.orm import Session, aliased

from .pydantic_models import (
Forecast,
MultiplePVActual,
PVActualValue,
PVClientMetadata,
PVSiteMetadata,
SiteForecastValues,
)
from .session import get_session

logger = structlog.stdlib.get_logger()

Expand Down Expand Up @@ -229,9 +238,32 @@ def site_to_pydantic(site: SiteSQL) -> PVSiteMetadata:
return pv_site


def client_to_pydantic(client: ClientSQL) -> PVClientMetadata:
"""Converts a ClientSQL object into a PVClientMetadata object."""
pv_client = PVClientMetadata(
client_uuid=str(client.client_uuid), client_name=client.client_name
)
return pv_client


def does_site_exist(session: Session, site_uuid: str) -> bool:
"""Checks if a site exists."""
return (
session.execute(sa.select(SiteSQL).where(SiteSQL.site_uuid == site_uuid)).one_or_none()
is not None
)


def get_inverters_for_site(
site_uuid: str, session: Session = Depends(get_session)
) -> list[Row] | None:
"""Path dependency to get a list of inverters for a site, or None if the site doesn't exist"""
if not does_site_exist(session, site_uuid):
return None

query = session.query(InverterSQL).filter(InverterSQL.site_uuid == site_uuid)
inverters = query.all()

logger.info(f"Found {len(inverters)} inverters for site {site_uuid}")

return inverters
21 changes: 18 additions & 3 deletions pv_site_api/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import jwt
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pvsite_datamodel.sqlmodels import ClientSQL
from sqlalchemy.orm import Session

from .session import get_session

token_auth_scheme = HTTPBearer()

Expand All @@ -15,7 +19,11 @@ def __init__(self, domain: str, api_audience: str, algorithm: str):

self._jwks_client = jwt.PyJWKClient(f"https://{domain}/.well-known/jwks.json")

def __call__(self, auth_credentials: HTTPAuthorizationCredentials = Depends(token_auth_scheme)):
def __call__(
self,
auth_credentials: HTTPAuthorizationCredentials = Depends(token_auth_scheme),
session: Session = Depends(get_session),
):
token = auth_credentials.credentials

try:
Expand All @@ -24,7 +32,7 @@ def __call__(self, auth_credentials: HTTPAuthorizationCredentials = Depends(toke
raise HTTPException(status_code=401, detail=str(e))

try:
payload = jwt.decode(
jwt.decode(
token,
signing_key,
algorithms=self._algorithm,
Expand All @@ -34,4 +42,11 @@ def __call__(self, auth_credentials: HTTPAuthorizationCredentials = Depends(toke
except Exception as e:
raise HTTPException(status_code=401, detail=str(e))

return payload
if session is None:
return None

# @TODO: get client corresponding to auth
# See: https://github.com/openclimatefix/pv-site-api/issues/90
client = session.query(ClientSQL).first()
assert client is not None
return client
7 changes: 7 additions & 0 deletions pv_site_api/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import structlog

from ._db_helpers import client_to_pydantic

logger = structlog.stdlib.get_logger()

CACHE_TIME_SECONDS = 120
Expand Down Expand Up @@ -43,6 +45,11 @@ def wrapper(*args, **kwargs): # noqa
if var in route_variables:
route_variables.pop(var)

# translate authenticated client to serializable type
route_variables["auth"] = (
route_variables["auth"] and client_to_pydantic(route_variables["auth"]).json()
)

# make into string
route_variables = json.dumps(route_variables)
args_as_json = json.dumps(args)
Expand Down
39 changes: 39 additions & 0 deletions pv_site_api/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

from .pydantic_models import (
Forecast,
InverterInformation,
InverterLocation,
InverterProductionState,
Inverters,
InverterValues,
MultiplePVActual,
PVActualValue,
PVSiteAPIStatus,
Expand All @@ -19,6 +24,35 @@
fake_client_uuid = "c97f68cd-50e0-49bb-a850-108d4a9f7b7e"


def make_fake_inverters() -> Inverters:
"""Make fake inverters"""
inverter = InverterValues(
id="string",
vendor="EMA",
chargingLocationId="8d90101b-3f2f-462a-bbb4-1ed320d33bbe",
lastSeen="2020-04-07T17:04:26Z",
isReachable=True,
productionState=InverterProductionState(
productionRate=0,
isProducing=True,
totalLifetimeProduction=100152.56,
lastUpdated="2020-04-07T17:04:26Z",
),
information=InverterInformation(
id="string",
brand="EMA",
model="Sunny Boy",
siteName="Sunny Plant",
installationDate="2020-04-07T17:04:26Z",
),
location=InverterLocation(latitude=10.7197486, longitude=59.9173985),
)
inverters_list = Inverters(
inverters=[inverter],
)
return inverters_list


def make_fake_site() -> PVSites:
"""Make a fake site"""
pv_site = PVSiteMetadata(
Expand Down Expand Up @@ -87,3 +121,8 @@ def make_fake_status() -> PVSiteAPIStatus:
message="The API is up and running",
)
return pv_api_status


def make_fake_enode_link_url() -> str:
"""Make fake Enode link URL"""
return "https://enode.com"
Loading