From bc26686ef9a03778d114bbb64f939f21d562b23d Mon Sep 17 00:00:00 2001 From: peterdudfield Date: Thu, 7 Nov 2024 21:19:30 +0000 Subject: [PATCH 1/3] remove pv database --- nowcasting_datamodel/fake.py | 6 - nowcasting_datamodel/models/__init__.py | 4 +- nowcasting_datamodel/models/base.py | 1 - nowcasting_datamodel/models/pv.py | 155 ----------------- nowcasting_datamodel/read/read.py | 28 ---- nowcasting_datamodel/read/read_pv.py | 212 ------------------------ nowcasting_datamodel/save/save.py | 41 ----- test-docker-compose.yml | 16 +- tests/conftest.py | 29 ---- tests/read/conftest.py | 61 ------- tests/read/test_read.py | 18 -- tests/read/test_read_pv.py | 164 ------------------ tests/save/test_save.py | 21 +-- tests/test_fake_pv.py | 24 --- 14 files changed, 5 insertions(+), 775 deletions(-) delete mode 100644 nowcasting_datamodel/models/pv.py delete mode 100644 nowcasting_datamodel/read/read_pv.py delete mode 100644 tests/read/conftest.py delete mode 100644 tests/read/test_read_pv.py delete mode 100644 tests/test_fake_pv.py diff --git a/nowcasting_datamodel/fake.py b/nowcasting_datamodel/fake.py index 6b7d8605..7b393079 100644 --- a/nowcasting_datamodel/fake.py +++ b/nowcasting_datamodel/fake.py @@ -12,7 +12,6 @@ MetricSQL, MetricValueSQL, MLModelSQL, - PVSystemSQL, national_gb_label, ) from nowcasting_datamodel.models.forecast import ForecastSQL, ForecastValueSQL @@ -32,11 +31,6 @@ def make_fake_location(gsp_id: int) -> LocationSQL: return LocationSQL(label=f"GSP_{gsp_id}", gsp_id=gsp_id) -def make_fake_pv_system() -> PVSystemSQL: - """Make fake location with gsp id""" - return PVSystemSQL(pv_system_id=1, provider="pvoutput.org", latitude=55, longitude=0) - - def make_fake_input_data_last_updated() -> InputDataLastUpdatedSQL: """Make fake input data last updated""" now = datetime.now(tz=timezone.utc) diff --git a/nowcasting_datamodel/models/__init__.py b/nowcasting_datamodel/models/__init__.py index c1184b15..af145b4e 100644 --- a/nowcasting_datamodel/models/__init__.py +++ b/nowcasting_datamodel/models/__init__.py @@ -8,8 +8,7 @@ 5. Input data status, shows when the data was collected (models.py) 6. Forecasts, a forecast that is made for one gsp, for several time steps into the future (models.py) -7. PV system for storing PV data (pv.py) -8. PV yield for storing PV data (pv.py) + Current these models have a primary index of 'id'. This keeps things very simple at the start. @@ -25,4 +24,3 @@ from .gsp import * # noqa F403 from .metric import * # noqa F403 from .models import * # noqa F403 -from .pv import * # noqa F403 diff --git a/nowcasting_datamodel/models/base.py b/nowcasting_datamodel/models/base.py index a68bdaf5..3a904120 100644 --- a/nowcasting_datamodel/models/base.py +++ b/nowcasting_datamodel/models/base.py @@ -3,4 +3,3 @@ from sqlalchemy.orm import declarative_base Base_Forecast = declarative_base() # noqa -Base_PV = declarative_base() # noqa diff --git a/nowcasting_datamodel/models/pv.py b/nowcasting_datamodel/models/pv.py deleted file mode 100644 index 76fee6f8..00000000 --- a/nowcasting_datamodel/models/pv.py +++ /dev/null @@ -1,155 +0,0 @@ -""" Pydantic and Sqlalchemy models for the database - -7. PV system for storing PV data (pv.py) -8. PV yield for storing PV data (pv.py) - -""" - -import logging -from datetime import datetime -from typing import Optional - -from pydantic import Field, field_validator -from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, String -from sqlalchemy.orm import relationship - -from nowcasting_datamodel.models.base import Base_PV -from nowcasting_datamodel.models.utils import CreatedMixin, EnhancedBaseModel -from nowcasting_datamodel.utils import datetime_with_timezone - -logger = logging.getLogger(__name__) -pv_output = "pvoutput.org" -solar_sheffield_passiv = "solar_sheffield_passiv" -providers = [pv_output, solar_sheffield_passiv] - -######## -# 7. PV Metadata -######## - - -class PVSystemSQL(Base_PV, CreatedMixin): - """Metadata for PV data""" - - __tablename__ = "pv_system" - - id = Column(Integer, primary_key=True) - pv_system_id = Column(Integer, index=True) - provider = Column(String) - latitude = Column(Float) - longitude = Column(Float) - name = Column(String, nullable=True) - orientation = Column(Float, nullable=True) - status_interval_minutes = Column(Integer, nullable=True) - installed_capacity_kw = Column( - Float, - nullable=True, - ) - ml_capacity_kw = Column( - Float, - nullable=True, - ) - ocf_id = Column(Integer, nullable=True) - correct_data = Column(Boolean, default=True) - - pv_yield = relationship("PVYieldSQL", back_populates="pv_system") - - -class PVSystem(EnhancedBaseModel): - """Metadata for PV data""" - - pv_system_id: int = Field(..., description="The PV system id") - provider: str = Field(..., description="The provider of the PV system") - latitude: float = Field(None, description="The latitude of the PV system") - longitude: float = Field(None, description="The longitude of the PV system") - name: Optional[str] = Field(None, description="The PV system name") - orientation: Optional[float] = Field(None, description="The orientation of the PV system") - status_interval_minutes: Optional[float] = Field( - None, description="The number of minutes for the pv data to be refreshed" - ) - installed_capacity_kw: Optional[float] = Field( - None, description="The capacity of the pv system in kw." - ) - ml_capacity_kw: Optional[float] = Field( - None, - description="The capacity of the pv system in kw, user for ML models. " - "This may be different from the installed capacity", - ) - correct_data: Optional[bool] = Field( - True, description="If the data from the pv system is not broken in some way" - ) - ocf_id: Optional[int] = Field(None, description="The PV system id that is unique to OCF") - - @field_validator("provider") - def validate_provider(cls, v): - """Validate the provider""" - if v not in providers: - raise Exception(f"Provider ({v}) must be in {providers}") - return v - - def to_orm(self) -> PVSystemSQL: - """Change model to PVSystemSQL""" - return PVSystemSQL( - pv_system_id=self.pv_system_id, - provider=self.provider, - latitude=self.latitude, - longitude=self.longitude, - name=self.name, - orientation=self.orientation, - status_interval_minutes=self.status_interval_minutes, - correct_data=self.correct_data, - installed_capacity_kw=self.installed_capacity_kw, - ml_capacity_kw=self.ml_capacity_kw, - ocf_id=self.ocf_id, - ) - - -######## -# 8. PV Yield -######## - - -class PVYieldSQL(Base_PV, CreatedMixin): - """PV Yield data""" - - __tablename__ = "pv_yield" - - id = Column(Integer, primary_key=True) - datetime_utc = Column(DateTime, index=True) - solar_generation_kw = Column(Float) - - # many (forecasts) to one (location) - pv_system = relationship("PVSystemSQL", back_populates="pv_yield") - pv_system_id = Column(Integer, ForeignKey("pv_system.id"), index=True) - - Index("ix_datetime_utc", datetime_utc.desc()) - - -class PVYield(EnhancedBaseModel): - """PV Yield data""" - - datetime_utc: datetime = Field(..., description="The timestamp of the pv system") - solar_generation_kw: float = Field(..., description="The provider of the PV system") - pv_system: Optional[PVSystem] = Field( - None, - description="The PV system associated with this model", - ) - - @field_validator("datetime_utc", mode="before") - def normalize_datetime_utc(cls, v): - """Normalize datetime_utc field""" - return datetime_with_timezone(cls, v) - - @field_validator("solar_generation_kw") - def validate_solar_generation_kw(cls, v): - """Validate the solar_generation_kw field""" - if v < 0: - logger.debug(f"Changing solar_generation_kw ({v}) to 0") - v = 0 - return v - - def to_orm(self) -> PVYieldSQL: - """Change model to PVYieldSQL""" - return PVYieldSQL( - datetime_utc=self.datetime_utc, - solar_generation_kw=self.solar_generation_kw, - ) diff --git a/nowcasting_datamodel/read/read.py b/nowcasting_datamodel/read/read.py index 95870cdf..b528523c 100644 --- a/nowcasting_datamodel/read/read.py +++ b/nowcasting_datamodel/read/read.py @@ -18,7 +18,6 @@ InputDataLastUpdatedSQL, LocationSQL, MLModelSQL, - PVSystemSQL, StatusSQL, national_gb_label, ) @@ -758,30 +757,3 @@ def get_all_locations(session: Session, gsp_ids: List[int] = None) -> List[Locat return locations - -def get_pv_system( - session: Session, pv_system_id: int, provider: Optional[str] = "pvoutput.org" -) -> PVSystemSQL: - """ - Get model object from name and version - - :param session: database session - :param pv_system_id: pv system id - :param provider: the pv provider, defaulted to pvoutput.org - - return: PVSystem object - """ - - # start main query - query = session.query(PVSystemSQL) - - # filter on pv_system_id and provider - query = query.filter(PVSystemSQL.pv_system_id == pv_system_id) - query = query.filter(PVSystemSQL.provider == provider) - - # get all results - pv_systems = query.all() - - pv_system = pv_systems[0] - - return pv_system diff --git a/nowcasting_datamodel/read/read_pv.py b/nowcasting_datamodel/read/read_pv.py deleted file mode 100644 index 004c0c06..00000000 --- a/nowcasting_datamodel/read/read_pv.py +++ /dev/null @@ -1,212 +0,0 @@ -""" Read pv functions """ - -import logging -from datetime import datetime, timezone -from typing import List, Optional, Union - -from sqlalchemy import desc -from sqlalchemy.orm import Session, joinedload - -from nowcasting_datamodel.models import PVSystemSQL, PVYieldSQL - -logger = logging.getLogger(__name__) - - -def get_pv_systems( - session: Session, - pv_systems_ids: Optional[List[int]] = None, - provider: Optional[str] = "pvoutput.org", - correct_data: Optional[bool] = None, -) -> List[PVSystemSQL]: - """ - Get all pv systems - - :param session: - :param pv_systems_ids: optional list of ids - :param provider: optional provider name - :param correct_data: Filters on incorrect_data in pv_systems - :return: list of pv systems - """ - - # start main query - query = session.query(PVSystemSQL) - query = query.distinct(PVSystemSQL.id) - - # only select correct data pv systems - if correct_data is not None: - query = query.filter(PVSystemSQL.correct_data == correct_data) - - # filter on pv_system_id and provider - if pv_systems_ids is not None: - query = query.filter(PVSystemSQL.pv_system_id.in_(pv_systems_ids)) - - if provider is not None: - query = query.filter(PVSystemSQL.provider == provider) - - # order by 'created_utc' desc, so we get the latest one - query = query.order_by(PVSystemSQL.id, desc(PVSystemSQL.created_utc)) - - # get all results - pv_systems = query.all() - - return pv_systems - - -def get_latest_pv_yield( - session: Session, - pv_systems: List[PVSystemSQL], - append_to_pv_systems: bool = False, - start_datetime_utc: Optional[datetime] = None, - start_created_utc: Optional[datetime] = None, - correct_data: Optional[bool] = None, -) -> Union[List[PVYieldSQL], List[PVSystemSQL]]: - """ - Get the last pv yield data - - :param session: database sessions - :param pv_systems: list of pv systems - :param append_to_pv_systems: append pv yield to pv systems, or return pv systems. - If appended the yield is access by 'pv_system.last_pv_yield' - :param start_created_utc: search filters > on 'created_utc'. Can be None - :param start_datetime_utc: search filters > on 'datetime_utc'. Can be None - :param correct_data: Filters on incorrect_data in pv_systems - :return: either list of pv yields, or pv systems - """ - - logger.info("Getting latest pv yield") - - pv_systems_ids = [pv_system.id for pv_system in pv_systems] - - # start main query - query = session.query(PVYieldSQL) - query = query.join(PVSystemSQL) - query = query.where( - PVSystemSQL.id == PVYieldSQL.pv_system_id, - ) - - # only select on results per pv system - query = query.distinct(PVSystemSQL.id) - - # select only th epv systems we want - query = query.where(PVSystemSQL.id.in_(pv_systems_ids)) - - # only select correct data pv systems - if correct_data is not None: - query = query.filter(PVSystemSQL.correct_data == correct_data) - - # filter on datetime utc - if start_datetime_utc is not None: - query = query.filter(PVYieldSQL.datetime_utc >= start_datetime_utc) - - # filter on created utc - if start_created_utc is not None: - query = query.filter(PVYieldSQL.created_utc >= start_created_utc) - - # order by 'created_utc' desc, so we get the latest one - query = query.order_by( - PVSystemSQL.id, desc(PVYieldSQL.datetime_utc), desc(PVYieldSQL.created_utc) - ) - - # get all results - pv_yields: List[PVYieldSQL] = query.all() - - # add utc timezone - for pv_yield in pv_yields: - pv_yield.datetime_utc = pv_yield.datetime_utc.replace(tzinfo=timezone.utc) - - if not append_to_pv_systems: - return pv_yields - else: - logger.info("Will be returning pv systems") - - # get list of pvsystems with last pv yields - pv_systems_with_pv_yields = [] - for pv_yield in pv_yields: - pv_system = pv_yield.pv_system - pv_system.last_pv_yield = pv_yield - - pv_systems_with_pv_yields.append(pv_system) - - # add pv systems that dont have any pv yields - pv_systems_with_pv_yields_ids = [pv_system.id for pv_system in pv_systems_with_pv_yields] - - logger.debug(f"Found {len(pv_systems_with_pv_yields_ids)} pv systems with pv yields") - - pv_systems_with_no_pv_yields = [] - for pv_system in pv_systems: - if pv_system.id not in pv_systems_with_pv_yields_ids: - pv_system.last_pv_yield = None - - pv_systems_with_no_pv_yields.append(pv_system) - - logger.debug(f"Found {len(pv_systems_with_no_pv_yields)} pv systems with no pv yields") - - all_pv_systems = pv_systems_with_pv_yields + pv_systems_with_no_pv_yields - - return all_pv_systems - - -def get_pv_yield( - session: Session, - pv_systems_ids: Optional[List[int]] = None, - start_utc: Optional[datetime] = None, - end_utc: Optional[datetime] = None, - correct_data: Optional[bool] = None, - providers: Optional[List[str]] = None, - distinct: Optional[bool] = False, -) -> Union[List[PVYieldSQL], List[PVSystemSQL]]: - """ - Get the last pv yield data - - :param session: database sessions - :param pv_systems_ids: list of pv systems ids - :param end_utc: search filters < on 'datetime_utc'. Can be None - :param start_utc: search filters >= on 'datetime_utc'. Can be None - :param correct_data: Filters on incorrect_data in pv_systems - :param providers: optional list of provider names - :param distinct: if True, only return distinct pv yields - :return: either list of pv yields, or pv systems - """ - - # start main query - query = session.query(PVYieldSQL) - - if distinct: - query = query.distinct(PVSystemSQL.id, PVYieldSQL.datetime_utc) - - query = query.join(PVSystemSQL) - query = query.options(joinedload(PVYieldSQL.pv_system)) - - # only select correct data pv systems - if correct_data is not None: - query = query.filter(PVSystemSQL.correct_data == correct_data) - - # select only th pv systems we want - if pv_systems_ids is not None: - query = query.where(PVSystemSQL.pv_system_id.in_(pv_systems_ids)) - - # filter on start time - if start_utc is not None: - query = query.filter(PVYieldSQL.datetime_utc >= start_utc) - - # filter on end time - if end_utc is not None: - query = query.filter(PVYieldSQL.datetime_utc < end_utc) - - if providers is not None: - query = query.filter(PVSystemSQL.provider.in_(providers)) - - # order by 'created_utc' desc, so we get the latest one - query = query.order_by( - PVSystemSQL.id, - PVYieldSQL.datetime_utc, - PVYieldSQL.created_utc.desc(), - ) - - # get all results - pv_yields: List[PVYieldSQL] = query.all() - - for pv_yield in pv_yields: - pv_yield.datetime_utc = pv_yield.datetime_utc.replace(tzinfo=timezone.utc) - - return pv_yields diff --git a/nowcasting_datamodel/save/save.py b/nowcasting_datamodel/save/save.py index 731a4991..ab447129 100644 --- a/nowcasting_datamodel/save/save.py +++ b/nowcasting_datamodel/save/save.py @@ -6,7 +6,6 @@ from sqlalchemy.orm.session import Session -from nowcasting_datamodel.models import PVSystem, PVSystemSQL from nowcasting_datamodel.models.forecast import ForecastSQL from nowcasting_datamodel.save.adjust import add_adjust_to_forecasts from nowcasting_datamodel.save.update import ( @@ -82,46 +81,6 @@ def save( session.commit() -def save_pv_system(session: Session, pv_system: PVSystem) -> PVSystemSQL: - """ - Get model object from name and version - - :param session: database session - :param pv_system: pv system sql object - - return: PVSystem object - - """ - - # start main query - query = session.query(PVSystemSQL) - - # filter on pv_system_id and provider - query = query.filter(PVSystemSQL.pv_system_id == pv_system.pv_system_id) - query = query.filter(PVSystemSQL.provider == pv_system.provider) - - # get all results - pv_systems = query.all() - - if len(pv_systems) == 0: - logger.debug( - f"Model for provider {pv_system.provider} and pv_system_id {pv_system.pv_system_id }" - f"does not exist so going to add it" - ) - - session.add(pv_system.to_orm()) - session.commit() - - else: - logger.debug( - f"Model for provider {pv_system.provider} and pv_system_id {pv_system.pv_system_id} " - f"is already there so not going to add it" - ) - pv_system = pv_systems[0] - - return pv_system - - def save_all_forecast_values_seven_days( session: Session, forecasts: List[ForecastSQL], remove_non_distinct: bool = False ): diff --git a/test-docker-compose.yml b/test-docker-compose.yml index 23f76871..bddd55da 100644 --- a/test-docker-compose.yml +++ b/test-docker-compose.yml @@ -2,7 +2,8 @@ version: "3" services: postgres_forecast: - image: postgres:14.5 + container_name: postgres_forecast + image: postgres:17 restart: always environment: - POSTGRES_USER=postgres @@ -10,27 +11,16 @@ services: ports: - "5432:5432" - postgres_pv: - image: postgres:14.5 - restart: always - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - ports: - - "5433:5432" - datamodel: build: context: . dockerfile: infrastructure/docker/Dockerfile environment: - DB_URL=postgresql://postgres:postgres@postgres_forecast:5432/postgres - - DB_URL_PV=postgresql://postgres:postgres@postgres_pv:5432/postgres - GIT_PYTHON_REFRESH=quiet - LOG_LEVEL=DEBUG depends_on: - postgres_forecast - - postgres_pv volumes: - ./tests/:/app/tests - ./nowcasting_datamodel/:/app/nowcasting_datamodel @@ -43,7 +33,6 @@ services: TESTING: 1 environment: - DB_URL=postgresql://postgres:postgres@postgres_forecast:5432/postgres - - DB_URL_PV=postgresql://postgres:postgres@postgres_pv:5432/postgres - GIT_PYTHON_REFRESH=quiet - LOG_LEVEL=DEBUG command: > @@ -55,7 +44,6 @@ services: && cp coverage.xml ./tests/" depends_on: - postgres_forecast - - postgres_pv - datamodel volumes: - ./tests/:/app/tests diff --git a/tests/conftest.py b/tests/conftest.py index 8ac64929..b44fa423 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ from nowcasting_datamodel.fake import make_fake_forecasts, make_fake_me_latest from nowcasting_datamodel.models import MetricValueSQL from nowcasting_datamodel.models.forecast import ForecastSQL -from nowcasting_datamodel.models.pv import Base_PV @pytest.fixture @@ -78,34 +77,6 @@ def db_session(db_connection): connection.close() -@pytest.fixture -def db_connection_pv(): - url = os.getenv("DB_URL_PV", "sqlite:///test_pv.db") - - connection = DatabaseConnection(url=url, base=Base_PV) - Base_PV.metadata.create_all(connection.engine) - - yield connection - - Base_PV.metadata.drop_all(connection.engine) - - -@pytest.fixture(scope="function", autouse=True) -def db_session_pv(db_connection_pv): - """Creates a new database session for a test.""" - - connection = db_connection_pv.engine.connect() - t = connection.begin() - - with db_connection_pv.get_session() as s: - s.begin() - yield s - s.rollback() - - t.rollback() - connection.close() - - @pytest.fixture def latest_me(db_session) -> List[MetricValueSQL]: # create diff --git a/tests/read/conftest.py b/tests/read/conftest.py deleted file mode 100644 index a11d3f0d..00000000 --- a/tests/read/conftest.py +++ /dev/null @@ -1,61 +0,0 @@ -from datetime import datetime - -import pytest - -from nowcasting_datamodel.models import PVSystem, PVSystemSQL, PVYield - - -@pytest.fixture() -def pv_systems(db_session_pv): - pv_system_sql_1: PVSystemSQL = PVSystem( - pv_system_id=1, provider="pvoutput.org", status_interval_minutes=5 - ).to_orm() - pv_system_sql_2: PVSystemSQL = PVSystem( - pv_system_id=2, provider="pvoutput.org", status_interval_minutes=5 - ).to_orm() - - # add to database - db_session_pv.add(pv_system_sql_1) - db_session_pv.add(pv_system_sql_2) - - db_session_pv.commit() - - return [pv_system_sql_1, pv_system_sql_2] - - -@pytest.fixture() -def pv_yields_and_systems(db_session_pv): - pv_yield_1 = PVYield(datetime_utc=datetime(2022, 1, 1), solar_generation_kw=1) - pv_yield_1_sql = pv_yield_1.to_orm() - - pv_yield_2 = PVYield(datetime_utc=datetime(2022, 1, 2), solar_generation_kw=2) - pv_yield_2_sql = pv_yield_2.to_orm() - - pv_yield_3 = PVYield(datetime_utc=datetime(2022, 1, 1), solar_generation_kw=3) - pv_yield_3_sql = pv_yield_3.to_orm() - - pv_system_sql_1: PVSystemSQL = PVSystem( - pv_system_id=1, provider="pvoutput.org", status_interval_minutes=5 - ).to_orm() - pv_system_sql_2: PVSystemSQL = PVSystem( - pv_system_id=2, provider="pvoutput.org", status_interval_minutes=5 - ).to_orm() - - # add pv system to yield object - pv_yield_1_sql.pv_system = pv_system_sql_1 - pv_yield_2_sql.pv_system = pv_system_sql_1 - pv_yield_3_sql.pv_system = pv_system_sql_2 - - # add to database - db_session_pv.add(pv_yield_1_sql) - db_session_pv.add(pv_yield_2_sql) - db_session_pv.add(pv_yield_3_sql) - db_session_pv.add(pv_system_sql_1) - db_session_pv.add(pv_system_sql_2) - - db_session_pv.commit() - - return { - "pv_yields": [pv_yield_1_sql, pv_yield_2_sql, pv_yield_3_sql], - "pv_systems": [pv_system_sql_1, pv_system_sql_2], - } diff --git a/tests/read/test_read.py b/tests/read/test_read.py index 6de19df0..881bed83 100644 --- a/tests/read/test_read.py +++ b/tests/read/test_read.py @@ -9,12 +9,10 @@ make_fake_forecast, make_fake_forecasts, make_fake_national_forecast, - make_fake_pv_system, ) from nowcasting_datamodel.models import ( InputDataLastUpdatedSQL, LocationSQL, - PVSystem, Status, national_gb_label, ) @@ -35,10 +33,8 @@ get_latest_national_forecast, get_latest_status, get_location, - get_pv_system, update_latest_input_data_last_updated, ) -from nowcasting_datamodel.save.save import save_pv_system logger = logging.getLogger(__name__) @@ -403,20 +399,6 @@ def test_get_national_latest_forecast(db_session): assert forecast_values_read == f2 -def test_get_pv_system(db_session_pv): - pv_system = PVSystem.model_validate(make_fake_pv_system(), from_attributes=True) - save_pv_system(session=db_session_pv, pv_system=pv_system) - - pv_system_get = get_pv_system( - session=db_session_pv, provider=pv_system.provider, pv_system_id=pv_system.pv_system_id - ) - # this get defaulted to True when adding to the database - pv_system.correct_data = True - assert PVSystem.model_validate(pv_system, from_attributes=True) == PVSystem.model_validate( - pv_system_get, from_attributes=True - ) - - def test_get_latest_input_data_last_updated_multiple_entries(db_session): now = datetime.now(tz=None) yesterday = now - timedelta(hours=24) diff --git a/tests/read/test_read_pv.py b/tests/read/test_read_pv.py deleted file mode 100644 index 311b3700..00000000 --- a/tests/read/test_read_pv.py +++ /dev/null @@ -1,164 +0,0 @@ -import logging -from datetime import datetime, timezone - -from nowcasting_datamodel.models import PVSystem, PVSystemSQL, pv_output, solar_sheffield_passiv -from nowcasting_datamodel.read.read_pv import get_latest_pv_yield, get_pv_systems, get_pv_yield -from nowcasting_datamodel.save.save import save_pv_system - -logger = logging.getLogger(__name__) - - -def test_save_pv_system(db_session_pv): - pv_system_1 = PVSystem(pv_system_id=1, provider="pvoutput.org", status_interval_minutes=5) - - save_pv_system(session=db_session_pv, pv_system=pv_system_1) - - pv_systems_get = get_pv_systems(session=db_session_pv) - assert len(pv_systems_get) == 1 - - -def test_save_pv_system_repeat(db_session_pv, pv_systems): - # add extra systems that is already there - save_pv_system(session=db_session_pv, pv_system=pv_systems[0]) - - pv_systems_get = get_pv_systems(session=db_session_pv) - assert len(pv_systems_get) == 2 - - -def test_save_pv_system_correct_data(db_session_pv, pv_systems): - pv_systems[0].correct_data = False - pv_systems_get = get_pv_systems(session=db_session_pv, correct_data=True) - assert len(pv_systems_get) == 1 - - pv_systems_get = get_pv_systems( - session=db_session_pv, pv_systems_ids=[pv_systems_get[0].id], correct_data=True - ) - - assert len(pv_systems_get) == 1 - - -def test_get_pv_system(db_session_pv, pv_systems): - pv_systems_get = get_pv_systems(session=db_session_pv) - assert len(pv_systems_get) == 2 - - pv_systems_get = get_pv_systems(session=db_session_pv, pv_systems_ids=[pv_systems_get[0].id]) - - assert len(pv_systems_get) == 1 - - -def test_get_latest_pv_yield(db_session_pv, pv_yields_and_systems): - [pv_system_sql_1, pv_system_sql_2] = pv_yields_and_systems["pv_systems"] - - pv_yields = get_latest_pv_yield( - session=db_session_pv, pv_systems=[pv_system_sql_1, pv_system_sql_2] - ) - - # read database - # this is 3 for when using sqlite as 'distinct' does work - assert len(pv_yields) == 2 - - assert pv_yields[0].datetime_utc == datetime(2022, 1, 2, tzinfo=timezone.utc) - assert pv_yields[1].datetime_utc == datetime(2022, 1, 1, tzinfo=timezone.utc) - - pv_systems = db_session_pv.query(PVSystemSQL).order_by(PVSystemSQL.created_utc).all() - pv_yields[0].pv_system.id = pv_systems[0].id - - -def test_get_latest_pv_yield_correct_data(db_session_pv, pv_yields_and_systems): - [pv_system_sql_1, pv_system_sql_2] = pv_yields_and_systems["pv_systems"] - pv_system_sql_1.correct_data = False - - pv_yields = get_latest_pv_yield( - session=db_session_pv, - pv_systems=[pv_system_sql_1, pv_system_sql_2], - correct_data=True, - ) - - # read database - # this is 2 for when using sqlite as 'distinct' does work - assert len(pv_yields) == 1 - - -def test_get_latest_pv_yield_filter(db_session_pv, pv_yields_and_systems): - [pv_system_sql_1, pv_system_sql_2] = pv_yields_and_systems["pv_systems"] - - pv_yields = get_latest_pv_yield( - session=db_session_pv, - pv_systems=[pv_system_sql_1, pv_system_sql_2], - start_datetime_utc=datetime(2022, 1, 2), - start_created_utc=datetime(2022, 1, 2), - ) - - # read database - assert len(pv_yields) == 1 - - assert pv_yields[0].datetime_utc == datetime(2022, 1, 2, tzinfo=timezone.utc) - - pv_systems = db_session_pv.query(PVSystemSQL).order_by(PVSystemSQL.created_utc).all() - pv_yields[0].pv_system.id = pv_systems[0].id - - -def test_get_latest_pv_yield_append_no_yields(db_session_pv, pv_systems): - [pv_system_sql_1, pv_system_sql_2] = pv_systems - - pv_systems = get_latest_pv_yield( - session=db_session_pv, - pv_systems=[pv_system_sql_1, pv_system_sql_2], - append_to_pv_systems=True, - ) - - assert pv_systems[0].last_pv_yield is None - assert len(pv_systems) == 2 - - -def test_get_latest_pv_yield_append(db_session_pv, pv_yields_and_systems): - [pv_system_sql_1, pv_system_sql_2] = pv_yields_and_systems["pv_systems"] - - assert pv_system_sql_1.pv_system_id == 1 - assert pv_system_sql_2.pv_system_id == 2 - - pv_systems = get_latest_pv_yield( - session=db_session_pv, - pv_systems=[pv_system_sql_1, pv_system_sql_2], - append_to_pv_systems=True, - ) - assert pv_systems[0].last_pv_yield is not None - # this is 3 for when using sqlite as 'distinct' does work - assert len(pv_systems) == 2 - - -def test_read_pv_yield(db_session_pv, pv_yields_and_systems): - assert len(get_pv_yield(session=db_session_pv, pv_systems_ids=[1])) == 2 - assert len(get_pv_yield(session=db_session_pv, pv_systems_ids=[1, 2])) == 3 - - -def test_read_pv_yield_providers(db_session_pv, pv_yields_and_systems): - assert len(get_pv_yield(session=db_session_pv, providers=[pv_output])) == 3 - assert len(get_pv_yield(session=db_session_pv, providers=[solar_sheffield_passiv])) == 0 - - -def test_read_pv_yield_correct_data(db_session_pv, pv_yields_and_systems): - pv_yields_and_systems["pv_systems"][0].correct_data = False - - assert len(get_pv_yield(session=db_session_pv, pv_systems_ids=[1], correct_data=True)) == 0 - assert len(get_pv_yield(session=db_session_pv, pv_systems_ids=[1, 2], correct_data=True)) == 1 - - -def test_read_pv_yield_start_utc(db_session_pv, pv_yields_and_systems): - pv_yields = get_pv_yield( - session=db_session_pv, pv_systems_ids=[1], start_utc=datetime(2022, 1, 2) - ) - assert len(pv_yields) == 1 - assert pv_yields[0].datetime_utc == datetime(2022, 1, 2, tzinfo=timezone.utc) - - -def test_read_pv_yield_end_utc(db_session_pv, pv_yields_and_systems): - pv_yields = get_pv_yield( - session=db_session_pv, pv_systems_ids=[1, 2], end_utc=datetime(2022, 1, 2), distinct=True - ) - assert len(pv_yields) == 2 - - print(pv_yields) - - assert pv_yields[0].datetime_utc == datetime(2022, 1, 1, tzinfo=timezone.utc) - assert pv_yields[1].datetime_utc == datetime(2022, 1, 1, tzinfo=timezone.utc) diff --git a/tests/save/test_save.py b/tests/save/test_save.py index b993e124..920155c5 100644 --- a/tests/save/test_save.py +++ b/tests/save/test_save.py @@ -11,8 +11,8 @@ ForecastValueSevenDaysSQL, ForecastValueSQL, ) -from nowcasting_datamodel.models.pv import PVSystem, PVSystemSQL -from nowcasting_datamodel.save.save import save, save_all_forecast_values_seven_days, save_pv_system + +from nowcasting_datamodel.save.save import save, save_all_forecast_values_seven_days @freeze_time("2024-01-01 00:00:00") @@ -132,23 +132,6 @@ def test_save_no_adjuster(db_session): assert forecast_latest_values[0].gsp_id == forecast_latest_values[1].gsp_id -def test_save_pv_system(db_session_pv): - pv_systems = db_session_pv.query(PVSystemSQL).all() - assert len(pv_systems) == 0 - - pv_system = PVSystem(pv_system_id=2, provider="pvoutput.org", latitude=55, longitude=0) - - save_pv_system(session=db_session_pv, pv_system=pv_system) - - pv_systems = db_session_pv.query(PVSystemSQL).all() - assert len(pv_systems) == 1 - - save_pv_system(session=db_session_pv, pv_system=pv_system) - - pv_systems = db_session_pv.query(PVSystemSQL).all() - assert len(pv_systems) == 1 - - def test_save_all_forecast_values_seven_days(db_session): now = datetime.now(tz=timezone.utc) forecasts = make_fake_forecasts(gsp_ids=range(0, 3), session=db_session, t0_datetime_utc=now) diff --git a/tests/test_fake_pv.py b/tests/test_fake_pv.py deleted file mode 100644 index 9546d4c9..00000000 --- a/tests/test_fake_pv.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import datetime - -import pytest - -from nowcasting_datamodel.fake import make_fake_pv_system -from nowcasting_datamodel.models import PVSystem, PVSystemSQL, PVYield - - -def test_make_fake_pv_system(): - pv_system_sql: PVSystemSQL = make_fake_pv_system() - pv_system = PVSystem.model_validate(pv_system_sql) - _ = PVSystem.to_orm(pv_system) - - -def test_pv_system_error(): - with pytest.raises(Exception): - _ = PVSystem(pv_system_id=1, provider="fake.com", latitude=55, longitude=0) - - -def test_make_fake_pv_yield(): - pv_yield = PVYield(datetime_utc=datetime(2022, 1, 1), solar_generation_kw=-1) - assert pv_yield.solar_generation_kw == 0 - - _ = pv_yield.to_orm() From 693522c63a617dcb0452a87212c5ed077b0f0bf6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:50:15 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- nowcasting_datamodel/read/read.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nowcasting_datamodel/read/read.py b/nowcasting_datamodel/read/read.py index b528523c..108d6182 100644 --- a/nowcasting_datamodel/read/read.py +++ b/nowcasting_datamodel/read/read.py @@ -756,4 +756,3 @@ def get_all_locations(session: Session, gsp_ids: List[int] = None) -> List[Locat locations = [nation] + locations return locations - From 5969411dccdc27909cfc3d66732a7cf6d0c4f897 Mon Sep 17 00:00:00 2001 From: peterdudfield Date: Thu, 7 Nov 2024 21:51:19 +0000 Subject: [PATCH 3/3] tidy up readme --- README.md | 1 - diagram_pv.png | Bin 49887 -> 0 bytes 2 files changed, 1 deletion(-) delete mode 100644 diagram_pv.png diff --git a/README.md b/README.md index 96f97fbb..8e5a393d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ All models are in `nowcasting_datamodel.models.py`. The diagram below shows how the different tables are connected. ![Models](diagram.png) -![Models](diagram_pv.png) ### connection.py diff --git a/diagram_pv.png b/diagram_pv.png deleted file mode 100644 index 56472f6a8361f4879b26a0b9ee79ebd0693f1554..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49887 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