From a1028304a628ebfbb21f0caa89855525262bc568 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 13 Apr 2023 18:27:25 -0500 Subject: [PATCH 01/23] Create BaseModels for inverter data --- pv_site_api/pydantic_models.py | 92 +++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/pv_site_api/pydantic_models.py b/pv_site_api/pydantic_models.py index 1e7b0e0..315a8f1 100644 --- a/pv_site_api/pydantic_models.py +++ b/pv_site_api/pydantic_models.py @@ -22,23 +22,30 @@ class PVSiteMetadata(BaseModel): """Site metadata""" site_uuid: str = Field(..., description="Unique internal ID for site.") - client_name: str = Field(..., description="Unique name for user providing the site data.") - client_site_id: str = Field(..., description="The site ID as given by the providing user.") + client_name: str = Field(..., + description="Unique name for user providing the site data.") + client_site_id: str = Field(..., + description="The site ID as given by the providing user.") client_site_name: str = Field( None, decription="The name of the site as given by the providing uuser." ) region: Optional[str] = Field(None, decription="The region of the PV site") - dno: Optional[str] = Field(None, decription="The distribution network operator of the PV site.") - gsp: Optional[str] = Field(None, decription="The grid supply point of the PV site.") + dno: Optional[str] = Field( + None, decription="The distribution network operator of the PV site.") + gsp: Optional[str] = Field( + None, decription="The grid supply point of the PV site.") orientation: Optional[float] = Field( None, description="The rotation of the panel in degrees. 180° points south" ) tilt: Optional[float] = Field( None, description="The tile of the panel in degrees. 90° indicates the panel is vertical." ) - latitude: float = Field(..., description="The site's latitude", ge=-90, le=90) - longitude: float = Field(..., description="The site's longitude", ge=-180, le=180) - installed_capacity_kw: float = Field(..., description="The site's capacity in kw", ge=0) + latitude: float = Field(..., + description="The site's latitude", ge=-90, le=90) + longitude: float = Field(..., + description="The site's longitude", ge=-180, le=180) + installed_capacity_kw: float = Field(..., + description="The site's capacity in kw", ge=0) # post_pv_actual @@ -50,7 +57,8 @@ class PVActualValue(BaseModel): """PV Actual Value list""" datetime_utc: datetime = Field(..., description="Time of data input") - actual_generation_kw: float = Field(..., description="Actual kw generation", ge=0) + actual_generation_kw: float = Field(..., + description="Actual kw generation", ge=0) class MultiplePVActual(BaseModel): @@ -66,8 +74,10 @@ class SiteForecastValues(BaseModel): """Forecast value list""" # forecast_value_uuid: str = Field(..., description="ID for this specific forecast value") - target_datetime_utc: datetime = Field(..., description="Target time for forecast") - expected_generation_kw: float = Field(..., description="Expected generation in kw") + target_datetime_utc: datetime = Field(..., + description="Target time for forecast") + expected_generation_kw: float = Field(..., + description="Expected generation in kw") # get_forecast @@ -99,8 +109,10 @@ class PVSites(BaseModel): class ClearskyEstimateValues(BaseModel): """Clearsky estimate data for a single time""" - target_datetime_utc: datetime = Field(..., description="Time for clearsky estimate") - clearsky_generation_kw: float = Field(..., description="Clearsky estimate in kW", ge=0) + target_datetime_utc: datetime = Field(..., + description="Time for clearsky estimate") + clearsky_generation_kw: float = Field(..., + description="Clearsky estimate in kW", ge=0) class ClearskyEstimate(BaseModel): @@ -109,3 +121,59 @@ class ClearskyEstimate(BaseModel): clearsky_estimate: List[ClearskyEstimateValues] = Field( ..., description="List of times and clearsky estimate" ) + + +class InverterProductionState(BaseModel): + """Production State data for an inverter""" + productionRate: float = Field(..., + description="The current production rate in kW") + isProducing: bool = Field( + ..., description="Whether the solar inverter is actively producing energy or not") + totalLifetimeProduction: float = Field( + ..., description="The total lifetime production in kWh") + lastUpdated: str = Field( + ..., description="ISO8601 UTC timestamp of last received production state update") + + +class InverterInformation(BaseModel): + """"Inverter information""" + id: str = Field(..., description="Solar inverter vendor ID") + brand: str = Field(..., description="Solar inverter brand") + model: str = Field(..., description="Solar inverter model") + siteName: str = Field(..., description="Name of the site, as set by the user on the device/vendor. If no user-specified name is available, we construct a fallback name using the vendor/device/model names.") + installationDate: str = Field(..., + description="Solar inverter installation date") + + +class InverterLocation(BaseModel): + """"Longitude and Latitude of inverter""" + + longitude: float = Field(..., description="Longitude in degrees") + latitude: float = Field(..., description="Latitude in degrees") + + +class InverterValues(BaseModel): + """Inverter Data for a site""" + + id: str = Field(..., description="Solar Inverter ID") + vendor: str = Field( + ..., description="One of EMA ENPHASE FRONIUS GOODWE GROWATT HUAWEI SMA SOLAREDGE SOLIS") + chargingLocationId: str = Field( + ..., description="ID of the charging location the solar inverter is currently positioned at (if any).") + lastSeen: str = Field( + ..., description="The last time the solar inverter was successfully communicated with") + isReachable: str = Field(..., description="Whether live data from the solar inverter is currently reachable from Enode's perspective. This 'reachability' may refer to reading from a cache operated by the solar inverter's cloud service if that service has determined that its cache is valid.") + productionState: InverterProductionState = Field( + ..., description="Descriptive information about the production state") + information: InverterInformation = Field( + ..., description="Descriptive information about the solar inverter") + location: InverterLocation = Field(..., + description="Solar inverter's GPS coordinates") + + +class Inverters(BaseModel): + """Return all Inverter Data""" + + inverters: List[InverterValues] = Field( + ..., description="List of inverter data" + ) From 8ed7b70e7f7213e78e9a0ec9b5ecda2ffb8c9cfd Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 13 Apr 2023 18:42:06 -0500 Subject: [PATCH 02/23] Test --- pv_site_api/main.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 1a51a7f..03b6aaa 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -38,6 +38,7 @@ PVSiteAPIStatus, PVSiteMetadata, PVSites, + Inverters ) from .redoc_theme import get_redoc_html_with_theme from .session import get_session @@ -188,7 +189,8 @@ def post_site_info(site_info: PVSiteMetadata, session: Session = Depends(get_ses """ if int(os.environ["FAKE"]): - print(f"Successfully added {site_info.dict()} for site {site_info.client_site_name}") + print( + f"Successfully added {site_info.dict()} for site {site_info.client_site_name}") print("Not doing anything with it (yet!)") return @@ -318,7 +320,8 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess # Create DatetimeIndex over four days, with a frequency of 15 minutes. # Starts from midnight yesterday. - times = pd.date_range(start=get_yesterday_midnight(), periods=384, freq="15min", tz="UTC") + times = pd.date_range(start=get_yesterday_midnight(), + periods=384, freq="15min", tz="UTC") clearsky = loc.get_clearsky(times) solar_position = loc.get_solarposition(times=times) @@ -342,19 +345,39 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess pv_system = pvsystem.PVSystem( surface_tilt=tilt, surface_azimuth=orientation, - module_parameters={"pdc0": (1.5 * site.installed_capacity_kw), "gamma_pdc": -0.005}, + module_parameters={ + "pdc0": (1.5 * site.installed_capacity_kw), "gamma_pdc": -0.005}, inverter_parameters={"pdc0": site.installed_capacity_kw}, ) pac = irr.apply( lambda row: pv_system.get_ac("pvwatts", pv_system.pvwatts_dc(row["poa_global"], 25)), axis=1 ) pac = pac.reset_index() - pac = pac.rename(columns={"index": "target_datetime_utc", 0: "clearsky_generation_kw"}) + pac = pac.rename( + columns={"index": "target_datetime_utc", 0: "clearsky_generation_kw"}) pac["target_datetime_utc"] = pac["target_datetime_utc"].dt.tz_convert(None) res = {"clearsky_estimate": pac.to_dict("records")} return res +# @app.get("/inverters", response_model=Inverters) +# def get_inverters( +# site_uuids: str, +# session: Session = Depends(get_session), +# ): +# """ +# ### Get the actual power generation for a list of sites. +# """ +# site_uuids_list = site_uuids.split(",") + +# if int(os.environ["FAKE"]): +# return [make_fake_pv_generation(site_uuid) for site_uuid in site_uuids_list] + +# start_utc = get_yesterday_midnight() + +# return get_generation_by_sites(session, site_uuids=site_uuids_list, start_utc=start_utc) + + # get_status: get the status of the system @app.get("/api_status", response_model=PVSiteAPIStatus) def get_status(session: Session = Depends(get_session)): From 1dbd8306b1a190f508dd36cedbc6b1440e45ab9a Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Mon, 17 Apr 2023 17:24:41 -0500 Subject: [PATCH 03/23] Add fake inverter test --- pv_site_api/fake.py | 35 ++++++++++++++++++++++++++++ pv_site_api/main.py | 51 ++++++++++++++++++++++++++++++----------- tests/conftest.py | 21 +++++++++++++++++ tests/test_inverters.py | 18 +++++++++++++++ 4 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 tests/test_inverters.py diff --git a/pv_site_api/fake.py b/pv_site_api/fake.py index e5511f1..b2886c0 100644 --- a/pv_site_api/fake.py +++ b/pv_site_api/fake.py @@ -12,12 +12,47 @@ PVSiteMetadata, PVSites, SiteForecastValues, + InverterValues, + Inverters, + InverterProductionState, + InverterLocation, + InverterInformation ) from .utils import make_fake_intensity fake_site_uuid = "b97f68cd-50e0-49bb-a850-108d4a9f7b7e" fake_client_uuid = "c97f68cd-50e0-49bb-a850-108d4a9f7b7e" +def make_fake_inverters() -> Inverters: + """Make fake inverters""" + inverter = InverterValues( + id="0", + vendor="Test", + chargingLocationId="0", + lastSeen="never", + isReachable="false", + productionState=InverterProductionState( + productionRate=0.1, + isProducing=False, + totalLifetimeProduction=0.5, + lastUpdated="never" + ), + information=InverterInformation( + id="0", + brand="tesla", + model="x", + siteName="ocf", + installationDate="1-23-4567" + ), + location=InverterLocation( + latitude=0.0, + longitude=0.1 + ) + ) + inverters_list = Inverters( + inverters=[inverter], + ) + return inverters_list def make_fake_site() -> PVSites: """Make a fake site""" diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 03b6aaa..a591eaf 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -30,6 +30,7 @@ make_fake_pv_generation, make_fake_site, make_fake_status, + make_fake_inverters ) from .pydantic_models import ( ClearskyEstimate, @@ -360,22 +361,46 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess return res -# @app.get("/inverters", response_model=Inverters) -# def get_inverters( -# site_uuids: str, -# session: Session = Depends(get_session), -# ): -# """ -# ### Get the actual power generation for a list of sites. -# """ -# site_uuids_list = site_uuids.split(",") +@app.get("/inverters") +def get_inverters( + session: Session = Depends(get_session), +): + if int(os.environ["FAKE"]): + return make_fake_inverters() + + # client = ClientSQL(client_uuid=1, client_name="bob") + + # site = SiteSQL( + # client_uuid=client.client_uuid, + # client_site_id="123", + # client_site_name="bobby", + # region="grainger", + # dno="x", + # gsp="idk", + # orientation=50, + # tilt=98, + # latitude=45, + # longitude=45, + # capacity_kw=240, + # ml_id=1, # TODO remove this once https://github.com/openclimatefix/pvsite-datamodel/issues/27 is complete # noqa + # ) -# if int(os.environ["FAKE"]): -# return [make_fake_pv_generation(site_uuid) for site_uuid in site_uuids_list] + # add site + # session.add(client) + # session.add(site) + # session.commit() + + # print(session) + + # client = session.query(ClientSQL).first() + # assert client is not None + + # siteUUIDs = session.query(ClientSQL, SiteSQL).join(SiteSQL).filter(SiteSQL.client_uuid == ClientSQL.client_uuid) + # print(siteUUIDs) -# start_utc = get_yesterday_midnight() + # inverters = get_inverters_for_client(session=session, client_uuid=client.client_uuid) -# return get_generation_by_sites(session, site_uuids=site_uuids_list, start_utc=start_utc) + return "hi" # get_status: get the status of the system diff --git a/tests/conftest.py b/tests/conftest.py index 3996622..b6d052a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ ForecastValueSQL, GenerationSQL, SiteSQL, + InverterSQL ) from sqlalchemy import create_engine from sqlalchemy.orm import Session @@ -89,6 +90,26 @@ def sites(db_session, clients): return sites +@pytest.fixture() +def inverters(db_session, sites): + """Create some fake inverters""" + inverters = [] + num_inverters = 3 + for site in sites: + for j in range(num_inverters): + inverter = InverterSQL( + site_uuid=site.site_uuid, + inverter_id=j, + inverter_name=f"inverter_{j}", + ) + inverters.append(inverter) + + db_session.add_all(inverters) + db_session.commit() + + return inverters + + @pytest.fixture() def generations(db_session, sites): """Create some fake generations""" diff --git a/tests/test_inverters.py b/tests/test_inverters.py new file mode 100644 index 0000000..03b1aa9 --- /dev/null +++ b/tests/test_inverters.py @@ -0,0 +1,18 @@ +""" Test for main app """ + +from pv_site_api.pydantic_models import Inverters + + +def test_get_inverters_fake(client, fake): + response = client.get("/inverters") + assert response.status_code == 200 + + inverters = Inverters(**response.json()) + assert len(inverters.inverters) > 0 + +# def test_get_inverters(db_session, client, forecast_values): +# response = client.get(f"/sites/{site_uuid}/clearsky_estimate") +# assert response.status_code == 200 + +# clearsky_estimate = ClearskyEstimate(**response.json()) +# assert len(clearsky_estimate.clearsky_estimate) > 0 From 31b05e8b5a520b0f75a37bc723c248dfff2a7f36 Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Mon, 17 Apr 2023 17:26:55 -0500 Subject: [PATCH 04/23] Fix lint errors --- pv_site_api/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index a591eaf..9b56051 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -374,7 +374,6 @@ def get_inverters( # client_uuid=client.client_uuid, # client_site_id="123", # client_site_name="bobby", - # region="grainger", # dno="x", # gsp="idk", # orientation=50, From 1a092caae1c3781658432a3a671e204c5a57c23e Mon Sep 17 00:00:00 2001 From: neha-vard Date: Mon, 17 Apr 2023 22:04:59 -0500 Subject: [PATCH 05/23] Finish both endpoints --- .vscode/settings.json | 3 ++ poetry.lock | 16 +++---- pv_site_api/_db_helpers.py | 9 +++- pv_site_api/main.py | 88 +++++++++++++++++++++----------------- tests/conftest.py | 6 +-- 5 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de288e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index ed6ba62..cf177a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "anyio" @@ -781,14 +781,14 @@ files = [ [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] @@ -1085,7 +1085,7 @@ sqlalchemy = "1.4.46" type = "git" url = "https://github.com/openclimatefix/pv-site-datamodel.git" reference = "enode-updates" -resolved_reference = "83e91e390106e76637495de6baeb1e4afe337836" +resolved_reference = "211241af267c4bdaeb32009ede7228ec911845c9" [[package]] name = "pydantic" @@ -1157,14 +1157,14 @@ plugins = ["importlib-metadata"] [[package]] name = "pytest" -version = "7.3.0" +version = "7.3.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.0-py3-none-any.whl", hash = "sha256:933051fa1bfbd38a21e73c3960cebdad4cf59483ddba7696c48509727e17f201"}, - {file = "pytest-7.3.0.tar.gz", hash = "sha256:58ecc27ebf0ea643ebfdf7fb1249335da761a00c9f955bcd922349bcb68ee57d"}, + {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, + {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, ] [package.dependencies] diff --git a/pv_site_api/_db_helpers.py b/pv_site_api/_db_helpers.py index c1fd83a..fa3f5e7 100644 --- a/pv_site_api/_db_helpers.py +++ b/pv_site_api/_db_helpers.py @@ -13,7 +13,7 @@ import sqlalchemy as sa from pvsite_datamodel.read.generation import get_pv_generation_by_sites -from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, SiteSQL +from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, SiteSQL, InverterSQL from sqlalchemy.orm import Session, aliased from .pydantic_models import ( @@ -54,6 +54,13 @@ def _get_forecasts_for_horizon( return list(session.execute(stmt)) +def _get_inverters_by_site(session: Session, site_uuid: str) -> list[Row]: + query = ( + session.query(InverterSQL) + .filter(InverterSQL.site_uuid.is_(site_uuid)) + ) + + return query.all() def _get_latest_forecast_by_sites(session: Session, site_uuids: list[str]) -> list[Row]: """Get the latest forecast for given site uuids.""" diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 9b56051..82f4c24 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -19,6 +19,7 @@ import pv_site_api from ._db_helpers import ( + _get_inverters_by_site, does_site_exist, get_forecasts_by_sites, get_generation_by_sites, @@ -30,7 +31,7 @@ make_fake_pv_generation, make_fake_site, make_fake_status, - make_fake_inverters + make_fake_inverters, ) from .pydantic_models import ( ClearskyEstimate, @@ -39,11 +40,15 @@ PVSiteAPIStatus, PVSiteMetadata, PVSites, - Inverters + Inverters, + InverterValues, ) from .redoc_theme import get_redoc_html_with_theme from .session import get_session from .utils import get_yesterday_midnight +import httpx +import json +import asyncio load_dotenv() @@ -190,8 +195,7 @@ def post_site_info(site_info: PVSiteMetadata, session: Session = Depends(get_ses """ if int(os.environ["FAKE"]): - print( - f"Successfully added {site_info.dict()} for site {site_info.client_site_name}") + print(f"Successfully added {site_info.dict()} for site {site_info.client_site_name}") print("Not doing anything with it (yet!)") return @@ -321,8 +325,7 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess # Create DatetimeIndex over four days, with a frequency of 15 minutes. # Starts from midnight yesterday. - times = pd.date_range(start=get_yesterday_midnight(), - periods=384, freq="15min", tz="UTC") + times = pd.date_range(start=get_yesterday_midnight(), periods=384, freq="15min", tz="UTC") clearsky = loc.get_clearsky(times) solar_position = loc.get_solarposition(times=times) @@ -346,60 +349,65 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess pv_system = pvsystem.PVSystem( surface_tilt=tilt, surface_azimuth=orientation, - module_parameters={ - "pdc0": (1.5 * site.installed_capacity_kw), "gamma_pdc": -0.005}, + module_parameters={"pdc0": (1.5 * site.installed_capacity_kw), "gamma_pdc": -0.005}, inverter_parameters={"pdc0": site.installed_capacity_kw}, ) pac = irr.apply( lambda row: pv_system.get_ac("pvwatts", pv_system.pvwatts_dc(row["poa_global"], 25)), axis=1 ) pac = pac.reset_index() - pac = pac.rename( - columns={"index": "target_datetime_utc", 0: "clearsky_generation_kw"}) + pac = pac.rename(columns={"index": "target_datetime_utc", 0: "clearsky_generation_kw"}) pac["target_datetime_utc"] = pac["target_datetime_utc"].dt.tz_convert(None) res = {"clearsky_estimate": pac.to_dict("records")} return res -@app.get("/inverters") -def get_inverters( - session: Session = Depends(get_session), -): +async def get_inverters_helper(session, inverter_ids): if int(os.environ["FAKE"]): return make_fake_inverters() - # client = ClientSQL(client_uuid=1, client_name="bob") - - # site = SiteSQL( - # client_uuid=client.client_uuid, - # client_site_id="123", - # client_site_name="bobby", - # dno="x", - # gsp="idk", - # orientation=50, - # tilt=98, - # latitude=45, - # longitude=45, - # capacity_kw=240, - # ml_id=1, # TODO remove this once https://github.com/openclimatefix/pvsite-datamodel/issues/27 is complete # noqa - # ) + client = session.query(ClientSQL).first() + assert client is not None - # add site - # session.add(client) - # session.add(site) - # session.commit() + async with httpx.AsyncClient() as httpxClient: + headers = {"Enode-User-Id": client.client_uuid} + if not inverter_ids.length: + return None + inverters_raw = await asyncio.gather( + *[ + httpxClient.get( + f"https://enode-api.production.enode.io/inverters/{id}", headers=headers + ) + for id in inverter_ids + ] + ) + inverters = [InverterValues(**inverter_raw.json()) for inverter_raw in inverters_raw] + + return Inverters(inverters) - # print(session) - # client = session.query(ClientSQL).first() - # assert client is not None +@app.get("/inverters") +async def get_inverters( + session: Session = Depends(get_session), +): + async with httpx.AsyncClient() as httpxClient: + headers = {"Enode-User-Id": client.client_uuid} + r = await httpxClient.get( + "https://enode-api.production.enode.io/inverters", headers=headers + ).json() + inverter_ids = [str(value) for value in r] + + return get_inverters_helper(session, inverter_ids) - # siteUUIDs = session.query(ClientSQL, SiteSQL).join(SiteSQL).filter(SiteSQL.client_uuid == ClientSQL.client_uuid) - # print(siteUUIDs) - # inverters = get_inverters_for_client(session=session, client_uuid=client.client_uuid) +@app.get("/sites/{site_uuid}/inverters") +async def get_inverters( + site_uuid: str, + session: Session = Depends(get_session), +): + inverter_ids = [inverter.inverter_uuid for inverter in _get_inverters_by_site(site_uuid)] - return "hi" + return get_inverters_helper(session, inverter_ids) # get_status: get the status of the system diff --git a/tests/conftest.py b/tests/conftest.py index b6d052a..8e03a4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,8 +99,7 @@ def inverters(db_session, sites): for j in range(num_inverters): inverter = InverterSQL( site_uuid=site.site_uuid, - inverter_id=j, - inverter_name=f"inverter_{j}", + client_id="test" ) inverters.append(inverter) @@ -151,7 +150,8 @@ def forecast_values(db_session, sites): num_forecasts = 10 num_values_per_forecast = 11 - timestamps = [datetime.utcnow() - timedelta(minutes=10 * i) for i in range(num_forecasts)] + timestamps = [datetime.utcnow() - timedelta(minutes=10 * i) + for i in range(num_forecasts)] # To make things trickier we make a second forecast at the same for one of the timestamps. timestamps = timestamps + timestamps[-1:] From ac98ac85cb3edbe73d5e65f121911d7ac58acfa6 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Mon, 17 Apr 2023 22:08:19 -0500 Subject: [PATCH 06/23] Fix formatting and lint errors --- pv_site_api/_db_helpers.py | 9 ++-- pv_site_api/fake.py | 28 ++++------- pv_site_api/main.py | 16 +++--- pv_site_api/pydantic_models.py | 90 +++++++++++++++++----------------- tests/conftest.py | 10 ++-- tests/test_inverters.py | 1 + 6 files changed, 71 insertions(+), 83 deletions(-) diff --git a/pv_site_api/_db_helpers.py b/pv_site_api/_db_helpers.py index fa3f5e7..a907386 100644 --- a/pv_site_api/_db_helpers.py +++ b/pv_site_api/_db_helpers.py @@ -13,7 +13,7 @@ import sqlalchemy as sa from pvsite_datamodel.read.generation import get_pv_generation_by_sites -from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, SiteSQL, InverterSQL +from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, InverterSQL, SiteSQL from sqlalchemy.orm import Session, aliased from .pydantic_models import ( @@ -54,14 +54,13 @@ def _get_forecasts_for_horizon( return list(session.execute(stmt)) + def _get_inverters_by_site(session: Session, site_uuid: str) -> list[Row]: - query = ( - session.query(InverterSQL) - .filter(InverterSQL.site_uuid.is_(site_uuid)) - ) + query = session.query(InverterSQL).filter(InverterSQL.site_uuid.is_(site_uuid)) return query.all() + def _get_latest_forecast_by_sites(session: Session, site_uuids: list[str]) -> list[Row]: """Get the latest forecast for given site uuids.""" # Get the latest forecast for each site. diff --git a/pv_site_api/fake.py b/pv_site_api/fake.py index b2886c0..6dd3659 100644 --- a/pv_site_api/fake.py +++ b/pv_site_api/fake.py @@ -6,23 +6,24 @@ from .pydantic_models import ( Forecast, + InverterInformation, + InverterLocation, + InverterProductionState, + Inverters, + InverterValues, MultiplePVActual, PVActualValue, PVSiteAPIStatus, PVSiteMetadata, PVSites, SiteForecastValues, - InverterValues, - Inverters, - InverterProductionState, - InverterLocation, - InverterInformation ) from .utils import make_fake_intensity fake_site_uuid = "b97f68cd-50e0-49bb-a850-108d4a9f7b7e" fake_client_uuid = "c97f68cd-50e0-49bb-a850-108d4a9f7b7e" + def make_fake_inverters() -> Inverters: """Make fake inverters""" inverter = InverterValues( @@ -32,28 +33,19 @@ def make_fake_inverters() -> Inverters: lastSeen="never", isReachable="false", productionState=InverterProductionState( - productionRate=0.1, - isProducing=False, - totalLifetimeProduction=0.5, - lastUpdated="never" + productionRate=0.1, isProducing=False, totalLifetimeProduction=0.5, lastUpdated="never" ), information=InverterInformation( - id="0", - brand="tesla", - model="x", - siteName="ocf", - installationDate="1-23-4567" + id="0", brand="tesla", model="x", siteName="ocf", installationDate="1-23-4567" ), - location=InverterLocation( - latitude=0.0, - longitude=0.1 - ) + location=InverterLocation(latitude=0.0, longitude=0.1), ) inverters_list = Inverters( inverters=[inverter], ) return inverters_list + def make_fake_site() -> PVSites: """Make a fake site""" pv_site = PVSiteMetadata( diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 82f4c24..214aeac 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -1,7 +1,9 @@ """Main API Routes""" +import asyncio import logging import os +import httpx import pandas as pd import sentry_sdk from dotenv import load_dotenv @@ -28,27 +30,24 @@ from .fake import ( fake_site_uuid, make_fake_forecast, + make_fake_inverters, make_fake_pv_generation, make_fake_site, make_fake_status, - make_fake_inverters, ) from .pydantic_models import ( ClearskyEstimate, Forecast, + Inverters, + InverterValues, MultiplePVActual, PVSiteAPIStatus, PVSiteMetadata, PVSites, - Inverters, - InverterValues, ) from .redoc_theme import get_redoc_html_with_theme from .session import get_session from .utils import get_yesterday_midnight -import httpx -import json -import asyncio load_dotenv() @@ -390,6 +389,9 @@ async def get_inverters_helper(session, inverter_ids): async def get_inverters( session: Session = Depends(get_session), ): + client = session.query(ClientSQL).first() + assert client is not None + async with httpx.AsyncClient() as httpxClient: headers = {"Enode-User-Id": client.client_uuid} r = await httpxClient.get( @@ -401,7 +403,7 @@ async def get_inverters( @app.get("/sites/{site_uuid}/inverters") -async def get_inverters( +async def get_inverters_by_site( site_uuid: str, session: Session = Depends(get_session), ): diff --git a/pv_site_api/pydantic_models.py b/pv_site_api/pydantic_models.py index 315a8f1..748f646 100644 --- a/pv_site_api/pydantic_models.py +++ b/pv_site_api/pydantic_models.py @@ -22,30 +22,23 @@ class PVSiteMetadata(BaseModel): """Site metadata""" site_uuid: str = Field(..., description="Unique internal ID for site.") - client_name: str = Field(..., - description="Unique name for user providing the site data.") - client_site_id: str = Field(..., - description="The site ID as given by the providing user.") + client_name: str = Field(..., description="Unique name for user providing the site data.") + client_site_id: str = Field(..., description="The site ID as given by the providing user.") client_site_name: str = Field( None, decription="The name of the site as given by the providing uuser." ) region: Optional[str] = Field(None, decription="The region of the PV site") - dno: Optional[str] = Field( - None, decription="The distribution network operator of the PV site.") - gsp: Optional[str] = Field( - None, decription="The grid supply point of the PV site.") + dno: Optional[str] = Field(None, decription="The distribution network operator of the PV site.") + gsp: Optional[str] = Field(None, decription="The grid supply point of the PV site.") orientation: Optional[float] = Field( None, description="The rotation of the panel in degrees. 180° points south" ) tilt: Optional[float] = Field( None, description="The tile of the panel in degrees. 90° indicates the panel is vertical." ) - latitude: float = Field(..., - description="The site's latitude", ge=-90, le=90) - longitude: float = Field(..., - description="The site's longitude", ge=-180, le=180) - installed_capacity_kw: float = Field(..., - description="The site's capacity in kw", ge=0) + latitude: float = Field(..., description="The site's latitude", ge=-90, le=90) + longitude: float = Field(..., description="The site's longitude", ge=-180, le=180) + installed_capacity_kw: float = Field(..., description="The site's capacity in kw", ge=0) # post_pv_actual @@ -57,8 +50,7 @@ class PVActualValue(BaseModel): """PV Actual Value list""" datetime_utc: datetime = Field(..., description="Time of data input") - actual_generation_kw: float = Field(..., - description="Actual kw generation", ge=0) + actual_generation_kw: float = Field(..., description="Actual kw generation", ge=0) class MultiplePVActual(BaseModel): @@ -74,10 +66,8 @@ class SiteForecastValues(BaseModel): """Forecast value list""" # forecast_value_uuid: str = Field(..., description="ID for this specific forecast value") - target_datetime_utc: datetime = Field(..., - description="Target time for forecast") - expected_generation_kw: float = Field(..., - description="Expected generation in kw") + target_datetime_utc: datetime = Field(..., description="Target time for forecast") + expected_generation_kw: float = Field(..., description="Expected generation in kw") # get_forecast @@ -109,10 +99,8 @@ class PVSites(BaseModel): class ClearskyEstimateValues(BaseModel): """Clearsky estimate data for a single time""" - target_datetime_utc: datetime = Field(..., - description="Time for clearsky estimate") - clearsky_generation_kw: float = Field(..., - description="Clearsky estimate in kW", ge=0) + target_datetime_utc: datetime = Field(..., description="Time for clearsky estimate") + clearsky_generation_kw: float = Field(..., description="Clearsky estimate in kW", ge=0) class ClearskyEstimate(BaseModel): @@ -125,28 +113,32 @@ class ClearskyEstimate(BaseModel): class InverterProductionState(BaseModel): """Production State data for an inverter""" - productionRate: float = Field(..., - description="The current production rate in kW") + + productionRate: float = Field(..., description="The current production rate in kW") isProducing: bool = Field( - ..., description="Whether the solar inverter is actively producing energy or not") - totalLifetimeProduction: float = Field( - ..., description="The total lifetime production in kWh") + ..., description="Whether the solar inverter is actively producing energy or not" + ) + totalLifetimeProduction: float = Field(..., description="The total lifetime production in kWh") lastUpdated: str = Field( - ..., description="ISO8601 UTC timestamp of last received production state update") + ..., description="ISO8601 UTC timestamp of last received production state update" + ) class InverterInformation(BaseModel): - """"Inverter information""" + """ "Inverter information""" + id: str = Field(..., description="Solar inverter vendor ID") brand: str = Field(..., description="Solar inverter brand") model: str = Field(..., description="Solar inverter model") - siteName: str = Field(..., description="Name of the site, as set by the user on the device/vendor. If no user-specified name is available, we construct a fallback name using the vendor/device/model names.") - installationDate: str = Field(..., - description="Solar inverter installation date") + siteName: str = Field( + ..., + description="Name of the site, as set by the user on the device/vendor.", + ) + installationDate: str = Field(..., description="Solar inverter installation date") class InverterLocation(BaseModel): - """"Longitude and Latitude of inverter""" + """ "Longitude and Latitude of inverter""" longitude: float = Field(..., description="Longitude in degrees") latitude: float = Field(..., description="Latitude in degrees") @@ -157,23 +149,29 @@ class InverterValues(BaseModel): id: str = Field(..., description="Solar Inverter ID") vendor: str = Field( - ..., description="One of EMA ENPHASE FRONIUS GOODWE GROWATT HUAWEI SMA SOLAREDGE SOLIS") + ..., description="One of EMA ENPHASE FRONIUS GOODWE GROWATT HUAWEI SMA SOLAREDGE SOLIS" + ) chargingLocationId: str = Field( - ..., description="ID of the charging location the solar inverter is currently positioned at (if any).") + ..., + description="ID of the charging location the solar inverter is currently positioned at.", + ) lastSeen: str = Field( - ..., description="The last time the solar inverter was successfully communicated with") - isReachable: str = Field(..., description="Whether live data from the solar inverter is currently reachable from Enode's perspective. This 'reachability' may refer to reading from a cache operated by the solar inverter's cloud service if that service has determined that its cache is valid.") + ..., description="The last time the solar inverter was successfully communicated with" + ) + isReachable: str = Field( + ..., + description="Whether live data from the solar inverter is currently reachable from Enode.", + ) productionState: InverterProductionState = Field( - ..., description="Descriptive information about the production state") + ..., description="Descriptive information about the production state" + ) information: InverterInformation = Field( - ..., description="Descriptive information about the solar inverter") - location: InverterLocation = Field(..., - description="Solar inverter's GPS coordinates") + ..., description="Descriptive information about the solar inverter" + ) + location: InverterLocation = Field(..., description="Solar inverter's GPS coordinates") class Inverters(BaseModel): """Return all Inverter Data""" - inverters: List[InverterValues] = Field( - ..., description="List of inverter data" - ) + inverters: List[InverterValues] = Field(..., description="List of inverter data") diff --git a/tests/conftest.py b/tests/conftest.py index 8e03a4a..4c55a05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,8 @@ ForecastSQL, ForecastValueSQL, GenerationSQL, + InverterSQL, SiteSQL, - InverterSQL ) from sqlalchemy import create_engine from sqlalchemy.orm import Session @@ -97,10 +97,7 @@ def inverters(db_session, sites): num_inverters = 3 for site in sites: for j in range(num_inverters): - inverter = InverterSQL( - site_uuid=site.site_uuid, - client_id="test" - ) + inverter = InverterSQL(site_uuid=site.site_uuid, client_id="test") inverters.append(inverter) db_session.add_all(inverters) @@ -150,8 +147,7 @@ def forecast_values(db_session, sites): num_forecasts = 10 num_values_per_forecast = 11 - timestamps = [datetime.utcnow() - timedelta(minutes=10 * i) - for i in range(num_forecasts)] + timestamps = [datetime.utcnow() - timedelta(minutes=10 * i) for i in range(num_forecasts)] # To make things trickier we make a second forecast at the same for one of the timestamps. timestamps = timestamps + timestamps[-1:] diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 03b1aa9..9d9c967 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -10,6 +10,7 @@ def test_get_inverters_fake(client, fake): inverters = Inverters(**response.json()) assert len(inverters.inverters) > 0 + # def test_get_inverters(db_session, client, forecast_values): # response = client.get(f"/sites/{site_uuid}/clearsky_estimate") # assert response.status_code == 200 From ae4a4049d3794909ef8b9ee40441c30b7eb83155 Mon Sep 17 00:00:00 2001 From: Anya Parekh <49364484+anyaparekh@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:13:51 -0500 Subject: [PATCH 07/23] Update pv_site_api/main.py Co-authored-by: Andrew Lester --- pv_site_api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 214aeac..608bcb9 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -409,7 +409,7 @@ async def get_inverters_by_site( ): inverter_ids = [inverter.inverter_uuid for inverter in _get_inverters_by_site(site_uuid)] - return get_inverters_helper(session, inverter_ids) + return await get_inverters_helper(session, inverter_ids) # get_status: get the status of the system From 91cabb46df905b89781d42e9ac37d26d3382a8b7 Mon Sep 17 00:00:00 2001 From: Anya Parekh <49364484+anyaparekh@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:14:59 -0500 Subject: [PATCH 08/23] Update pv_site_api/main.py Co-authored-by: Andrew Lester --- pv_site_api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 608bcb9..134c05b 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -399,7 +399,7 @@ async def get_inverters( ).json() inverter_ids = [str(value) for value in r] - return get_inverters_helper(session, inverter_ids) + return await get_inverters_helper(session, inverter_ids) @app.get("/sites/{site_uuid}/inverters") From 05c4a212fa5a34bc8697516cfff1ae0e9bddea4c Mon Sep 17 00:00:00 2001 From: Anya Parekh <49364484+anyaparekh@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:15:29 -0500 Subject: [PATCH 09/23] Update pv_site_api/main.py Co-authored-by: Andrew Lester --- pv_site_api/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 134c05b..ec8c9d2 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -372,6 +372,7 @@ async def get_inverters_helper(session, inverter_ids): headers = {"Enode-User-Id": client.client_uuid} if not inverter_ids.length: return None + inverters_raw = await asyncio.gather( *[ httpxClient.get( From 459d6b3136ff38ae5bd79ab37b9a528a62016b12 Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Thu, 20 Apr 2023 15:22:04 -0500 Subject: [PATCH 10/23] Address PR comments --- .vscode/settings.json | 3 - poetry.lock | 213 +++++++++++++++++---------------- pv_site_api/fake.py | 16 +-- pv_site_api/main.py | 41 ++----- pv_site_api/pydantic_models.py | 2 +- pv_site_api/utils.py | 26 ++++ pyproject.toml | 3 +- tests/conftest.py | 4 + tests/test_inverters.py | 13 +- 9 files changed, 164 insertions(+), 157 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index de288e1..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.formatting.provider": "black" -} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index cf177a5..ba9d70a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -515,14 +515,14 @@ numpy = ">=1.14.5" [[package]] name = "httpcore" -version = "0.16.3" +version = "0.17.0" description = "A minimal low-level HTTP client." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-0.17.0-py3-none-any.whl", hash = "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599"}, + {file = "httpcore-0.17.0.tar.gz", hash = "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"}, ] [package.dependencies] @@ -591,25 +591,25 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.24.0" description = "The next generation HTTP client." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, + {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, ] [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = ">=0.15.0,<0.18.0" +idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] @@ -1085,7 +1085,7 @@ sqlalchemy = "1.4.46" type = "git" url = "https://github.com/openclimatefix/pv-site-datamodel.git" reference = "enode-updates" -resolved_reference = "211241af267c4bdaeb32009ede7228ec911845c9" +resolved_reference = "ac0735f277944c2c283e45e6970b2b9f5c5264de" [[package]] name = "pydantic" @@ -1142,14 +1142,14 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygments" -version = "2.15.0" +version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, - {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, ] [package.extras] @@ -1197,6 +1197,25 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-httpx" +version = "0.22.0" +description = "Send responses to httpx." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_httpx-0.22.0-py3-none-any.whl", hash = "sha256:cefb7dcf66a4cb0601b0de05e576cca423b6081f3245e7912a4d84c58fa3eae8"}, + {file = "pytest_httpx-0.22.0.tar.gz", hash = "sha256:3a82797f3a9a14d51e8c6b7fa97524b68b847ee801109c062e696b4744f4431c"}, +] + +[package.dependencies] +httpx = ">=0.24.0,<0.25.0" +pytest = ">=6.0,<8.0" + +[package.extras] +testing = ["pytest-asyncio (>=0.20.0,<0.21.0)", "pytest-cov (>=4.0.0,<5.0.0)"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1335,24 +1354,6 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - [[package]] name = "ruff" version = "0.0.253" @@ -1421,14 +1422,14 @@ test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "sciki [[package]] name = "sentry-sdk" -version = "1.19.1" +version = "1.20.0" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.19.1.tar.gz", hash = "sha256:7ae78bd921981a5010ab540d6bdf3b793659a4db8cccf7f16180702d48a80d84"}, - {file = "sentry_sdk-1.19.1-py2.py3-none-any.whl", hash = "sha256:885a11c69df23e53eb281d003b9ff15a5bdfa43d8a2a53589be52104a1b4582f"}, + {file = "sentry-sdk-1.20.0.tar.gz", hash = "sha256:a3410381ae769a436c0852cce140a5e5e49f566a07fb7c2ab445af1302f6ad89"}, + {file = "sentry_sdk-1.20.0-py2.py3-none-any.whl", hash = "sha256:0ad6bbbe78057b8031a07de7aca6d2a83234e51adc4d436eaf8d8c697184db71"}, ] [package.dependencies] @@ -1823,82 +1824,82 @@ test = ["websockets"] [[package]] name = "websockets" -version = "11.0.1" +version = "11.0.2" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "websockets-11.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d30cc1a90bcbf9e22e1f667c1c5a7428e2d37362288b4ebfd5118eb0b11afa9"}, - {file = "websockets-11.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc77283a7c7b2b24e00fe8c3c4f7cf36bba4f65125777e906aae4d58d06d0460"}, - {file = "websockets-11.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0929c2ebdf00cedda77bf77685693e38c269011236e7c62182fee5848c29a4fa"}, - {file = "websockets-11.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db234da3aff01e8483cf0015b75486c04d50dbf90004bd3e5b46d384e1bd6c9e"}, - {file = "websockets-11.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7fdfbed727ce6b4b5e6622d15a6efb2098b2d9e22ba4dc54b2e3ce80f982045"}, - {file = "websockets-11.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5f3d0d177b3db3d1d02cce7ba6c0063586499ac28afe0c992be74ffc40d9257"}, - {file = "websockets-11.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25ea5dbd3b00c56b034639dc6fe4d1dd095b8205bab1782d9a47cb020695fdf4"}, - {file = "websockets-11.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dbeada3b8f1f6d9497840f761906c4236f912a42da4515520168bc7c525b52b0"}, - {file = "websockets-11.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:892959b627eedcdf98ac7022f9f71f050a59624b380b67862da10c32ea3c221a"}, - {file = "websockets-11.0.1-cp310-cp310-win32.whl", hash = "sha256:fc0a96a6828bfa6f1ccec62b54630bcdcc205d483f5a8806c0a8abb26101c54b"}, - {file = "websockets-11.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:3a88375b648a2c479532943cc19a018df1e5fcea85d5f31963c0b22794d1bdc1"}, - {file = "websockets-11.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3cf18bbd44b36749b7b66f047a30a40b799b8c0bd9a1b9173cba86a234b4306b"}, - {file = "websockets-11.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:deb0dd98ea4e76b833f0bfd7a6042b51115360d5dfcc7c1daa72dfc417b3327a"}, - {file = "websockets-11.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45a85dc6b3ff76239379feb4355aadebc18d6e587c8deb866d11060755f4d3ea"}, - {file = "websockets-11.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d68bd2a3e9fff6f7043c0a711cb1ebba9f202c196a3943d0c885650cd0b6464"}, - {file = "websockets-11.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfd0b9b18d64c51e5cd322e16b5bf4fe490db65c9f7b18fd5382c824062ead7e"}, - {file = "websockets-11.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0e6253c36e42f2637cfa3ff9b3903df60d05ec040c718999f6a0644ce1c497"}, - {file = "websockets-11.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:12180bc1d72c6a9247472c1dee9dfd7fc2e23786f25feee7204406972d8dab39"}, - {file = "websockets-11.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a797da96d4127e517a5cb0965cd03fd6ec21e02667c1258fa0579501537fbe5c"}, - {file = "websockets-11.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:07cc20655fb16aeef1a8f03236ba8671c61d332580b996b6396a5b7967ba4b3d"}, - {file = "websockets-11.0.1-cp311-cp311-win32.whl", hash = "sha256:a01c674e0efe0f14aec7e722ed0e0e272fa2f10e8ea8260837e1f4f5dc4b3e53"}, - {file = "websockets-11.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:2796f097841619acf053245f266a4f66cb27c040f0d9097e5f21301aab95ff43"}, - {file = "websockets-11.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:54d084756c50dfc8086dce97b945f210ca43950154e1e04a44a30c6e6a2bcbb1"}, - {file = "websockets-11.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe2aed5963ca267c40a2d29b1ee4e8ab008ac8d5daa284fdda9275201b8a334"}, - {file = "websockets-11.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e92dbac318a84fef722f38ca57acef19cbb89527aba5d420b96aa2656970ee"}, - {file = "websockets-11.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec4e87eb9916b481216b1fede7d8913be799915f5216a0c801867cbed8eeb903"}, - {file = "websockets-11.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d4e0990b6a04b07095c969969da659eecf9069cf8e7b8f49c8f5ee1bb50e3352"}, - {file = "websockets-11.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c90343fd0774749d23c1891dd8b3e9210f9afd30986673ce0f9d5857f5cb1562"}, - {file = "websockets-11.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ac042e8ba9d7f2618e84af27927fdce0f3e03528eb74f343977486c093868389"}, - {file = "websockets-11.0.1-cp37-cp37m-win32.whl", hash = "sha256:385c5391becb9b58e0a4f33345e12762fd857ccf9fbf6fee428669929ba45e4c"}, - {file = "websockets-11.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:aef1602db81096ce3d3847865128c8879635bdad7963fb2b7df290edb9e9150a"}, - {file = "websockets-11.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:52ba83ea132390e426f9a7b48848248a2dc0e7120ca8c65d5a8fc1efaa4eb51b"}, - {file = "websockets-11.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:007ed0d62f7e06eeb6e3a848b0d83b9fbd9e14674a59a61326845f27d20d7452"}, - {file = "websockets-11.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f888b9565ca1d1c25ab827d184f57f4772ffbfa6baf5710b873b01936cc335ee"}, - {file = "websockets-11.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db78535b791840a584c48cf3f4215eae38a7e2f43271ecd27ce4ba8a798beaaa"}, - {file = "websockets-11.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa1c23ed3a02732fba906ec337df65d4cc23f9f453635e1a803c285b59c7d987"}, - {file = "websockets-11.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e039f106d48d3c241f1943bccfb383bd38ec39900d6dcaad0c73cc5fe129f346"}, - {file = "websockets-11.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0ed24a3aa4213029e100257e5e73c5f912e70ca35630081de94b7f9e2cf4a9b"}, - {file = "websockets-11.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5a3022f9291bf2d35ebf65929297d625e68effd3a5647b8eb8b89d51b09394c"}, - {file = "websockets-11.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1cb23597819f68ac6a6d133a002a1b3ef12a22850236b083242c93f81f206d5a"}, - {file = "websockets-11.0.1-cp38-cp38-win32.whl", hash = "sha256:349dd1fa56a30d530555988be98013688de67809f384671883f8bf8b8c9de984"}, - {file = "websockets-11.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:87ae582cf2319e45bc457a57232daded27a3c771263cab42fb8864214bbd74ea"}, - {file = "websockets-11.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a88815a0c6253ad1312ef186620832fb347706c177730efec34e3efe75e0e248"}, - {file = "websockets-11.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5a6fa353b5ef36970c3bd1cd7cecbc08bb8f2f1a3d008b0691208cf34ebf5b0"}, - {file = "websockets-11.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5ffe6fc5e5fe9f2634cdc59b805e4ba1fcccf3a5622f5f36c3c7c287f606e283"}, - {file = "websockets-11.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b138f4bf8a64c344e12c76283dac279d11adab89ac62ae4a32ac8490d3c94832"}, - {file = "websockets-11.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aedd94422745da60672a901f53de1f50b16e85408b18672b9b210db4a776b5a6"}, - {file = "websockets-11.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4667d4e41fa37fa3d836b2603b8b40d6887fa4838496d48791036394f7ace39"}, - {file = "websockets-11.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:43e0de552be624e5c0323ff4fcc9f0b4a9a6dc6e0116b8aa2cbb6e0d3d2baf09"}, - {file = "websockets-11.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ceeef57b9aec8f27e523de4da73c518ece7721aefe7064f18aa28baabfe61b94"}, - {file = "websockets-11.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9d91279d57f6546eaf43671d1de50621e0578f13c2f17c96c458a72d170698d7"}, - {file = "websockets-11.0.1-cp39-cp39-win32.whl", hash = "sha256:29282631da3bfeb5db497e4d3d94d56ee36222fbebd0b51014e68a2e70736fb1"}, - {file = "websockets-11.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e2654e94c705ce9b768441d8e3a387a84951ca1056efdc4a26a4a6ee723c01b6"}, - {file = "websockets-11.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:60a19d4ff5f451254f8623f6aa4169065f73a50ec7b59ab6b9dcddff4aa00267"}, - {file = "websockets-11.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a58e83f82098d062ae5d4cbe7073b8783999c284d6f079f2fefe87cd8957ac8"}, - {file = "websockets-11.0.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b91657b65355954e47f0df874917fa200426b3a7f4e68073326a8cfc2f6deef8"}, - {file = "websockets-11.0.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b8e1ee01eb5b8be5c8a69ae26b0820dbc198d092ad50b3451adc3cdd55d455"}, - {file = "websockets-11.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5d8d5d17371ed9eb9f0e3a8d326bdf8172700164c2e705bc7f1905a719a189be"}, - {file = "websockets-11.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e53419201c6c1439148feb99de6b307651a88b8defd41348cc23bbe2a290de1d"}, - {file = "websockets-11.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718d19c494637f28e651031b3df6a791b9e86e0097c65ed5e8ec49b400b1210e"}, - {file = "websockets-11.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b2544eb3e7bc39ce59812371214cd97762080dab90c3afc857890039384753"}, - {file = "websockets-11.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec4a887d2236e3878c07033ad5566f6b4d5d954b85f92a219519a1745d0c93e9"}, - {file = "websockets-11.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ae59a9f0a77ecb0cbdedea7d206a547ff136e8bfbc7d2d98772fb02d398797bb"}, - {file = "websockets-11.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ef35cef161f76031f833146f895e7e302196e01c704c00d269c04d8e18f3ac37"}, - {file = "websockets-11.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79b6548e57ab18f071b9bfe3ffe02af7184dd899bc674e2817d8fe7e9e7489ec"}, - {file = "websockets-11.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d9793f3fb0da16232503df14411dabafed5a81fc9077dc430cfc6f60e71179"}, - {file = "websockets-11.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42aa05e890fcf1faed8e535c088a1f0f27675827cbacf62d3024eb1e6d4c9e0c"}, - {file = "websockets-11.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5d4f4b341100d313b08149d7031eb6d12738ac758b0c90d2f9be8675f401b019"}, - {file = "websockets-11.0.1-py3-none-any.whl", hash = "sha256:85b4127f7da332feb932eee833c70e5e1670469e8c9de7ef3874aa2a91a6fbb2"}, - {file = "websockets-11.0.1.tar.gz", hash = "sha256:369410925b240b30ef1c1deadbd6331e9cd865ad0b8966bf31e276cc8e0da159"}, + {file = "websockets-11.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:580cc95c58118f8c39106be71e24d0b7e1ad11a155f40a2ee687f99b3e5e432e"}, + {file = "websockets-11.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:143782041e95b63083b02107f31cda999f392903ae331de1307441f3a4557d51"}, + {file = "websockets-11.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8df63dcd955eb6b2e371d95aacf8b7c535e482192cff1b6ce927d8f43fb4f552"}, + {file = "websockets-11.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9b2dced5cbbc5094678cc1ec62160f7b0fe4defd601cd28a36fde7ee71bbb5"}, + {file = "websockets-11.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0eeeea3b01c97fd3b5049a46c908823f68b59bf0e18d79b231d8d6764bc81ee"}, + {file = "websockets-11.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:502683c5dedfc94b9f0f6790efb26aa0591526e8403ad443dce922cd6c0ec83b"}, + {file = "websockets-11.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3cc3e48b6c9f7df8c3798004b9c4b92abca09eeea5e1b0a39698f05b7a33b9d"}, + {file = "websockets-11.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:808b8a33c961bbd6d33c55908f7c137569b09ea7dd024bce969969aa04ecf07c"}, + {file = "websockets-11.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:34a6f8996964ccaa40da42ee36aa1572adcb1e213665e24aa2f1037da6080909"}, + {file = "websockets-11.0.2-cp310-cp310-win32.whl", hash = "sha256:8f24cd758cbe1607a91b720537685b64e4d39415649cac9177cd1257317cf30c"}, + {file = "websockets-11.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:3b87cd302f08ea9e74fdc080470eddbed1e165113c1823fb3ee6328bc40ca1d3"}, + {file = "websockets-11.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3565a8f8c7bdde7c29ebe46146bd191290413ee6f8e94cf350609720c075b0a1"}, + {file = "websockets-11.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f97e03d4d5a4f0dca739ea274be9092822f7430b77d25aa02da6775e490f6846"}, + {file = "websockets-11.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f392587eb2767afa8a34e909f2fec779f90b630622adc95d8b5e26ea8823cb8"}, + {file = "websockets-11.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7742cd4524622cc7aa71734b51294644492a961243c4fe67874971c4d3045982"}, + {file = "websockets-11.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46dda4bc2030c335abe192b94e98686615f9274f6b56f32f2dd661fb303d9d12"}, + {file = "websockets-11.0.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6b2bfa1d884c254b841b0ff79373b6b80779088df6704f034858e4d705a4802"}, + {file = "websockets-11.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1df2413266bf48430ef2a752c49b93086c6bf192d708e4a9920544c74cd2baa6"}, + {file = "websockets-11.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf45d273202b0c1cec0f03a7972c655b93611f2e996669667414557230a87b88"}, + {file = "websockets-11.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a09cce3dacb6ad638fdfa3154d9e54a98efe7c8f68f000e55ca9c716496ca67"}, + {file = "websockets-11.0.2-cp311-cp311-win32.whl", hash = "sha256:2174a75d579d811279855df5824676d851a69f52852edb0e7551e0eeac6f59a4"}, + {file = "websockets-11.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:c78ca3037a954a4209b9f900e0eabbc471fb4ebe96914016281df2c974a93e3e"}, + {file = "websockets-11.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2100b02d1aaf66dc48ff1b2a72f34f6ebc575a02bc0350cc8e9fbb35940166"}, + {file = "websockets-11.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dca9708eea9f9ed300394d4775beb2667288e998eb6f542cdb6c02027430c599"}, + {file = "websockets-11.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:320ddceefd2364d4afe6576195201a3632a6f2e6d207b0c01333e965b22dbc84"}, + {file = "websockets-11.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2a573c8d71b7af937852b61e7ccb37151d719974146b5dc734aad350ef55a02"}, + {file = "websockets-11.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:13bd5bebcd16a4b5e403061b8b9dcc5c77e7a71e3c57e072d8dff23e33f70fba"}, + {file = "websockets-11.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:95c09427c1c57206fe04277bf871b396476d5a8857fa1b99703283ee497c7a5d"}, + {file = "websockets-11.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2eb042734e710d39e9bc58deab23a65bd2750e161436101488f8af92f183c239"}, + {file = "websockets-11.0.2-cp37-cp37m-win32.whl", hash = "sha256:5875f623a10b9ba154cb61967f940ab469039f0b5e61c80dd153a65f024d9fb7"}, + {file = "websockets-11.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:634239bc844131863762865b75211a913c536817c0da27f691400d49d256df1d"}, + {file = "websockets-11.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3178d965ec204773ab67985a09f5696ca6c3869afeed0bb51703ea404a24e975"}, + {file = "websockets-11.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:955fcdb304833df2e172ce2492b7b47b4aab5dcc035a10e093d911a1916f2c87"}, + {file = "websockets-11.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb46d2c7631b2e6f10f7c8bac7854f7c5e5288f024f1c137d4633c79ead1e3c0"}, + {file = "websockets-11.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25aae96c1060e85836552a113495db6d857400288161299d77b7b20f2ac569f2"}, + {file = "websockets-11.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2abeeae63154b7f63d9f764685b2d299e9141171b8b896688bd8baec6b3e2303"}, + {file = "websockets-11.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daa1e8ea47507555ed7a34f8b49398d33dff5b8548eae3de1dc0ef0607273a33"}, + {file = "websockets-11.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:954eb789c960fa5daaed3cfe336abc066941a5d456ff6be8f0e03dd89886bb4c"}, + {file = "websockets-11.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3ffe251a31f37e65b9b9aca5d2d67fd091c234e530f13d9dce4a67959d5a3fba"}, + {file = "websockets-11.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:adf6385f677ed2e0b021845b36f55c43f171dab3a9ee0ace94da67302f1bc364"}, + {file = "websockets-11.0.2-cp38-cp38-win32.whl", hash = "sha256:aa7b33c1fb2f7b7b9820f93a5d61ffd47f5a91711bc5fa4583bbe0c0601ec0b2"}, + {file = "websockets-11.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:220d5b93764dd70d7617f1663da64256df7e7ea31fc66bc52c0e3750ee134ae3"}, + {file = "websockets-11.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fb4480556825e4e6bf2eebdbeb130d9474c62705100c90e59f2f56459ddab42"}, + {file = "websockets-11.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec00401846569aaf018700249996143f567d50050c5b7b650148989f956547af"}, + {file = "websockets-11.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87c69f50281126dcdaccd64d951fb57fbce272578d24efc59bce72cf264725d0"}, + {file = "websockets-11.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:232b6ba974f5d09b1b747ac232f3a3d8f86de401d7b565e837cc86988edf37ac"}, + {file = "websockets-11.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392d409178db1e46d1055e51cc850136d302434e12d412a555e5291ab810f622"}, + {file = "websockets-11.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4fe2442091ff71dee0769a10449420fd5d3b606c590f78dd2b97d94b7455640"}, + {file = "websockets-11.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ede13a6998ba2568b21825809d96e69a38dc43184bdeebbde3699c8baa21d015"}, + {file = "websockets-11.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4c54086b2d2aec3c3cb887ad97e9c02c6be9f1d48381c7419a4aa932d31661e4"}, + {file = "websockets-11.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e37a76ccd483a6457580077d43bc3dfe1fd784ecb2151fcb9d1c73f424deaeba"}, + {file = "websockets-11.0.2-cp39-cp39-win32.whl", hash = "sha256:d1881518b488a920434a271a6e8a5c9481a67c4f6352ebbdd249b789c0467ddc"}, + {file = "websockets-11.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:25e265686ea385f22a00cc2b719b880797cd1bb53b46dbde969e554fb458bfde"}, + {file = "websockets-11.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce69f5c742eefd039dce8622e99d811ef2135b69d10f9aa79fbf2fdcc1e56cd7"}, + {file = "websockets-11.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b985ba2b9e972cf99ddffc07df1a314b893095f62c75bc7c5354a9c4647c6503"}, + {file = "websockets-11.0.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b52def56d2a26e0e9c464f90cadb7e628e04f67b0ff3a76a4d9a18dfc35e3dd"}, + {file = "websockets-11.0.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70a438ef2a22a581d65ad7648e949d4ccd20e3c8ed7a90bbc46df4e60320891"}, + {file = "websockets-11.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:752fbf420c71416fb1472fec1b4cb8631c1aa2be7149e0a5ba7e5771d75d2bb9"}, + {file = "websockets-11.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd906b0cdc417ea7a5f13bb3c6ca3b5fd563338dc596996cb0fdd7872d691c0a"}, + {file = "websockets-11.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e79065ff6549dd3c765e7916067e12a9c91df2affea0ac51bcd302aaf7ad207"}, + {file = "websockets-11.0.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46388a050d9e40316e58a3f0838c63caacb72f94129eb621a659a6e49bad27ce"}, + {file = "websockets-11.0.2-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7de298371d913824f71b30f7685bb07ad13969c79679cca5b1f7f94fec012f"}, + {file = "websockets-11.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6d872c972c87c393e6a49c1afbdc596432df8c06d0ff7cd05aa18e885e7cfb7c"}, + {file = "websockets-11.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b444366b605d2885f0034dd889faf91b4b47668dd125591e2c64bfde611ac7e1"}, + {file = "websockets-11.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b967a4849db6b567dec3f7dd5d97b15ce653e3497b8ce0814e470d5e074750"}, + {file = "websockets-11.0.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2acdc82099999e44fa7bd8c886f03c70a22b1d53ae74252f389be30d64fd6004"}, + {file = "websockets-11.0.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:518ed6782d9916c5721ebd61bb7651d244178b74399028302c8617d0620af291"}, + {file = "websockets-11.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:58477b041099bb504e1a5ddd8aa86302ed1d5c6995bdd3db2b3084ef0135d277"}, + {file = "websockets-11.0.2-py3-none-any.whl", hash = "sha256:5004c087d17251938a52cce21b3dbdabeecbbe432ce3f5bbbf15d8692c36eac9"}, + {file = "websockets-11.0.2.tar.gz", hash = "sha256:b1a69701eb98ed83dd099de4a686dc892c413d974fa31602bc00aca7cb988ac9"}, ] [[package]] @@ -1989,4 +1990,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7d40ec6ff03dfcba06a62d9876081b3814157dad550e82e336448b646310f05c" +content-hash = "c97b76c8280335ca70d6e6bb755328db3b0c1c6f10e8abca9e43bf5709e32a77" diff --git a/pv_site_api/fake.py b/pv_site_api/fake.py index 6dd3659..5a9fa9a 100644 --- a/pv_site_api/fake.py +++ b/pv_site_api/fake.py @@ -27,18 +27,18 @@ def make_fake_inverters() -> Inverters: """Make fake inverters""" inverter = InverterValues( - id="0", - vendor="Test", - chargingLocationId="0", - lastSeen="never", - isReachable="false", + id="string", + vendor="EMA", + chargingLocationId="8d90101b-3f2f-462a-bbb4-1ed320d33bbe", + lastSeen="2020-04-07T17:04:26Z", + isReachable=True, productionState=InverterProductionState( - productionRate=0.1, isProducing=False, totalLifetimeProduction=0.5, lastUpdated="never" + productionRate=0, isProducing=True, totalLifetimeProduction=100152.56, lastUpdated="2020-04-07T17:04:26Z" ), information=InverterInformation( - id="0", brand="tesla", model="x", siteName="ocf", installationDate="1-23-4567" + id="string", brand="EMA", model="Sunny Boy", siteName="Sunny Plant", installationDate="2020-04-07T17:04:26Z" ), - location=InverterLocation(latitude=0.0, longitude=0.1), + location=InverterLocation(latitude=10.7197486, longitude=59.9173985), ) inverters_list = Inverters( inverters=[inverter], diff --git a/pv_site_api/main.py b/pv_site_api/main.py index ec8c9d2..2b0000a 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -1,5 +1,4 @@ """Main API Routes""" -import asyncio import logging import os @@ -38,8 +37,6 @@ from .pydantic_models import ( ClearskyEstimate, Forecast, - Inverters, - InverterValues, MultiplePVActual, PVSiteAPIStatus, PVSiteMetadata, @@ -47,7 +44,7 @@ ) from .redoc_theme import get_redoc_html_with_theme from .session import get_session -from .utils import get_yesterday_midnight +from .utils import get_yesterday_midnight, get_inverters_list load_dotenv() @@ -360,36 +357,13 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess res = {"clearsky_estimate": pac.to_dict("records")} return res - -async def get_inverters_helper(session, inverter_ids): - if int(os.environ["FAKE"]): - return make_fake_inverters() - - client = session.query(ClientSQL).first() - assert client is not None - - async with httpx.AsyncClient() as httpxClient: - headers = {"Enode-User-Id": client.client_uuid} - if not inverter_ids.length: - return None - - inverters_raw = await asyncio.gather( - *[ - httpxClient.get( - f"https://enode-api.production.enode.io/inverters/{id}", headers=headers - ) - for id in inverter_ids - ] - ) - inverters = [InverterValues(**inverter_raw.json()) for inverter_raw in inverters_raw] - - return Inverters(inverters) - - @app.get("/inverters") async def get_inverters( session: Session = Depends(get_session), ): + if int(os.environ["FAKE"]): + return make_fake_inverters() + client = session.query(ClientSQL).first() assert client is not None @@ -400,7 +374,7 @@ async def get_inverters( ).json() inverter_ids = [str(value) for value in r] - return await get_inverters_helper(session, inverter_ids) + return await get_inverters_list(session, inverter_ids) @app.get("/sites/{site_uuid}/inverters") @@ -408,9 +382,12 @@ async def get_inverters_by_site( site_uuid: str, session: Session = Depends(get_session), ): + if int(os.environ["FAKE"]): + return make_fake_inverters() + inverter_ids = [inverter.inverter_uuid for inverter in _get_inverters_by_site(site_uuid)] - return await get_inverters_helper(session, inverter_ids) + return await get_inverters_list(session, inverter_ids) # get_status: get the status of the system diff --git a/pv_site_api/pydantic_models.py b/pv_site_api/pydantic_models.py index 748f646..a579f1a 100644 --- a/pv_site_api/pydantic_models.py +++ b/pv_site_api/pydantic_models.py @@ -158,7 +158,7 @@ class InverterValues(BaseModel): lastSeen: str = Field( ..., description="The last time the solar inverter was successfully communicated with" ) - isReachable: str = Field( + isReachable: bool = Field( ..., description="Whether live data from the solar inverter is currently reachable from Enode.", ) diff --git a/pv_site_api/utils.py b/pv_site_api/utils.py index 149d46c..011daec 100644 --- a/pv_site_api/utils.py +++ b/pv_site_api/utils.py @@ -1,10 +1,36 @@ """ make fake intensity""" import math +import httpx +import asyncio from datetime import datetime, timedelta, timezone from typing import List +from pvsite_datamodel.sqlmodels import ClientSQL + +from .pydantic_models import ( + Inverters, + InverterValues, +) TOTAL_MINUTES_IN_ONE_DAY = 24 * 60 +async def get_inverters_list(session, inverter_ids): + client = session.query(ClientSQL).first() + assert client is not None + + async with httpx.AsyncClient() as httpxClient: + headers = {"Enode-User-Id": client.client_uuid} + inverters_raw = await asyncio.gather( + *[ + httpxClient.get( + f"https://enode-api.production.enode.io/inverters/{id}", headers=headers + ) + for id in inverter_ids + ] + ) + + inverters = [InverterValues(**inverter_raw.json()) for inverter_raw in inverters_raw] + + return Inverters(inverters) def make_fake_intensity(datetime_utc: datetime) -> float: """ diff --git a/pyproject.toml b/pyproject.toml index b39f921..ae67890 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ psycopg2-binary = "^2.9.5" sqlalchemy = "^1.4.46" pvsite-datamodel = { git = "https://github.com/openclimatefix/pv-site-datamodel.git", branch = "enode-updates" } fastapi = "^0.92.0" -httpx = "^0.23.3" +httpx = "^0.24.0" sentry-sdk = "^1.16.0" pvlib = "^0.9.5" @@ -26,6 +26,7 @@ pytest = "^7.2.1" pytest-cov = "^4.0.0" testcontainers-postgres = "^0.0.1rc1" ipython = "^8.11.0" +pytest-httpx = "^0.22.0" [build-system] requires = ["poetry-core"] diff --git a/tests/conftest.py b/tests/conftest.py index 4c55a05..5fc3052 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,10 @@ from pv_site_api.session import get_session +@pytest.fixture +def non_mocked_hosts() -> list: + return ["testserver"] + @pytest.fixture(scope="session") def engine(): """Make database engine""" diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 9d9c967..08e5b2a 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -1,6 +1,7 @@ """ Test for main app """ from pv_site_api.pydantic_models import Inverters +import httpx def test_get_inverters_fake(client, fake): @@ -11,9 +12,9 @@ def test_get_inverters_fake(client, fake): assert len(inverters.inverters) > 0 -# def test_get_inverters(db_session, client, forecast_values): -# response = client.get(f"/sites/{site_uuid}/clearsky_estimate") -# assert response.status_code == 200 - -# clearsky_estimate = ClearskyEstimate(**response.json()) -# assert len(clearsky_estimate.clearsky_estimate) > 0 +def test_get_inverters(httpx_mock): + httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters") + + with httpx.Client() as client: + response = client.get("https://enode-api.production.enode.io/inverters") + assert response.status_code == 200 From 4ac13f64e545713ed33cf52237e4203230aaf0a1 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 18:40:58 -0500 Subject: [PATCH 11/23] Update pyproject.toml --- .vscode/settings.json | 3 +++ poetry.lock | 22 +++++++++------------- pyproject.toml | 2 +- tests/conftest.py | 4 ++-- 4 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de288e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index ba9d70a..53a611c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1068,25 +1068,21 @@ test = ["pytest", "pytest-cov", "pytest-mock", "pytest-remotedata", "pytest-reru [[package]] name = "pvsite-datamodel" -version = "0.1.32" +version = "0.1.33" description = "SDK for interacting with the PVSite database" category = "main" optional = false -python-versions = "^3.10" -files = [] -develop = false +python-versions = ">=3.10,<4.0" +files = [ + {file = "pvsite_datamodel-0.1.33-py3-none-any.whl", hash = "sha256:f24968bef98fe6702df264826f855e517fab7e1fd818aa5cbd00e19a9c645862"}, + {file = "pvsite_datamodel-0.1.33.tar.gz", hash = "sha256:fd78605f8fa3f9d6b3082db678d12dc4cdadf5b1a6ac195b56d73364de68af88"}, +] [package.dependencies] -pandas = "^1.5.3" -psycopg2-binary = "^2.9.5" +pandas = ">=1.5.3,<2.0.0" +psycopg2-binary = ">=2.9.5,<3.0.0" sqlalchemy = "1.4.46" -[package.source] -type = "git" -url = "https://github.com/openclimatefix/pv-site-datamodel.git" -reference = "enode-updates" -resolved_reference = "ac0735f277944c2c283e45e6970b2b9f5c5264de" - [[package]] name = "pydantic" version = "1.10.7" @@ -1990,4 +1986,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c97b76c8280335ca70d6e6bb755328db3b0c1c6f10e8abca9e43bf5709e32a77" +content-hash = "8ab5d4f3fe5ce9195ff51416052fa5c74112a6bb6974fc5a6136fabbae8ba426" diff --git a/pyproject.toml b/pyproject.toml index ae67890..773b965 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ pydantic = "^1.10.5" uvicorn = {extras = ["standard"], version = "^0.20.0"} psycopg2-binary = "^2.9.5" sqlalchemy = "^1.4.46" -pvsite-datamodel = { git = "https://github.com/openclimatefix/pv-site-datamodel.git", branch = "enode-updates" } +pvsite-datamodel = "^0.1.33" fastapi = "^0.92.0" httpx = "^0.24.0" sentry-sdk = "^1.16.0" diff --git a/tests/conftest.py b/tests/conftest.py index 5fc3052..bb48332 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,8 +21,8 @@ from pv_site_api.session import get_session -@pytest.fixture -def non_mocked_hosts() -> list: +@pytest.fixture() +def non_mocked_hosts(): return ["testserver"] @pytest.fixture(scope="session") From 45609e27dac1c85819e5c655f530a1db4c29a3ee Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 18:41:34 -0500 Subject: [PATCH 12/23] Delete settings.json --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index de288e1..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.formatting.provider": "black" -} \ No newline at end of file From e35f84e88aee37066709022adfd4e8a6da5c32f9 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 18:44:29 -0500 Subject: [PATCH 13/23] Fix lint errors --- pv_site_api/fake.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pv_site_api/fake.py b/pv_site_api/fake.py index 5a9fa9a..d50901d 100644 --- a/pv_site_api/fake.py +++ b/pv_site_api/fake.py @@ -33,10 +33,17 @@ def make_fake_inverters() -> Inverters: lastSeen="2020-04-07T17:04:26Z", isReachable=True, productionState=InverterProductionState( - productionRate=0, isProducing=True, totalLifetimeProduction=100152.56, lastUpdated="2020-04-07T17:04:26Z" + 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" + id="string", + brand="EMA", + model="Sunny Boy", + siteName="Sunny Plant", + installationDate="2020-04-07T17:04:26Z" ), location=InverterLocation(latitude=10.7197486, longitude=59.9173985), ) From 3b7c0378a39299c3a1a13865d8561ed6237e318e Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 18:48:52 -0500 Subject: [PATCH 14/23] Fix formatting --- pv_site_api/fake.py | 18 +++++++++--------- pv_site_api/main.py | 1 + pv_site_api/utils.py | 4 +++- tests/conftest.py | 1 + tests/test_inverters.py | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pv_site_api/fake.py b/pv_site_api/fake.py index d50901d..4d26b98 100644 --- a/pv_site_api/fake.py +++ b/pv_site_api/fake.py @@ -33,17 +33,17 @@ def make_fake_inverters() -> Inverters: lastSeen="2020-04-07T17:04:26Z", isReachable=True, productionState=InverterProductionState( - productionRate=0, - isProducing=True, - totalLifetimeProduction=100152.56, - lastUpdated="2020-04-07T17:04:26Z" + 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" + id="string", + brand="EMA", + model="Sunny Boy", + siteName="Sunny Plant", + installationDate="2020-04-07T17:04:26Z", ), location=InverterLocation(latitude=10.7197486, longitude=59.9173985), ) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 2b0000a..8adf095 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -357,6 +357,7 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess res = {"clearsky_estimate": pac.to_dict("records")} return res + @app.get("/inverters") async def get_inverters( session: Session = Depends(get_session), diff --git a/pv_site_api/utils.py b/pv_site_api/utils.py index 011daec..172125d 100644 --- a/pv_site_api/utils.py +++ b/pv_site_api/utils.py @@ -13,6 +13,7 @@ TOTAL_MINUTES_IN_ONE_DAY = 24 * 60 + async def get_inverters_list(session, inverter_ids): client = session.query(ClientSQL).first() assert client is not None @@ -27,11 +28,12 @@ async def get_inverters_list(session, inverter_ids): for id in inverter_ids ] ) - + inverters = [InverterValues(**inverter_raw.json()) for inverter_raw in inverters_raw] return Inverters(inverters) + def make_fake_intensity(datetime_utc: datetime) -> float: """ Make a fake intesnity value based on the time of the day diff --git a/tests/conftest.py b/tests/conftest.py index bb48332..4242836 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,7 @@ def non_mocked_hosts(): return ["testserver"] + @pytest.fixture(scope="session") def engine(): """Make database engine""" diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 08e5b2a..2423357 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -14,7 +14,7 @@ def test_get_inverters_fake(client, fake): def test_get_inverters(httpx_mock): httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters") - + with httpx.Client() as client: response = client.get("https://enode-api.production.enode.io/inverters") assert response.status_code == 200 From 16556d54bb2fd0f17fe8449db2518f423571fd44 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 18:51:13 -0500 Subject: [PATCH 15/23] Fix imports --- pv_site_api/main.py | 2 +- pv_site_api/utils.py | 10 ++++------ tests/test_inverters.py | 3 ++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 8adf095..3a6883e 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -44,7 +44,7 @@ ) from .redoc_theme import get_redoc_html_with_theme from .session import get_session -from .utils import get_yesterday_midnight, get_inverters_list +from .utils import get_inverters_list, get_yesterday_midnight load_dotenv() diff --git a/pv_site_api/utils.py b/pv_site_api/utils.py index 172125d..b81a3f7 100644 --- a/pv_site_api/utils.py +++ b/pv_site_api/utils.py @@ -1,15 +1,13 @@ """ make fake intensity""" -import math -import httpx import asyncio +import math from datetime import datetime, timedelta, timezone from typing import List + +import httpx from pvsite_datamodel.sqlmodels import ClientSQL -from .pydantic_models import ( - Inverters, - InverterValues, -) +from .pydantic_models import Inverters, InverterValues TOTAL_MINUTES_IN_ONE_DAY = 24 * 60 diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 2423357..6284b48 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -1,8 +1,9 @@ """ Test for main app """ -from pv_site_api.pydantic_models import Inverters import httpx +from pv_site_api.pydantic_models import Inverters + def test_get_inverters_fake(client, fake): response = client.get("/inverters") From 13fad4570636a2463b89f318487c840dc5a8be45 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 19:20:18 -0500 Subject: [PATCH 16/23] Fix bugs --- pv_site_api/main.py | 6 +++--- pv_site_api/utils.py | 4 ++-- tests/test_inverters.py | 41 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 3a6883e..51e091e 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -369,10 +369,10 @@ async def get_inverters( assert client is not None async with httpx.AsyncClient() as httpxClient: - headers = {"Enode-User-Id": client.client_uuid} - r = await httpxClient.get( + headers = {"Enode-User-Id": str(client.client_uuid)} + r = (await httpxClient.get( "https://enode-api.production.enode.io/inverters", headers=headers - ).json() + )).json() inverter_ids = [str(value) for value in r] return await get_inverters_list(session, inverter_ids) diff --git a/pv_site_api/utils.py b/pv_site_api/utils.py index b81a3f7..9730c25 100644 --- a/pv_site_api/utils.py +++ b/pv_site_api/utils.py @@ -17,7 +17,7 @@ async def get_inverters_list(session, inverter_ids): assert client is not None async with httpx.AsyncClient() as httpxClient: - headers = {"Enode-User-Id": client.client_uuid} + headers = {"Enode-User-Id": str(client.client_uuid)} inverters_raw = await asyncio.gather( *[ httpxClient.get( @@ -29,7 +29,7 @@ async def get_inverters_list(session, inverter_ids): inverters = [InverterValues(**inverter_raw.json()) for inverter_raw in inverters_raw] - return Inverters(inverters) + return Inverters(inverters=inverters) def make_fake_intensity(datetime_utc: datetime) -> float: diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 6284b48..0bf763a 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -2,7 +2,9 @@ import httpx -from pv_site_api.pydantic_models import Inverters +from pv_site_api.pydantic_models import ( + Inverters, +) def test_get_inverters_fake(client, fake): @@ -13,9 +15,38 @@ def test_get_inverters_fake(client, fake): assert len(inverters.inverters) > 0 -def test_get_inverters(httpx_mock): - httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters") +def test_get_inverters(client, httpx_mock, clients): + httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters", json=['id1']) + + httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters/id1", json= + { + "id": "string", + "vendor": "EMA", + "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", + "lastSeen": "2020-04-07T17:04:26Z", + "isReachable": True, + "productionState": { + "productionRate": 0, + "isProducing": True, + "totalLifetimeProduction": 100152.56, + "lastUpdated": "2020-04-07T17:04:26Z" + }, + "information": { + "id": "string", + "brand": "EMA", + "model": "Sunny Boy", + "siteName": "Sunny Plant", + "installationDate": "2020-04-07T17:04:26Z" + }, + "location": { + "longitude": 10.7197486, + "latitude": 59.9173985 + } + } + ) - with httpx.Client() as client: - response = client.get("https://enode-api.production.enode.io/inverters") + response = client.get("/inverters") assert response.status_code == 200 + + inverters = Inverters(**response.json()) + assert len(inverters.inverters) > 0 From 459bd23338d6ae384f5cc1c8d36be780ce8b4b6d Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 19:22:48 -0500 Subject: [PATCH 17/23] Remove unused import --- tests/test_inverters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 0bf763a..fa2e68b 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -1,7 +1,5 @@ """ Test for main app """ -import httpx - from pv_site_api.pydantic_models import ( Inverters, ) From 943d63bd70c4a6cb7430492a402cd11104919ccb Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 19:24:20 -0500 Subject: [PATCH 18/23] Fix formatting --- pv_site_api/main.py | 8 ++++--- tests/test_inverters.py | 48 ++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 51e091e..38f2568 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -370,9 +370,11 @@ async def get_inverters( async with httpx.AsyncClient() as httpxClient: headers = {"Enode-User-Id": str(client.client_uuid)} - r = (await httpxClient.get( - "https://enode-api.production.enode.io/inverters", headers=headers - )).json() + r = ( + await httpxClient.get( + "https://enode-api.production.enode.io/inverters", headers=headers + ) + ).json() inverter_ids = [str(value) for value in r] return await get_inverters_list(session, inverter_ids) diff --git a/tests/test_inverters.py b/tests/test_inverters.py index fa2e68b..83b606e 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -14,33 +14,31 @@ def test_get_inverters_fake(client, fake): def test_get_inverters(client, httpx_mock, clients): - httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters", json=['id1']) - - httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters/id1", json= - { - "id": "string", - "vendor": "EMA", - "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", - "lastSeen": "2020-04-07T17:04:26Z", - "isReachable": True, - "productionState": { - "productionRate": 0, - "isProducing": True, - "totalLifetimeProduction": 100152.56, - "lastUpdated": "2020-04-07T17:04:26Z" - }, - "information": { + httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters", json=["id1"]) + + httpx_mock.add_response( + url="https://enode-api.production.enode.io/inverters/id1", + json={ "id": "string", - "brand": "EMA", - "model": "Sunny Boy", - "siteName": "Sunny Plant", - "installationDate": "2020-04-07T17:04:26Z" + "vendor": "EMA", + "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", + "lastSeen": "2020-04-07T17:04:26Z", + "isReachable": True, + "productionState": { + "productionRate": 0, + "isProducing": True, + "totalLifetimeProduction": 100152.56, + "lastUpdated": "2020-04-07T17:04:26Z", + }, + "information": { + "id": "string", + "brand": "EMA", + "model": "Sunny Boy", + "siteName": "Sunny Plant", + "installationDate": "2020-04-07T17:04:26Z", + }, + "location": {"longitude": 10.7197486, "latitude": 59.9173985}, }, - "location": { - "longitude": 10.7197486, - "latitude": 59.9173985 - } - } ) response = client.get("/inverters") From f64dda2cd0cdfd62e45f354e97e66f2fa6e5c771 Mon Sep 17 00:00:00 2001 From: neha-vard Date: Thu, 20 Apr 2023 19:25:37 -0500 Subject: [PATCH 19/23] Fix import order --- tests/test_inverters.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 83b606e..f6b2f09 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -1,8 +1,6 @@ """ Test for main app """ -from pv_site_api.pydantic_models import ( - Inverters, -) +from pv_site_api.pydantic_models import Inverters def test_get_inverters_fake(client, fake): From 401ac7b2c8977381c7c0cf7521678ff7aeb32cfd Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Thu, 20 Apr 2023 20:35:40 -0500 Subject: [PATCH 20/23] Add inverters test for site specific --- pv_site_api/_db_helpers.py | 2 +- pv_site_api/main.py | 2 +- pv_site_api/pydantic_models.py | 14 +++--- pv_site_api/utils.py | 5 +- tests/conftest.py | 9 ++-- tests/test_inverters.py | 86 +++++++++++++++++++++++++++++++++- 6 files changed, 99 insertions(+), 19 deletions(-) diff --git a/pv_site_api/_db_helpers.py b/pv_site_api/_db_helpers.py index a907386..e4d418b 100644 --- a/pv_site_api/_db_helpers.py +++ b/pv_site_api/_db_helpers.py @@ -56,7 +56,7 @@ def _get_forecasts_for_horizon( def _get_inverters_by_site(session: Session, site_uuid: str) -> list[Row]: - query = session.query(InverterSQL).filter(InverterSQL.site_uuid.is_(site_uuid)) + query = session.query(InverterSQL).filter(InverterSQL.site_uuid == site_uuid) return query.all() diff --git a/pv_site_api/main.py b/pv_site_api/main.py index 38f2568..efdfd58 100644 --- a/pv_site_api/main.py +++ b/pv_site_api/main.py @@ -388,7 +388,7 @@ async def get_inverters_by_site( if int(os.environ["FAKE"]): return make_fake_inverters() - inverter_ids = [inverter.inverter_uuid for inverter in _get_inverters_by_site(site_uuid)] + inverter_ids = [inverter.client_id for inverter in _get_inverters_by_site(session, site_uuid)] return await get_inverters_list(session, inverter_ids) diff --git a/pv_site_api/pydantic_models.py b/pv_site_api/pydantic_models.py index a579f1a..3a4f0dd 100644 --- a/pv_site_api/pydantic_models.py +++ b/pv_site_api/pydantic_models.py @@ -114,12 +114,12 @@ class ClearskyEstimate(BaseModel): class InverterProductionState(BaseModel): """Production State data for an inverter""" - productionRate: float = Field(..., description="The current production rate in kW") - isProducing: bool = Field( + productionRate: Optional[float] = Field(..., description="The current production rate in kW") + isProducing: Optional[bool] = Field( ..., description="Whether the solar inverter is actively producing energy or not" ) - totalLifetimeProduction: float = Field(..., description="The total lifetime production in kWh") - lastUpdated: str = Field( + totalLifetimeProduction: Optional[float] = Field(..., description="The total lifetime production in kWh") + lastUpdated: Optional[str] = Field( ..., description="ISO8601 UTC timestamp of last received production state update" ) @@ -140,8 +140,8 @@ class InverterInformation(BaseModel): class InverterLocation(BaseModel): """ "Longitude and Latitude of inverter""" - longitude: float = Field(..., description="Longitude in degrees") - latitude: float = Field(..., description="Latitude in degrees") + longitude: Optional[float] = Field(..., description="Longitude in degrees") + latitude: Optional[float] = Field(..., description="Latitude in degrees") class InverterValues(BaseModel): @@ -151,7 +151,7 @@ class InverterValues(BaseModel): vendor: str = Field( ..., description="One of EMA ENPHASE FRONIUS GOODWE GROWATT HUAWEI SMA SOLAREDGE SOLIS" ) - chargingLocationId: str = Field( + chargingLocationId: Optional[str] = Field( ..., description="ID of the charging location the solar inverter is currently positioned at.", ) diff --git a/pv_site_api/utils.py b/pv_site_api/utils.py index 9730c25..5a81766 100644 --- a/pv_site_api/utils.py +++ b/pv_site_api/utils.py @@ -26,15 +26,14 @@ async def get_inverters_list(session, inverter_ids): for id in inverter_ids ] ) - - inverters = [InverterValues(**inverter_raw.json()) for inverter_raw in inverters_raw] + inverters = [InverterValues(**(inverter_raw.json())) for inverter_raw in inverters_raw] return Inverters(inverters=inverters) def make_fake_intensity(datetime_utc: datetime) -> float: """ - Make a fake intesnity value based on the time of the day + Make a fake intensity value based on the time of the day :param datetime_utc: :return: intensity, between 0 and 1 diff --git a/tests/conftest.py b/tests/conftest.py index 4242836..8c9d5d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,13 +97,12 @@ def sites(db_session, clients): @pytest.fixture() def inverters(db_session, sites): - """Create some fake inverters""" + """Create some fake inverters for site 0""" inverters = [] num_inverters = 3 - for site in sites: - for j in range(num_inverters): - inverter = InverterSQL(site_uuid=site.site_uuid, client_id="test") - inverters.append(inverter) + for j in range(num_inverters): + inverter = InverterSQL(site_uuid=sites[0].site_uuid, client_id=f"id{j+1}") + inverters.append(inverter) db_session.add_all(inverters) db_session.commit() diff --git a/tests/test_inverters.py b/tests/test_inverters.py index f6b2f09..7b5b1d1 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -2,13 +2,95 @@ from pv_site_api.pydantic_models import Inverters +def test_get_inverters_from_site(client, sites, inverters, httpx_mock): + httpx_mock.add_response( + url="https://enode-api.production.enode.io/inverters/id1", + json={ + "id": "id1", + "vendor": "EMA", + "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", + "lastSeen": "2020-04-07T17:04:26Z", + "isReachable": True, + "productionState": { + "productionRate": 0, + "isProducing": True, + "totalLifetimeProduction": 100152.56, + "lastUpdated": "2020-04-07T17:04:26Z", + }, + "information": { + "id": "string", + "brand": "EMA", + "model": "Sunny Boy", + "siteName": "Sunny Plant", + "installationDate": "2020-04-07T17:04:26Z", + }, + "location": {"longitude": 10.7197486, "latitude": 59.9173985}, + }, + ) + + httpx_mock.add_response( + url="https://enode-api.production.enode.io/inverters/id2", + json={ + "id": "id2", + "vendor": "EMA", + "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", + "lastSeen": "2020-04-07T17:04:26Z", + "isReachable": True, + "productionState": { + "productionRate": 0, + "isProducing": True, + "totalLifetimeProduction": 100152.56, + "lastUpdated": "2020-04-07T17:04:26Z", + }, + "information": { + "id": "string", + "brand": "EMA", + "model": "Sunny Boy", + "siteName": "Sunny Plant", + "installationDate": "2020-04-07T17:04:26Z", + }, + "location": {"longitude": 10.7197486, "latitude": 59.9173985}, + }, + ) + + httpx_mock.add_response( + url="https://enode-api.production.enode.io/inverters/id3", + json={ + "id": "id3", + "vendor": "EMA", + "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", + "lastSeen": "2020-04-07T17:04:26Z", + "isReachable": True, + "productionState": { + "productionRate": 0, + "isProducing": True, + "totalLifetimeProduction": 100152.56, + "lastUpdated": "2020-04-07T17:04:26Z", + }, + "information": { + "id": "string", + "brand": "EMA", + "model": "Sunny Boy", + "siteName": "Sunny Plant", + "installationDate": "2020-04-07T17:04:26Z", + }, + "location": {"longitude": 10.7197486, "latitude": 59.9173985}, + }, + ) + + response = client.get(f"/sites/{sites[0].site_uuid}/inverters") + assert response.status_code == 200 + + response_inverters = Inverters(**response.json()) + assert len(inverters) == len(response_inverters.inverters) + def test_get_inverters_fake(client, fake): response = client.get("/inverters") assert response.status_code == 200 - inverters = Inverters(**response.json()) - assert len(inverters.inverters) > 0 + response_inverters = Inverters(**response.json()) + assert len(response_inverters.inverters) > 0 def test_get_inverters(client, httpx_mock, clients): From 7c8481f655fcf8435d679d5158f96cc1f9914646 Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Thu, 20 Apr 2023 20:37:35 -0500 Subject: [PATCH 21/23] Fix lint errors --- pv_site_api/pydantic_models.py | 4 +++- tests/test_inverters.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pv_site_api/pydantic_models.py b/pv_site_api/pydantic_models.py index 3a4f0dd..d432f19 100644 --- a/pv_site_api/pydantic_models.py +++ b/pv_site_api/pydantic_models.py @@ -118,7 +118,9 @@ class InverterProductionState(BaseModel): isProducing: Optional[bool] = Field( ..., description="Whether the solar inverter is actively producing energy or not" ) - totalLifetimeProduction: Optional[float] = Field(..., description="The total lifetime production in kWh") + totalLifetimeProduction: Optional[float] = Field( + ..., description="The total lifetime production in kWh" + ) lastUpdated: Optional[str] = Field( ..., description="ISO8601 UTC timestamp of last received production state update" ) diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 7b5b1d1..a84a2e2 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -2,6 +2,7 @@ from pv_site_api.pydantic_models import Inverters + def test_get_inverters_from_site(client, sites, inverters, httpx_mock): httpx_mock.add_response( url="https://enode-api.production.enode.io/inverters/id1", @@ -27,7 +28,7 @@ def test_get_inverters_from_site(client, sites, inverters, httpx_mock): "location": {"longitude": 10.7197486, "latitude": 59.9173985}, }, ) - + httpx_mock.add_response( url="https://enode-api.production.enode.io/inverters/id2", json={ @@ -77,7 +78,7 @@ def test_get_inverters_from_site(client, sites, inverters, httpx_mock): "location": {"longitude": 10.7197486, "latitude": 59.9173985}, }, ) - + response = client.get(f"/sites/{sites[0].site_uuid}/inverters") assert response.status_code == 200 From 375f68c7dc53d91d6c9f387d83bbdbf4a73a4460 Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Thu, 20 Apr 2023 21:14:08 -0500 Subject: [PATCH 22/23] Modularize code --- tests/test_inverters.py | 87 +++++------------------------------------ 1 file changed, 9 insertions(+), 78 deletions(-) diff --git a/tests/test_inverters.py b/tests/test_inverters.py index a84a2e2..4378c5a 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -2,12 +2,11 @@ from pv_site_api.pydantic_models import Inverters - -def test_get_inverters_from_site(client, sites, inverters, httpx_mock): +def add_response(id, httpx_mock): httpx_mock.add_response( - url="https://enode-api.production.enode.io/inverters/id1", + url=f"https://enode-api.production.enode.io/inverters/{id}", json={ - "id": "id1", + "id": "string", "vendor": "EMA", "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", "lastSeen": "2020-04-07T17:04:26Z", @@ -29,56 +28,12 @@ def test_get_inverters_from_site(client, sites, inverters, httpx_mock): }, ) - httpx_mock.add_response( - url="https://enode-api.production.enode.io/inverters/id2", - json={ - "id": "id2", - "vendor": "EMA", - "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", - "lastSeen": "2020-04-07T17:04:26Z", - "isReachable": True, - "productionState": { - "productionRate": 0, - "isProducing": True, - "totalLifetimeProduction": 100152.56, - "lastUpdated": "2020-04-07T17:04:26Z", - }, - "information": { - "id": "string", - "brand": "EMA", - "model": "Sunny Boy", - "siteName": "Sunny Plant", - "installationDate": "2020-04-07T17:04:26Z", - }, - "location": {"longitude": 10.7197486, "latitude": 59.9173985}, - }, - ) - - httpx_mock.add_response( - url="https://enode-api.production.enode.io/inverters/id3", - json={ - "id": "id3", - "vendor": "EMA", - "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", - "lastSeen": "2020-04-07T17:04:26Z", - "isReachable": True, - "productionState": { - "productionRate": 0, - "isProducing": True, - "totalLifetimeProduction": 100152.56, - "lastUpdated": "2020-04-07T17:04:26Z", - }, - "information": { - "id": "string", - "brand": "EMA", - "model": "Sunny Boy", - "siteName": "Sunny Plant", - "installationDate": "2020-04-07T17:04:26Z", - }, - "location": {"longitude": 10.7197486, "latitude": 59.9173985}, - }, - ) +def test_get_inverters_from_site(client, sites, inverters, httpx_mock): + add_response('id1', httpx_mock) + add_response('id2', httpx_mock) + add_response('id3', httpx_mock) + response = client.get(f"/sites/{sites[0].site_uuid}/inverters") assert response.status_code == 200 @@ -96,31 +51,7 @@ def test_get_inverters_fake(client, fake): def test_get_inverters(client, httpx_mock, clients): httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters", json=["id1"]) - - httpx_mock.add_response( - url="https://enode-api.production.enode.io/inverters/id1", - json={ - "id": "string", - "vendor": "EMA", - "chargingLocationId": "8d90101b-3f2f-462a-bbb4-1ed320d33bbe", - "lastSeen": "2020-04-07T17:04:26Z", - "isReachable": True, - "productionState": { - "productionRate": 0, - "isProducing": True, - "totalLifetimeProduction": 100152.56, - "lastUpdated": "2020-04-07T17:04:26Z", - }, - "information": { - "id": "string", - "brand": "EMA", - "model": "Sunny Boy", - "siteName": "Sunny Plant", - "installationDate": "2020-04-07T17:04:26Z", - }, - "location": {"longitude": 10.7197486, "latitude": 59.9173985}, - }, - ) + add_response('id1', httpx_mock) response = client.get("/inverters") assert response.status_code == 200 From b0a2e6c56958308349454b6d1d3c067c9737a7a7 Mon Sep 17 00:00:00 2001 From: anyaparekh Date: Thu, 20 Apr 2023 21:17:06 -0500 Subject: [PATCH 23/23] Fix lint errors --- tests/test_inverters.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_inverters.py b/tests/test_inverters.py index 4378c5a..a706434 100644 --- a/tests/test_inverters.py +++ b/tests/test_inverters.py @@ -2,6 +2,7 @@ from pv_site_api.pydantic_models import Inverters + def add_response(id, httpx_mock): httpx_mock.add_response( url=f"https://enode-api.production.enode.io/inverters/{id}", @@ -30,10 +31,10 @@ def add_response(id, httpx_mock): def test_get_inverters_from_site(client, sites, inverters, httpx_mock): - add_response('id1', httpx_mock) - add_response('id2', httpx_mock) - add_response('id3', httpx_mock) - + add_response("id1", httpx_mock) + add_response("id2", httpx_mock) + add_response("id3", httpx_mock) + response = client.get(f"/sites/{sites[0].site_uuid}/inverters") assert response.status_code == 200 @@ -51,7 +52,7 @@ def test_get_inverters_fake(client, fake): def test_get_inverters(client, httpx_mock, clients): httpx_mock.add_response(url="https://enode-api.production.enode.io/inverters", json=["id1"]) - add_response('id1', httpx_mock) + add_response("id1", httpx_mock) response = client.get("/inverters") assert response.status_code == 200