diff --git a/hub/CONTRIBUTING_EXTERNALS.md b/hub/CONTRIBUTING_EXTERNALS.md index 47694bf4..48fb5112 100644 --- a/hub/CONTRIBUTING_EXTERNALS.md +++ b/hub/CONTRIBUTING_EXTERNALS.md @@ -90,7 +90,7 @@ pylint --rcfile=pylintrc myfile.py Before any pull request, the code must been manually and automatically tested to ensure at least some quality minimum. There are a few practices for unit tests that we believe are important, so we encourage you to follow it. -* The test should be self-contained, which implies that your tests will prepare and clean up everything before and after the test execution. +* The test should be cls-contained, which implies that your tests will prepare and clean up everything before and after the test execution. * We encourage you to create if possible functional tests that cover the complete workflow of the implemented functionality. * Maximize your code coverage by ensuring that you are testing as much of your code as possible. diff --git a/hub/PYGUIDE.md b/hub/PYGUIDE.md index 85a6113c..60c3dfdd 100644 --- a/hub/PYGUIDE.md +++ b/hub/PYGUIDE.md @@ -48,11 +48,11 @@ Use properties whenever it is possible. Encapsulate the access to all the calcul ```python @property - def object_attribute(self): - if self._object_attribute is None: - self._object_attribute = ... + def object_attribute(cls): + if cls._object_attribute is None: + cls._object_attribute = ... ... - return self._object_attribute + return cls._object_attribute ``` @@ -61,12 +61,12 @@ And like in the following example for read and write properties: ```python @property - def object_changeable_attribute(self): - return self._object_changeable_attribute + def object_changeable_attribute(cls): + return cls._object_changeable_attribute @object_changeable_attribute.setter - def object_changeable_attribute(self, value): - self._object_changeable_attribute = value + def object_changeable_attribute(cls, value): + cls._object_changeable_attribute = value ``` @@ -75,11 +75,11 @@ If your method or attribute returns a complex object, use type hints as in this ```python @property - def complex_object(self) -> ComplexObject: - return self._object_changeable_attribute + def complex_object(cls) -> ComplexObject: + return cls._object_changeable_attribute - def new_complex_object(self, first_param, second_param) -> ComplexObject: - other_needed_property = self.other_needed_property + def new_complex_object(cls, first_param, second_param) -> ComplexObject: + other_needed_property = cls.other_needed_property return ComplexObject(first_param, second_param, other_needed_property) ``` @@ -89,11 +89,11 @@ Always access your variable through the method and avoid to access directly. ```python @property - def object_attribute(self): - return self._object_attribute + def object_attribute(cls): + return cls._object_attribute - def operation(self, first_param, second_param): - return self.object_attribute * 2 + def operation(cls, first_param, second_param): + return cls.object_attribute * 2 ``` @@ -110,23 +110,23 @@ All public classes, properties, and methods must have code comments. Code commen MyClass class perform models class operations """ - def __init__(self): + def __init__(cls): @property - def object_attribute(self): + def object_attribute(cls): """ Get my class object attribute :return: int """ - return self._object_attribute + return cls._object_attribute - def operation(self, first_param, second_param): + def operation(cls, first_param, second_param): """ Multiplies object_attribute by two :return: int """ - return self.object_attribute * 2 + return cls.object_attribute * 2 ``` @@ -135,20 +135,20 @@ Comments at getters and setters always start with Get and Set, and identity the ```python @property - def object_attribute(self): + def object_attribute(cls): """ Get object attribute :return: int """ - return self._object_attribute + return cls._object_attribute @object_attribute.setter - def object_attribute(self, value): + def object_attribute(cls, value): """ Set object attribute :param value: int """ - self._object_attribute = value + cls._object_attribute = value ``` @@ -157,12 +157,12 @@ Attributes with known units should be explicit in method's comment. ```python @property - def distance(self): + def distance(cls): """ My class distance in meters :return: float """ - return self._distance + return cls._distance ``` #### To do's. diff --git a/hub/helpers/geometry_helper.py b/hub/helpers/geometry_helper.py index e9b9a6d9..ade42a16 100644 --- a/hub/helpers/geometry_helper.py +++ b/hub/helpers/geometry_helper.py @@ -70,13 +70,8 @@ class GeometryHelper: @staticmethod def city_mapping(city, building_names=None, plot=False): - """ - - Returns a shared_information dictionary like - { - "building_name" : [{line: 0 coordinate_1: [x,y,z], coordinate_2:[x, y, z], points: 0}] - } + Returns a shared_information dictionary """ lines_information = {} if building_names is None: diff --git a/hub/persistence/configuration.py b/hub/persistence/configuration.py index a48b2c91..f1da1642 100644 --- a/hub/persistence/configuration.py +++ b/hub/persistence/configuration.py @@ -6,6 +6,7 @@ Project Coder Peter Yefi peteryefi@gmail.com """ import os +from pathlib import Path from dotenv import load_dotenv from sqlalchemy.ext.declarative import declarative_base from hub.hub_logger import logger @@ -25,7 +26,11 @@ class Configuration: """ try: # load environmental variables + if not Path(dotenv_path).exists(): + logger.error(f'File not exist: {dotenv_path}') + raise FileNotFoundError(f'dotenv file doesn\'t exists at {dotenv_path}') load_dotenv(dotenv_path=dotenv_path) + self._db_name = db_name self._db_host = os.getenv(f'{app_env}_DB_HOST') self._db_user = os.getenv(f'{app_env}_DB_USER') @@ -35,14 +40,26 @@ class Configuration: except KeyError as err: logger.error(f'Error with credentials: {err}') - def conn_string(self): + @property + def connection_string(self): + """ + Returns a connection string postgresql + :return: connection string """ - Returns a connection string postgresql - :return: connection string - """ if self._db_pass: return f'postgresql://{self._db_user}:{self._db_pass}@{self._db_host}:{self._db_port}/{self._db_name}' return f'postgresql://{self._db_user}@{self._db_host}:{self._db_port}/{self._db_name}' - def get_db_user(self): + @property + def db_user(self): + """ + retrieve the configured user name + """ return self._db_user + + @property + def db_name(self): + """ + retrieve the configured database name + """ + return self._db_name diff --git a/hub/persistence/models/city_object.py b/hub/persistence/models/city_object.py index 7229a2ac..64eb5905 100644 --- a/hub/persistence/models/city_object.py +++ b/hub/persistence/models/city_object.py @@ -9,6 +9,8 @@ import datetime from sqlalchemy import Column, Integer, String, Sequence, ForeignKey, Float from sqlalchemy import DateTime + +from hub.city_model_structure.building import Building from hub.persistence.configuration import Models class CityObject(Models): @@ -26,16 +28,21 @@ class CityObject(Models): usage = Column(String, nullable=True) volume = Column(Float, nullable=False) area = Column(Float, nullable=False) + total_heating_area = Column(Float, nullable=False) + wall_area = Column(Float, nullable=False) + windows_area = Column(Float, nullable=False) + system_name = Column(String, nullable=False) created = Column(DateTime, default=datetime.datetime.utcnow) updated = Column(DateTime, default=datetime.datetime.utcnow) - def __init__(self, city_id, name, alias, object_type, year_of_construction, function, usage, volume, area): + # def __init__(self, city_id, name, alias, object_type, year_of_construction, function, usage, volume, area): + def __init__(self, city_id, building: Building): self.city_id = city_id - self.name = name - self.alias = alias - self.type = object_type - self.year_of_construction = year_of_construction - self.function = function - self.usage = usage - self.volume = volume - self.area = area + self.name = building.name + self.alias = building.alias + self.type = building.type + self.year_of_construction = building.year_of_construction + self.function = building.function + self.usage = building.usages_percentage + self.volume = building.volume + self.area = building.floor_area diff --git a/hub/persistence/repositories/city.py b/hub/persistence/repositories/city.py index a242286a..7515bfef 100644 --- a/hub/persistence/repositories/city.py +++ b/hub/persistence/repositories/city.py @@ -57,23 +57,8 @@ class City(Repository): self.session.flush() self.session.commit() for building in city.buildings: - object_usage = '' - for internal_zone in building.internal_zones: - if internal_zone is None or internal_zone.usages is None: - object_usage = 'Unknown' - else: - for usage in internal_zone.usages: - object_usage = f'{object_usage}{usage.name}_{usage.percentage} ' - object_usage = object_usage.rstrip() db_city_object = CityObject(db_city.id, - building.name, - building.alias, - building.type, - building.year_of_construction, - building.function, - object_usage, - building.volume, - building.floor_area) + building) self.session.add(db_city_object) self.session.flush() self.session.commit() diff --git a/hub/persistence/repository.py b/hub/persistence/repository.py index d9cea244..e7c7332f 100644 --- a/hub/persistence/repository.py +++ b/hub/persistence/repository.py @@ -15,7 +15,7 @@ class Repository: def __init__(self, db_name, dotenv_path: str, app_env='TEST'): try: self.configuration = Configuration(db_name, dotenv_path, app_env) - self.engine = create_engine(self.configuration.conn_string()) + self.engine = create_engine(self.configuration.connection_string) self.session = Session(self.engine) except ValueError as err: print(f'Missing value for credentials: {err}') diff --git a/hub/unittests/test_db_factory.py b/hub/unittests/test_db_factory.py index ee5730af..6e92c294 100644 --- a/hub/unittests/test_db_factory.py +++ b/hub/unittests/test_db_factory.py @@ -1,16 +1,17 @@ """ -Test EnergySystemsFactory and various heatpump models +Test db factory SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Peter Yefi peteryefi@gmail.com """ +import os import unittest from unittest import TestCase - +from pathlib import Path import sqlalchemy.exc from hub.imports.geometry_factory import GeometryFactory -from hub.imports.db_factory import DBFactory +from hub.imports.db_factory import DBFactory as ImportDBFactory from hub.imports.user_factory import UserFactory from hub.exports.db_factory import DBFactory as ExportDBFactory from hub.persistence.repository import Repository @@ -20,103 +21,125 @@ from hub.persistence.models import User, UserRoles from sqlalchemy.exc import ProgrammingError import uuid -class Skip: +class Configure: - _value = False - _message = 'PostgreSQL not properly installed in host machine' + _skip_test = False + _skip_reason = 'PostgreSQL not properly installed in host machine' def __init__(self): + """ + Test + setup + :return: None + """ + self._skip_test = False # Create test database - env = '/usr/local/etc/hub/.env' - repo = Repository(db_name='test_db', app_env='TEST', dotenv_path=env) - eng = create_engine(f'postgresql://{repo.configuration.get_db_user()}@/{repo.configuration.get_db_user()}') + dotenv_path = str(Path("{}/.local/etc/hub/.env".format(os.path.expanduser('~'))).resolve()) + repository = Repository(db_name='hub_unittest', app_env='TEST', dotenv_path=dotenv_path) + engine = create_engine(repository.configuration.connection_string) try: # delete test database if it exists - conn = eng.connect() - conn.execute('commit') - conn.execute('DROP DATABASE test_db') - conn.close() - except ProgrammingError as err: + connection = engine.connect() + connection.execute('commit') + connection.close() + except ProgrammingError: print(f'Database does not exist. Nothing to delete') - except sqlalchemy.exc.OperationalError: - self._value = True + except sqlalchemy.exc.OperationalError as operational_error: + self._skip_test = True + self._skip_reason = f'{operational_error}' + return + + Application.__table__.create(bind=repository.engine, checkfirst=True) + User.__table__.create(bind=repository.engine, checkfirst=True) + City.__table__.create(bind=repository.engine, checkfirst=True) + CityObject.__table__.create(bind=repository.engine, checkfirst=True) + + city_file = "tests_data/FZK_Haus_LoD_2.gml" + self._city = GeometryFactory('citygml', city_file).city + self._import_db_factory = ImportDBFactory( + db_name=repository.configuration.db_name, + app_env='TEST', + dotenv_path=dotenv_path) + self._export_db_factory = ExportDBFactory( + db_name=repository.configuration.db_name, + app_env='TEST', + dotenv_path=dotenv_path) + user_factory = UserFactory( + db_name=repository.configuration.db_name, + app_env='TEST', + dotenv_path=dotenv_path) + self._unique_id = str(uuid.uuid4()) + self._application = self.import_db_factory.persist_application("test", "test application", self.unique_id) + self._user = user_factory.create_user("Admin", self.application.id, "Admin@123", UserRoles.Admin) + self._pickle_path = 'tests_data/pickle_path.bz2' @property - def value(self): - return self._value + def import_db_factory(self): + return self._import_db_factory + + @property + def export_db_factory(self): + return self._export_db_factory + + @property + def unique_id(self): + return self._unique_id + + @property + def application(self): + return self._application + + @property + def user(self): + return self._user + + @property + def skip_test(self): + return self._skip_test + + @property + def skip_reason(self): + return self._skip_reason @property def message(self): - return self._message + return self._skip_message - @value.setter - def value(self, skip_value): - self._value = skip_value + @property + def city(self): + return self._city -skip = Skip() + @property + def pickle_path(self): + return self._pickle_path + +configure = Configure() class TestDBFactory(TestCase): """ TestDBFactory """ - @classmethod - def setUpClass(cls) -> None: - """ - Test setup - :return: None - """ - # Create test database - env = '/usr/local/etc/hub/.env' - repo = Repository(db_name='test_db', app_env='TEST', dotenv_path=env) - eng = create_engine(f'postgresql://{repo.configuration.get_db_user()}@/{repo.configuration.get_db_user()}') - try: - # delete test database if it exists - conn = eng.connect() - conn.execute('commit') - conn.execute('DROP DATABASE test_db') - conn.close() - except ProgrammingError as err: - print(f'Database does not exist. Nothing to delete') - except sqlalchemy.exc.OperationalError: - skip.value = True - return - cnn = eng.connect() - cnn.execute('commit') - cnn.execute("CREATE DATABASE test_db") - cnn.close() - - Application.__table__.create(bind=repo.engine, checkfirst=True) - User.__table__.create(bind=repo.engine, checkfirst=True) - City.__table__.create(bind=repo.engine, checkfirst=True) - CityObject.__table__.create(bind=repo.engine, checkfirst=True) - - city_file = "tests_data/C40_Final.gml" - cls.city = GeometryFactory('citygml', city_file).city - cls._db_factory = DBFactory(db_name='test_db', app_env='TEST', dotenv_path=env) - cls._export_db_factory = ExportDBFactory(db_name='test_db', app_env='TEST', dotenv_path=env) - user_factory = UserFactory(db_name='test_db', app_env='TEST', dotenv_path=env) - cls.unique_id = str(uuid.uuid4()) - cls.application = cls._db_factory.persist_application("test", "test application", cls.unique_id) - cls._user = user_factory.create_user("Admin", cls.application.id, "Admin@123", UserRoles.Admin) - cls.pickle_path = 'tests_data/pickle_path.bz2' - - @unittest.skipIf(skip.value, skip.message) + @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_save_application(self): - self.assertEqual(self.application.name, "test") - self.assertEqual(self.application.description, "test application") - self.assertEqual(str(self.application.application_uuid), self.unique_id) + self.assertEqual(configure.application.name, "test") + self.assertEqual(configure.application.description, "test application") + self.assertEqual(str(configure.application.application_uuid), configure.unique_id) - @unittest.skipIf(skip.value, skip.message) + @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_save_city(self): - self.city.name = "Montréal" - saved_city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) - self.assertEqual(saved_city.name, 'Montréal') + configure.city.name = "Montreal" + saved_city = configure.import_db_factory.persist_city( + configure.city, + configure.pickle_path, + configure.application.id, + configure.user.id) + self.assertEqual(saved_city.name, 'Montreal') self.assertEqual(saved_city.pickle_path, self.pickle_path) self.assertEqual(saved_city.level_of_detail, self.city.level_of_detail.geometry) self._db_factory.delete_city(saved_city.id) - @unittest.skipIf(skip.value, skip.message) + @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_get_city_by_name(self): city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) retrieved_city = self._export_db_factory.get_city_by_name(city.name) @@ -124,21 +147,21 @@ class TestDBFactory(TestCase): self.assertEqual(retrieved_city[0].user_id, self._user.id) self._db_factory.delete_city(city.id) - @unittest.skipIf(skip.value, skip.message) + @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_get_city_by_user(self): - city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) + city = self._import_db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) retrieved_city = self._export_db_factory.get_city_by_user(self._user.id) self.assertEqual(retrieved_city[0].pickle_path, self.pickle_path) self._db_factory.delete_city(city.id) - @unittest.skipIf(skip.value, skip.message) + @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_get_city_by_id(self): city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) retrieved_city = self._export_db_factory.get_city(city.id) self.assertEqual(retrieved_city.level_of_detail, self.city.level_of_detail.geometry) self._db_factory.delete_city(city.id) - @unittest.skipIf(skip.value, skip.message) + @unittest.skipIf(configure.skip_test, configure.skip_reason) def test_get_update_city(self): city = self._db_factory.persist_city(self.city, self.pickle_path, self.application.id, self._user.id) self.city.name = "Ottawa" diff --git a/hub/unittests/tests_data/pickle_path.bz2 b/hub/unittests/tests_data/pickle_path.bz2 index b4916489..8a80bdae 100644 Binary files a/hub/unittests/tests_data/pickle_path.bz2 and b/hub/unittests/tests_data/pickle_path.bz2 differ