From d63aaa5825bbfc26edbfc55d80584b7b0ff832e4 Mon Sep 17 00:00:00 2001 From: szvsw Date: Thu, 8 Feb 2024 09:39:32 -0500 Subject: [PATCH] create supabase client --- .vscode/settings.json | 22 +++++ campus-decarb/lib/__init__.py | 28 ++++++ campus-decarb/lib/client.py | 12 +++ campus-decarb/lib/supa.py | 155 ++++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 campus-decarb/lib/client.py create mode 100644 campus-decarb/lib/supa.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0d0d0eb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "isort.args": ["--profile", "black"], + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}/campus-decarb" + }, + "terminal.integrated.env.osx": { + "PYTHONPATH": "${workspaceFolder}/campus-decarb" + }, + "terminal.integrated.env.windows": { + "PYTHONPATH": "${workspaceFolder}/campus-decarb" + }, + "python.analysis.extraPaths": ["campus-decarb"], + "jupyter.notebookFileRoot": "${workspaceFolder}/campus-decarb", + "python.envFile": "${workspaceFolder}/.env" +} diff --git a/campus-decarb/lib/__init__.py b/campus-decarb/lib/__init__.py index e69de29..4c363c1 100644 --- a/campus-decarb/lib/__init__.py +++ b/campus-decarb/lib/__init__.py @@ -0,0 +1,28 @@ +from typing import Optional + +from pydantic import Field, computed_field +from pydantic_settings import BaseSettings + + +class SupaSettings(BaseSettings): + url: str = Field(..., env="URL") + service_role_key: str = Field(..., env="SERVICE_ROLE_KEY") + anon_key: Optional[str] = Field(..., env="ANON_KEY") + host: Optional[str] = Field(..., env="HOST") + port: Optional[int] = Field(..., env="PORT") + user: Optional[str] = Field(..., env="USER") + database: Optional[str] = Field(..., env="DATABASE") + password: Optional[str] = Field(..., env="PASSWORD") + echo: Optional[bool] = Field(False, env="ECHO") + + @computed_field + @property + def connection_string(self) -> str: + return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}" + + class Config: + env_prefix = "SUPABASE_" + env_file = ".env" + + +supa_settings = SupaSettings() diff --git a/campus-decarb/lib/client.py b/campus-decarb/lib/client.py new file mode 100644 index 0000000..b7c7dde --- /dev/null +++ b/campus-decarb/lib/client.py @@ -0,0 +1,12 @@ +import os + +from lib import supa_settings +from supabase import create_client +from supabase.client import ClientOptions + +client_options = ClientOptions(postgrest_client_timeout=60) +client = create_client( + supabase_url=supa_settings.url, + supabase_key=supa_settings.service_role_key, + options=client_options, +) diff --git a/campus-decarb/lib/supa.py b/campus-decarb/lib/supa.py new file mode 100644 index 0000000..b6a3f87 --- /dev/null +++ b/campus-decarb/lib/supa.py @@ -0,0 +1,155 @@ +import json +import os +from datetime import datetime +from typing import Optional + +import numpy as np +import pandas as pd +from lib.client import client +from pydantic import BaseModel, Field, model_validator, validate_call + + +class BuildingBase(BaseModel, extra="forbid"): + name: str = Field(..., description="Name of the building") + gfa: float = Field(..., gt=100, description="Gross Floor Area of the building, m2") + + +class Building(BuildingBase): + id: int = Field(..., description="Unique identifier for the building") + created_at: str = Field( + ..., description="Timestamp of the creation of the building" + ) + + @classmethod + def get(cls, id: int): + building = client.table("Building").select("*").eq("id", id).execute() + if len(building.data) == 0: + raise ValueError(f"Building with id {id} not found") + + return cls(**building.data[0]) + + @classmethod + @validate_call() + def create(cls, building: BuildingBase): + building = client.table("Building").insert(building.model_dump()).execute() + return cls(**building.data[0]) + + def commit(self): + client.table("Building").upsert(self.model_dump()).execute() + + +class BuildingSimulationResult(BaseModel, arbitrary_types_allowed=True, extra="forbid"): + id: int + created_at: Optional[str] = Field( + None, description="Timestamp of the creation of the record" + ) + heating: np.ndarray + cooling: np.ndarray + lighting: np.ndarray + equipment: np.ndarray + pumps: np.ndarray + fans: np.ndarray + water: np.ndarray + misc: np.ndarray + + # set up all np.ndarrays to serialize to list + class Config: + json_encoders = {np.ndarray: lambda v: v.tolist()} + + @property + def building_id(self): + res = ( + client.table("DemandScenarioBuilding") + .select("building_id") + .eq("id", self.id) + .execute() + ) + return res.data[0].get("building_id") + + @property + def demand_scenario_id(self): + res = ( + client.table("DemandScenarioBuilding") + .select("demand_scenario_id") + .eq("id", self.id) + .execute() + ) + return res.data[0].get("demand_scenario_id") + + @property + def design_vector_id(self): + res = ( + client.table("DemandScenarioBuilding") + .select("design_vector_id") + .eq("id", self.id) + .execute() + ) + return res.data[0].get("design_vector_id") + + @model_validator(mode="before") + def cast_fields_to_numpy(cls, v): + for key in v: + if cls.model_fields[key].annotation == np.ndarray: + if type(v[key]) == list: + v[key] = np.array(v[key]) + elif type(v[key]) == str: + v[key] = np.array(json.loads(v[key])) + elif type(v[key]) == pd.Series: + v[key] = v[key].values + else: + pass + + assert v[key].shape == ( + 8760, + ), f"Field {key} must have shape (8760,) but has shape {v[key].shape}" + return v + + @classmethod + def get(cls, id: int): + res = ( + client.table("BuildingSimulationResult").select("*").eq("id", id).execute() + ) + if len(res.data) == 0: + raise ValueError(f"BuildingSimulationResult with id {id} not found") + + return cls(**res.data[0]) + + def to_df(self) -> pd.DataFrame: + df = pd.DataFrame( + { + "heating": self.heating, + "cooling": self.cooling, + "lighting": self.lighting, + "equipment": self.equipment, + "pumps": self.pumps, + "fans": self.fans, + "water": self.water, + "misc": self.misc, + }, + index=pd.date_range( + start="2024-01-01 00:00:00", periods=8760, freq="H", name="timestep" + ), + ) + df = df.set_index( + pd.Series([self.id] * 8760, name="building_id"), + append=True, + ) + raise ValueError("Not finished implementing; decide on multiindex!") + df = df.unstack(level="timestep") + return df + + # def from_df(self, df: pd.DataFrame): + # series = df.loc[self.id] + # self.heating = series.heating.values + # self.cooling = series.cooling.values + # self.lighting = series.lighting.values + # self.equipment = series.equipment.values + # self.pumps = series.pumps.values + # self.fans = series.fans.values + # self.water = series.water.values + # self.misc = series.misc.values + + def commit(self): + client.table("BuildingSimulationResult").upsert( + self.model_dump(mode="json", exclude=["created_at"]) + ).execute()