From 139609ee0903255b4ae3d88e1926e614b9c86e45 Mon Sep 17 00:00:00 2001 From: Guille Date: Mon, 18 Mar 2024 11:26:40 +0100 Subject: [PATCH] Sync with cerc hub and overall code improvements --- .gitignore | 1 + cerc_persistence/__init__.py | 3 + cerc_persistence/db_control.py | 57 ++++++------- cerc_persistence/db_setup.py | 8 +- cerc_persistence/models/city_object.py | 3 +- cerc_persistence/repositories/__init__.py | 7 -- cerc_persistence/repositories/application.py | 10 ++- cerc_persistence/repositories/city.py | 13 ++- cerc_persistence/repositories/city_object.py | 82 ++++++++++++++++++- .../repositories/simulation_results.py | 46 +++++++++-- cerc_persistence/repositories/user.py | 6 +- tests/test_db_factory.py | 6 +- tests/test_db_retrieve.py | 19 ++--- 13 files changed, 182 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index 8edced5..9f831ba 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ **/__pycache__/ **/.idea/ ./dist +./cerc_persistence.egg-info \ No newline at end of file diff --git a/cerc_persistence/__init__.py b/cerc_persistence/__init__.py index e69de29..2fb3351 100644 --- a/cerc_persistence/__init__.py +++ b/cerc_persistence/__init__.py @@ -0,0 +1,3 @@ +""" +Repositories Package +""" diff --git a/cerc_persistence/db_control.py b/cerc_persistence/db_control.py index 521c368..49c187b 100644 --- a/cerc_persistence/db_control.py +++ b/cerc_persistence/db_control.py @@ -4,7 +4,6 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project CoderPeter Yefi peteryefi@gmail.com """ -import json from typing import Dict from cerc_persistence.repositories.application import Application @@ -74,10 +73,10 @@ class DBControl: : """ cities = self._city.get_by_user_id_application_id_and_scenario(user_id, application_id, scenario) - for city in cities: - result = self.building_info(name, city[0].id) - if result is not None: - return result + city = [city[0].id for city in cities] + result = self._city_object.building_in_cities_info(name, city) + if result is not None: + return result return None def building_info(self, name, city_id) -> CityObject: @@ -89,17 +88,14 @@ class DBControl: """ return self._city_object.get_by_name_or_alias_and_city(name, city_id) - def buildings_info(self, request_values, city_id) -> [CityObject]: + def building_info_in_cities(self, name, cities) -> CityObject: """ - Retrieve the buildings info from the database - :param request_values: Building names - :param city_id: City ID - :return: [CityObject] + Retrieve the building info from the database + :param name: Building name + :param cities: [City ID] + :return: CityObject """ - buildings = [] - for name in request_values['names']: - buildings.append(self.building_info(name, city_id)) - return buildings + return self._city_object.get_by_name_or_alias_in_cities(name, cities) def results(self, user_id, application_id, request_values, result_names=None) -> Dict: """ @@ -121,24 +117,21 @@ class DBControl: ) if result_sets is None: continue - for result_set in result_sets: - city_id = result_set[0].id + results[scenario_name] = [] + city_ids = [r[0].id for r in result_sets] + for building_name in scenario[scenario_name]: + _building = self._city_object.get_by_name_or_alias_in_cities(building_name, city_ids) + if _building is None: + continue + city_object_id = _building.id + _ = self._simulation_results.get_simulation_results_by_city_object_id_and_names( + city_object_id, + result_names) - results[scenario_name] = [] - for building_name in scenario[scenario_name]: - _building = self._city_object.get_by_name_or_alias_and_city(building_name, city_id) - if _building is None: - continue - city_object_id = _building.id - _ = self._simulation_results.get_simulation_results_by_city_id_city_object_id_and_names( - city_id, - city_object_id, - result_names) - - for value in _: - values = json.loads(value.values) - values["building"] = building_name - results[scenario_name].append(values) + for value in _: + values = value.values + values["building"] = building_name + results[scenario_name].append(values) return results def persist_city(self, city: City, pickle_path, scenario, application_id: int, user_id: int): @@ -151,7 +144,7 @@ class DBControl: :param user_id: User who create the city return identity_id """ - return self._city.insert(city, pickle_path, scenario, application_id, user_id) + return self._city.insert(city, pickle_path, scenario, application_id, user_id) def update_city(self, city_id, city): """ diff --git a/cerc_persistence/db_setup.py b/cerc_persistence/db_setup.py index 495f3ee..3b76fe1 100644 --- a/cerc_persistence/db_setup.py +++ b/cerc_persistence/db_setup.py @@ -50,14 +50,14 @@ class DBSetup: name = 'AdminTool' description = 'Admin tool to control city persistence and to test the API v1.4' logging.info('Creating default admin tool application...') - application_id = application_repo.insert(name, description, application_uuid) + application = application_repo.insert(name, description, application_uuid) - if isinstance(application_id, dict): - logging.info(application_id) + if isinstance(application, dict): + logging.info(application) else: msg = f'Created Admin tool with application_uuid: {application_uuid}' logging.info(msg) - return application_id + return application.id @staticmethod def _create_admin_user(user_repo, admin_password, application_id): diff --git a/cerc_persistence/models/city_object.py b/cerc_persistence/models/city_object.py index d245c93..0b602a3 100644 --- a/cerc_persistence/models/city_object.py +++ b/cerc_persistence/models/city_object.py @@ -50,8 +50,7 @@ class CityObject(Models): self.volume = building.volume self.area = building.floor_area self.roof_area = sum(roof.solid_polygon.area for roof in building.roofs) - self.total_pv_area = sum( - roof.solid_polygon.area * roof.solar_collectors_area_reduction_factor for roof in building.roofs) + self.total_pv_area = sum(roof.solid_polygon.area * roof.solar_collectors_area_reduction_factor for roof in building.roofs) storeys = building.storeys_above_ground wall_area = 0 window_ratio = 0 diff --git a/cerc_persistence/repositories/__init__.py b/cerc_persistence/repositories/__init__.py index e9bec62..2fb3351 100644 --- a/cerc_persistence/repositories/__init__.py +++ b/cerc_persistence/repositories/__init__.py @@ -1,10 +1,3 @@ """ Repositories Package """ -import datetime -import logging - -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session -from cerc_persistence.repository import Repository diff --git a/cerc_persistence/repositories/application.py b/cerc_persistence/repositories/application.py index 3ca672b..a3c3597 100644 --- a/cerc_persistence/repositories/application.py +++ b/cerc_persistence/repositories/application.py @@ -4,8 +4,16 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca """ + +import datetime +import logging + +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm.session import Session + +from cerc_persistence.repository import Repository from cerc_persistence.models import Application as Model -from . import datetime, logging, select, SQLAlchemyError, Session, Repository class Application(Repository): diff --git a/cerc_persistence/repositories/city.py b/cerc_persistence/repositories/city.py index e0686aa..bc9bdab 100644 --- a/cerc_persistence/repositories/city.py +++ b/cerc_persistence/repositories/city.py @@ -4,11 +4,18 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Peter Yefi peteryefi@gmail.com """ -from hub.city_model_structure.city import City as CityHub +import datetime +import logging + +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + from hub.version import __version__ +from hub.city_model_structure.city import City as CityHub +from cerc_persistence.repository import Repository from cerc_persistence.models import City as Model from cerc_persistence.models import CityObject -from . import datetime, logging, select, SQLAlchemyError, Session, Repository class City(Repository): @@ -33,7 +40,7 @@ class City(Repository): Inserts a city :param city: The complete city instance :param pickle_path: Path to the pickle - :param scenario: Simulation scenario name + param scenario: Simulation scenario name :param application_id: Application id owning the instance :param user_id: User id owning the instance :return: Identity id diff --git a/cerc_persistence/repositories/city_object.py b/cerc_persistence/repositories/city_object.py index afc7542..2521d8e 100644 --- a/cerc_persistence/repositories/city_object.py +++ b/cerc_persistence/repositories/city_object.py @@ -4,9 +4,17 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca """ +import datetime +import logging +from typing import Union + from hub.city_model_structure.building import Building +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + from cerc_persistence.models import CityObject as Model -from . import datetime, logging, select, SQLAlchemyError, Session, Repository +from cerc_persistence.repository import Repository class CityObject(Repository): @@ -65,7 +73,7 @@ class CityObject(Repository): with Session(self.engine) as session: session.query(Model).filter(Model.name == building.name, Model.city_id == city_id).update( {'name': building.name, - 'alias': building.alias, + 'aliases': building.aliases, 'object_type': building.type, 'year_of_construction': building.year_of_construction, 'function': building.function, @@ -90,10 +98,44 @@ class CityObject(Repository): session.query(Model).filter(Model.city_id == city_id, Model.name == name).delete() session.commit() except SQLAlchemyError as err: - logging.error('Error while deleting city_object %s', err) + logging.error('Error while deleting city object %s', err) raise SQLAlchemyError from err - def get_by_name_or_alias_and_city(self, name, city_id) -> Model: + def building_in_cities_info(self, name, cities): + """ + Fetch a city object based on name and city id + :param name: city object name + :param cities: city identifiers + :return: [CityObject] with the provided name or alias belonging to the city with id city_id + """ + try: + # search by name first + with Session(self.engine) as session: + city_object = session.execute(select(Model).where( + Model.name == name, Model.city_id.in_(cities)) + ).first() + if city_object is not None: + return city_object[0] + # name not found, so search by alias instead + city_objects = session.execute( + select(Model).where(Model.aliases.contains(name), Model.city_id.in_(cities)) + ).all() + for city_object in city_objects: + aliases = city_object[0].aliases.replace('{', '').replace('}', '').split(',') + for alias in aliases: + if alias == name: + # force the name as the alias + city_object[0].name = name + return city_object[0] + return None + except SQLAlchemyError as err: + logging.error('Error while fetching city object by name and city: %s', err) + raise SQLAlchemyError from err + except IndexError as err: + logging.error('Error while fetching city object by name and city, empty result %s', err) + raise IndexError from err + + def get_by_name_or_alias_and_city(self, name, city_id) -> Union[Model, None]: """ Fetch a city object based on name and city id :param name: city object name @@ -124,3 +166,35 @@ class CityObject(Repository): except IndexError as err: logging.error('Error while fetching city object by name and city, empty result %s', err) raise IndexError from err + + def get_by_name_or_alias_in_cities(self, name, city_ids) -> Model: + """ + Fetch a city object based on name and city ids + :param name: city object name + :param city_ids: a list of city identifiers + :return: [CityObject] with the provided name or alias belonging to the city with id city_id + """ + try: + # search by name first + with Session(self.engine) as session: + city_object = session.execute(select(Model).where(Model.name == name, Model.city_id.in_(tuple(city_ids)))).first() + if city_object is not None: + return city_object[0] + # name not found, so search by alias instead + city_objects = session.execute( + select(Model).where(Model.aliases.contains(name), Model.city_id.in_(tuple(city_ids))) + ).all() + for city_object in city_objects: + aliases = city_object[0].aliases.replace('{', '').replace('}', '').split(',') + for alias in aliases: + if alias == name: + # force the name as the alias + city_object[0].name = name + return city_object[0] + return None + except SQLAlchemyError as err: + logging.error('Error while fetching city object by name and city: %s', err) + raise SQLAlchemyError from err + except IndexError as err: + logging.error('Error while fetching city object by name and city, empty result %s', err) + raise IndexError from err diff --git a/cerc_persistence/repositories/simulation_results.py b/cerc_persistence/repositories/simulation_results.py index f8f0efd..f87e0be 100644 --- a/cerc_persistence/repositories/simulation_results.py +++ b/cerc_persistence/repositories/simulation_results.py @@ -4,11 +4,18 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca """ +import datetime +import logging + from sqlalchemy import or_ +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from cerc_persistence.repository import Repository from cerc_persistence.models import City from cerc_persistence.models import CityObject from cerc_persistence.models import SimulationResults as Model -from . import datetime, logging, select, SQLAlchemyError, Session, Repository class SimulationResults(Repository): @@ -69,10 +76,10 @@ class SimulationResults(Repository): with Session(self.engine) as session: if city_id is not None: session.query(Model).filter(Model.name == name, Model.city_id == city_id).update( - { - 'values': values, - 'updated': datetime.datetime.utcnow() - }) + { + 'values': values, + 'updated': datetime.datetime.utcnow() + }) session.commit() elif city_object_id is not None: session.query(Model).filter(Model.name == name, Model.city_object_id == city_object_id).update( @@ -84,7 +91,7 @@ class SimulationResults(Repository): else: raise NotImplementedError('Missing either city_id or city_object_id') except SQLAlchemyError as err: - logging.error('Error while updating simulation results %s', err) + logging.error('Error while updating simulation results for %s', err) raise SQLAlchemyError from err def delete(self, name: str, city_id=None, city_object_id=None): @@ -135,8 +142,7 @@ class SimulationResults(Repository): logging.error('Error while fetching city by city_id: %s', err) raise SQLAlchemyError from err - def get_simulation_results_by_city_id_city_object_id_and_names(self, city_id, city_object_id, - result_names=None) -> [Model]: + def get_simulation_results_by_city_id_city_object_id_and_names(self, city_id, city_object_id, result_names=None) -> [Model]: """ Fetch the simulation results based in the city_id or city_object_id with the given names or all :param city_id: the city id @@ -161,3 +167,27 @@ class SimulationResults(Repository): except SQLAlchemyError as err: logging.error('Error while fetching city by city_id: %s', err) raise SQLAlchemyError from err + + def get_simulation_results_by_city_object_id_and_names(self, city_object_id, result_names=None) -> [Model]: + """ + Fetch the simulation results based in the city_object_id with the given names or all + :param city_object_id: the city object id + :param result_names: if given filter the results + :return: [SimulationResult] + """ + try: + with Session(self.engine) as session: + result_set = session.execute(select(Model).where( + Model.city_object_id == city_object_id + )) + results = [r[0] for r in result_set] + if not result_names: + return results + filtered_results = [] + for result in results: + if result.name in result_names: + filtered_results.append(result) + return filtered_results + except SQLAlchemyError as err: + logging.error('Error while fetching city by city_id: %s', err) + raise SQLAlchemyError from err diff --git a/cerc_persistence/repositories/user.py b/cerc_persistence/repositories/user.py index d4cc87f..75d3a0e 100644 --- a/cerc_persistence/repositories/user.py +++ b/cerc_persistence/repositories/user.py @@ -4,12 +4,16 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Peter Yefi peteryefi@gmail.com """ +import datetime +import logging +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session from hub.helpers.auth import Auth from cerc_persistence.repository import Repository from cerc_persistence.models import User as Model, Application as ApplicationModel, UserRoles -from . import datetime, logging, select, SQLAlchemyError, Session, Repository class User(Repository): diff --git a/tests/test_db_factory.py b/tests/test_db_factory.py index a89295a..7eff3d2 100644 --- a/tests/test_db_factory.py +++ b/tests/test_db_factory.py @@ -279,10 +279,8 @@ TestDBFactory {'yearly_cooling_consumption': yearly_cooling_consumption}, {'monthly_domestic_hot_water_consumption': monthly_domestic_hot_water_consumption}, {'yearly_domestic_hot_water_consumption': yearly_domestic_hot_water_consumption}, - { - 'monthly_distribution_systems_electrical_consumption': monthly_distribution_systems_electrical_consumption}, - { - 'yearly_distribution_systems_electrical_consumption': yearly_distribution_systems_electrical_consumption}, + {'monthly_distribution_systems_electrical_consumption': monthly_distribution_systems_electrical_consumption}, + {'yearly_distribution_systems_electrical_consumption': yearly_distribution_systems_electrical_consumption}, {'monthly_on_site_electrical_production': monthly_on_site_electrical_production}, {'yearly_on_site_electrical_production': yearly_on_site_electrical_production} ]}) diff --git a/tests/test_db_retrieve.py b/tests/test_db_retrieve.py index c072939..161d766 100644 --- a/tests/test_db_retrieve.py +++ b/tests/test_db_retrieve.py @@ -4,6 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Ruben Sanchez ruben.sanchez@mail.concordia.ca """ +import datetime import distutils.spawn import logging import os @@ -114,21 +115,13 @@ class TestDBFactory(TestCase): @unittest.skipIf(control.skip_test, control.skip_reason) def test_retrieve_results(self): + datetime.datetime.now() request_values = { "scenarios": [ - { - "current status": ["01002777", "01002773", "01036804"] - }, - { - "skin retrofit": ["01002777", "01002773", "01036804"] - }, - { - "system retrofit and pv": ["01002777", "01002773", "01036804"] - }, - { - "skin and system retrofit with pv": ["01002777", "01002773", "01036804"] - } - + {"current status": ["01002777", "01002773", "01036804"]}, + {"skin retrofit": ["01002777", "01002773", "01036804"]}, + {"system retrofit and pv": ["01002777", "01002773", "01036804"]}, + {"skin and system retrofit with pv": ["01002777", "01002773", "01036804"]} ] } results = control.database.results(control.user_id, control.application_id, request_values)