From 126b25a11901f775145226d74ad455e51cf08328 Mon Sep 17 00:00:00 2001 From: Guille Date: Wed, 17 May 2023 12:30:11 -0400 Subject: [PATCH] Structure refactor --- hub/DEPLOYMENT.md | 2 +- hub/__init__.py | 0 hub/imports/db_factory.py | 81 -------------- hub/imports/user_factory.py | 46 -------- .../db_control.py} | 103 +++++++++++++++++- hub/unittests/test_db_factory.py | 79 ++++++-------- hub/unittests/tests_data/pickle_path.bz2 | Bin 7062 -> 7520 bytes 7 files changed, 131 insertions(+), 180 deletions(-) create mode 100644 hub/__init__.py delete mode 100644 hub/imports/db_factory.py delete mode 100644 hub/imports/user_factory.py rename hub/{exports/db_factory.py => persistence/db_control.py} (50%) diff --git a/hub/DEPLOYMENT.md b/hub/DEPLOYMENT.md index 422b1aa7..e93fdb83 100644 --- a/hub/DEPLOYMENT.md +++ b/hub/DEPLOYMENT.md @@ -58,7 +58,7 @@ section in persistence/README.md file. as shown below: ```python -from hub.exports.db_factory import DBFactory +from hub.persistence.db_control import DBFactory from pathlib import Path dotenv_path = (Path(__file__).parent / '.env').resolve() diff --git a/hub/__init__.py b/hub/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hub/imports/db_factory.py b/hub/imports/db_factory.py deleted file mode 100644 index e58ce073..00000000 --- a/hub/imports/db_factory.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -DBFactory performs database create, delete and update operations -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Concordia CERC group -Project CoderPeter Yefi peteryefi@gmail.com -""" -from hub.city_model_structure.city import City -from hub.persistence import City as CityRepository -from hub.persistence import SimulationResults -from hub.persistence import Application - - -class DBFactory: - """ - DBFactory class - """ - - def __init__(self, db_name, dotenv_path, app_env): - self._city_repository = CityRepository(db_name=db_name, dotenv_path=dotenv_path, app_env=app_env) - self._simulation_results = SimulationResults(db_name=db_name, dotenv_path=dotenv_path, app_env=app_env) - self._application = Application(db_name=db_name, dotenv_path=dotenv_path, app_env=app_env) - - def persist_city(self, city: City, pickle_path, application_id: int, user_id: int): - """ - Persist city into postgres database - :param city: City to be stored - :param pickle_path: Path to save the pickle file - :param application_id: Application id owning this city - :param user_id: User who create the city - """ - return self._city_repository.insert(city, pickle_path, application_id, user_id) - - def update_city(self, city_id, city): - """ - Update an existing city in postgres database - :param city_id: the id of the city to update - :param city: the updated city object - """ - return self._city_repository.update(city_id, city) - - def persist_application(self, name: str, description: str, application_uuid: str): - """ - Creates an application - :param name: name of application - :param description: the description of the application - :param application_uuid: the uuid of the application to be created - """ - return self._application.insert(name, description, application_uuid) - - def update_application(self, name: str, description: str, application_uuid: str): - """ - Update an application - :param name: name of application - :param description: the description of the application - :param application_uuid: the uuid of the application to be created - """ - return self._application.update(application_uuid, name, description) - - def delete_city(self, city_id): - """ - Deletes a single city from postgres - :param city_id: the id of the city to get - """ - self._city_repository.delete(city_id) - - def delete_application(self, application_uuid): - """ - Deletes a single application from postgres - :param application_uuid: the id of the application to get - """ - self._application.delete(application_uuid) - - def add_simulation_results(self, name, values, city_id=None, city_object_id=None): - """ - Add simulation results to the city or to the city_object - :param name: simulation and simulation engine name - :param values: simulation values in json format - :param city_id: city id or None - :param city_object_id: city object id or None - """ - self._simulation_results.insert(name, values, city_id, city_object_id) diff --git a/hub/imports/user_factory.py b/hub/imports/user_factory.py deleted file mode 100644 index ebd00087..00000000 --- a/hub/imports/user_factory.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -User performs user-related crud operations -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Concordia CERC group -Project CoderPeter Yefi peteryefi@gmail.com -""" - -from hub.persistence import User -from hub.persistence import UserRoles - - -class UserFactory: - """ - UserFactory class - """ - - def __init__(self, db_name, app_env, dotenv_path): - self._user_repo = User(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path) - - def create_user(self, name: str, application_id: int, password: str, role: UserRoles): - """ - Creates a new user - :param name: the name of the user - :param application_id: the application id of the user - :param password: the password of the user - :param role: the role of the user - """ - return self._user_repo.insert(name, password, role, application_id) - - def update_user(self, user_id: int, name: str, password: str, role: UserRoles): - """ - Creates a new user - :param user_id: the id of the user - :param name: the name of the user - :param email: the email of the user - :param password: the password of the user - :param role: the role of the user - """ - return self._user_repo.update(user_id, name, password, role) - - def delete_user(self, user_id): - """ - Retrieve a single user - :param user_id: the id of the user to delete - """ - return self._user_repo.delete(user_id) diff --git a/hub/exports/db_factory.py b/hub/persistence/db_control.py similarity index 50% rename from hub/exports/db_factory.py rename to hub/persistence/db_control.py index 959d1906..06471fde 100644 --- a/hub/exports/db_factory.py +++ b/hub/persistence/db_control.py @@ -7,20 +7,22 @@ Project CoderPeter Yefi peteryefi@gmail.com import json from typing import Union, Dict -from hub.persistence import City +from hub.city_model_structure.city import City from hub.persistence import Application -from hub.persistence import User +from hub.persistence import City as CityRepository from hub.persistence import CityObject from hub.persistence import SimulationResults +from hub.persistence import User +from hub.persistence import UserRoles -class DBFactory: +class DBControl: """ DBFactory class """ def __init__(self, db_name, app_env, dotenv_path): - self._city = City(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path) + self._city_repository = CityRepository(db_name=db_name, dotenv_path=dotenv_path, app_env=app_env) self._application = Application(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path) self._user = User(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path) self._city_object = CityObject(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path) @@ -61,7 +63,7 @@ class DBFactory: :param application_id: Application id :return: [City] """ - return self._city.get_by_user_id_and_application_id(user_id, application_id) + return self._city_repository.get_by_user_id_and_application_id(user_id, application_id) def building_info(self, name, city_id) -> Union[CityObject, None]: """ @@ -85,7 +87,7 @@ class DBFactory: results = {} for city in cities['cities']: city_name = next(iter(city)) - result_set = self._city.get_by_user_id_application_id_and_name(user_id, application_id, city_name) + result_set = self._city_repository.get_by_user_id_application_id_and_name(user_id, application_id, city_name) if result_set is None: continue city_id = result_set.id @@ -104,3 +106,92 @@ class DBFactory: values["building"] = building_name results[city_name].append(values) return results + + def persist_city(self, city: City, pickle_path, application_id: int, user_id: int): + """ + Persist city into postgres database + :param city: City to be stored + :param pickle_path: Path to save the pickle file + :param application_id: Application id owning this city + :param user_id: User who create the city + """ + return self._city_repository.insert(city, pickle_path, application_id, user_id) + + def update_city(self, city_id, city): + """ + Update an existing city in postgres database + :param city_id: the id of the city to update + :param city: the updated city object + """ + return self._city_repository.update(city_id, city) + + def persist_application(self, name: str, description: str, application_uuid: str): + """ + Creates an application + :param name: name of application + :param description: the description of the application + :param application_uuid: the uuid of the application to be created + """ + return self._application.insert(name, description, application_uuid) + + def update_application(self, name: str, description: str, application_uuid: str): + """ + Update an application + :param name: name of application + :param description: the description of the application + :param application_uuid: the uuid of the application to be created + """ + return self._application.update(application_uuid, name, description) + + def delete_city(self, city_id): + """ + Deletes a single city from postgres + :param city_id: the id of the city to get + """ + self._city_repository.delete(city_id) + + def delete_application(self, application_uuid): + """ + Deletes a single application from postgres + :param application_uuid: the id of the application to get + """ + self._application.delete(application_uuid) + + def add_simulation_results(self, name, values, city_id=None, city_object_id=None): + """ + Add simulation results to the city or to the city_object + :param name: simulation and simulation engine name + :param values: simulation values in json format + :param city_id: city id or None + :param city_object_id: city object id or None + """ + self._simulation_results.insert(name, values, city_id, city_object_id) + + def create_user(self, name: str, application_id: int, password: str, role: UserRoles): + """ + Creates a new user + :param name: the name of the user + :param application_id: the application id of the user + :param password: the password of the user + :param role: the role of the user + """ + return self._user.insert(name, password, role, application_id) + + def update_user(self, user_id: int, name: str, password: str, role: UserRoles): + """ + Creates a new user + :param user_id: the id of the user + :param name: the name of the user + :param email: the email of the user + :param password: the password of the user + :param role: the role of the user + """ + return self._user.update(user_id, name, password, role) + + def delete_user(self, user_id): + """ + Retrieve a single user + :param user_id: the id of the user to delete + """ + return self._user.delete(user_id) + diff --git a/hub/unittests/test_db_factory.py b/hub/unittests/test_db_factory.py index 391f21ff..6c373ceb 100644 --- a/hub/unittests/test_db_factory.py +++ b/hub/unittests/test_db_factory.py @@ -13,9 +13,7 @@ import sqlalchemy.exc from hub.imports.geometry_factory import GeometryFactory from hub.imports.construction_factory import ConstructionFactory from hub.imports.usage_factory import UsageFactory -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.db_control import DBControl from hub.persistence.repository import Repository from sqlalchemy import create_engine from hub.persistence.models import City, Application, CityObject @@ -25,7 +23,7 @@ from sqlalchemy.exc import ProgrammingError import uuid -class Configure: +class Control: _skip_test = False _skip_reason = 'PostgreSQL not properly installed in host machine' @@ -68,30 +66,19 @@ setup ConstructionFactory('nrcan', self._city).enrich() UsageFactory('nrcan', self._city).enrich() - 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( + self._database = DBControl( 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._application = self._database.persist_application("test", "test application", self.unique_id) + self._user = self._database.create_user("Admin", self.application.id, "Admin@123", UserRoles.Admin) self._pickle_path = 'tests_data/pickle_path.bz2' @property - def import_db_factory(self): - return self._import_db_factory - - @property - def export_db_factory(self): - return self._export_db_factory + def database(self): + return self._database @property def unique_id(self): @@ -126,7 +113,7 @@ setup return self._pickle_path -configure = Configure() +control = Control() class TestDBFactory(TestCase): @@ -134,38 +121,38 @@ class TestDBFactory(TestCase): TestDBFactory """ - @unittest.skipIf(configure.skip_test, configure.skip_reason) + @unittest.skipIf(control.skip_test, control.skip_reason) def test_save_application(self): - self.assertEqual(configure.application.name, "test") - self.assertEqual(configure.application.description, "test application") - self.assertEqual(str(configure.application.application_uuid), configure.unique_id) + self.assertEqual(control.application.name, "test") + self.assertEqual(control.application.description, "test application") + self.assertEqual(str(control.application.application_uuid), control.unique_id) - @unittest.skipIf(configure.skip_test, configure.skip_reason) + @unittest.skipIf(control.skip_test, control.skip_reason) def test_save_city(self): - configure.city.name = "Montreal" - city = configure.import_db_factory.persist_city( - configure.city, - configure.pickle_path, - configure.application.id, - configure.user.id) + control.city.name = "Montreal" + city = control.database.persist_city( + control.city, + control.pickle_path, + control.application.id, + control.user.id) self.assertEqual(city.name, 'Montreal') - self.assertEqual(city.pickle_path, configure.pickle_path) - self.assertEqual(city.level_of_detail, configure.city.level_of_detail.geometry) - configure.import_db_factory.delete_city(city.id) + self.assertEqual(city.pickle_path, control.pickle_path) + self.assertEqual(city.level_of_detail, control.city.level_of_detail.geometry) + control.database.delete_city(city.id) - @unittest.skipIf(configure.skip_test, configure.skip_reason) + @unittest.skipIf(control.skip_test, control.skip_reason) def test_get_update_city(self): - city = configure.import_db_factory.persist_city(configure.city, - configure.pickle_path, - configure.application.id, - configure._user.id) + city = control.database.persist_city(control.city, + control.pickle_path, + control.application.id, + control._user.id) city.name = "Ottawa" - configure.import_db_factory.update_city(city.id, configure.city) - cities = configure.export_db_factory.cities_by_user_and_application( - configure.user.id, - configure.application.id) + control.database.update_city(city.id, control.city) + cities = control.database.cities_by_user_and_application( + control.user.id, + control.application.id) for updated_city in cities: if updated_city.id == city.id: self.assertEqual(updated_city.name, city.name) break - configure.import_db_factory.delete_city(city.id) + control.database.delete_city(city.id) diff --git a/hub/unittests/tests_data/pickle_path.bz2 b/hub/unittests/tests_data/pickle_path.bz2 index df2daadfcad9e0c37bc0d6ddd17e457fccb857a0..c88706a3198d9dfcf5e93abd2f4d770b83ff0c04 100644 GIT binary patch literal 7520 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