diff --git a/db_migration.py b/db_migration.py index 459fcd09..30021a57 100644 --- a/db_migration.py +++ b/db_migration.py @@ -11,8 +11,7 @@ from persistence.models import Building from persistence.models import City if __name__ == '__main__': - config = BaseConfiguration() + config = BaseConfiguration(db_name='peteryefi') engine = create_engine(config.conn_string()) City.__table__.create(bind=engine, checkfirst=True) - Building.__table__.create(bind=engine, checkfirst=True) diff --git a/helpers/city_util.py b/helpers/city_util.py index 74530476..9c69f714 100644 --- a/helpers/city_util.py +++ b/helpers/city_util.py @@ -4,10 +4,21 @@ from typing import Dict, List, Union import numpy as np -class CityUtil: +class CityUtil(object): + # holds a single instance of this class + _instance = None + def __init__(self): pass + def __new__(cls): + """ + Implemented for a singleton pattern + """ + if cls._instance is None: + cls._instance = super(CityUtil, cls).__new__(cls) + return cls._instance + def _class_object_to_dict(self, class_obj) -> Union[Dict, None]: """ converts a class object to a dictionary @@ -34,6 +45,16 @@ class CityUtil: return building_dict[key] return None + def object_list_to_dict_list(self, object_list): + """ + converts a list of objects to a list of dictionaries + :param object_list: a list of objects + :return: + """ + if object_list is None: + return [] + return [self._class_object_to_dict(obj) for obj in object_list] + def _serialize_points(self, points) -> List: """ Deserializes arry of Point objects to array of dictionarier diff --git a/persistence/README.md b/persistence/README.md index 42d750f6..d7312f44 100644 --- a/persistence/README.md +++ b/persistence/README.md @@ -21,7 +21,6 @@ This Python file is in the root of Hub and should be run to create all the requi ### Database Configuration Parameter ### A .env file (or environment variables) with configuration parameters described below are needed to establish a database connection: ``` -DB_NAME=postgres-database-name DB_USER=postgres-database-user DB_PASSWORD=postgres-database-password DB_HOST=database-host diff --git a/persistence/__init__.py b/persistence/__init__.py index 9b8c510a..cbd57dff 100644 --- a/persistence/__init__.py +++ b/persistence/__init__.py @@ -1,3 +1,4 @@ -from .Base import BaseRepo -from .repositories.CityRepo import CityRepo -from .repositories.BuildingRepo import BuildingRepo \ No newline at end of file +from .base import BaseRepo +from .repositories.city_repo import CityRepo +from .repositories.building_repo import BuildingRepo +from .repositories.city_repo import CityRepo \ No newline at end of file diff --git a/persistence/Base.py b/persistence/base.py similarity index 81% rename from persistence/Base.py rename to persistence/base.py index 2fde7981..89478772 100644 --- a/persistence/Base.py +++ b/persistence/base.py @@ -11,7 +11,8 @@ from sqlalchemy.orm import Session class BaseRepo: - def __init__(self): - config = BaseConfiguration() + def __init__(self, db_name): + config = BaseConfiguration(db_name) engine = create_engine(config.conn_string()) + self.config = config self.session = Session(engine) diff --git a/persistence/db_config.py b/persistence/db_config.py index d4fc80d2..ab8afc07 100644 --- a/persistence/db_config.py +++ b/persistence/db_config.py @@ -20,12 +20,13 @@ class BaseConfiguration(object): """ Base configuration class to hold common persistence configuration """ - def __init__(self): - self._db_name = os.getenv('DB_NAME') + def __init__(self, db_name: str): + self._db_name = db_name self._db_host = os.getenv('DB_HOST') self._db_user = os.getenv('DB_USER') self._db_pass = os.getenv('DB_PASSWORD') self._db_port = os.getenv('DB_PORT') + self.hub_token = os.getenv('HUB_TOKEN') def conn_string(self): """ diff --git a/persistence/models/City.py b/persistence/models/City.py deleted file mode 100644 index bbcfa8e9..00000000 --- a/persistence/models/City.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Model representation of a City -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Concordia CERC group -Project Coder Peter Yefi peteryefi@gmail.com -""" - -from sqlalchemy import Column, Integer, String, Sequence, Float -from persistence.db_config import Base -from sqlalchemy.orm import relationship - - -class City(Base): - """A model representation of a city - - Attributes: - name The name of the city. - uid A unique identifier for each city object - location The location of the building. - country_code The country where the city is located. - latitude The GPS latitude location of the city. - longitude The GPS longitude location of the city. - building A relationship for fetching all buildings in the city - """ - __tablename__ = "city" - id = Column(Integer, Sequence('city_id_seq'), primary_key=True) - name = Column(String, nullable=False) - time_zone = Column(String, nullable=True) - country_code = Column(String, nullable=False) - latitude = Column(Float) - longitude = Column(Float) - buildings = relationship('Building', backref='city', lazy=True, cascade="all, delete-orphan") diff --git a/persistence/models/__init__.py b/persistence/models/__init__.py index 5ed1650e..2ae48ed8 100644 --- a/persistence/models/__init__.py +++ b/persistence/models/__init__.py @@ -1,2 +1,3 @@ -from .Building import Building -from .City import City +from .building import Building +from .city import City +from .city import City diff --git a/persistence/models/Building.py b/persistence/models/building.py similarity index 100% rename from persistence/models/Building.py rename to persistence/models/building.py diff --git a/persistence/models/city.py b/persistence/models/city.py new file mode 100644 index 00000000..515e65e8 --- /dev/null +++ b/persistence/models/city.py @@ -0,0 +1,30 @@ +""" +Model representation of a City +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Peter Yefi peteryefi@gmail.com +""" + +from sqlalchemy import Column, Integer, String, Sequence, PickleType, Float +from persistence.db_config import Base +from sqlalchemy.dialects.postgresql import JSONB + + +class City(Base): + """A model representation of a city + """ + __tablename__ = "city" + id = Column(Integer, Sequence('city_id_seq'), primary_key=True) + city = Column(PickleType, nullable=False) + name = Column(String, nullable=False) + srs_name = Column(String, nullable=False) + climate_reference_city = Column(String, nullable=True) + time_zone = Column(String, nullable=True) + country_code = Column(String, nullable=False) + latitude = Column(Float) + longitude = Column(Float) + lower_corner = Column(JSONB, nullable=False) + upper_corner = Column(JSONB, nullable=False) + hub_release = Column(String, nullable=False) + city_version = Column(Integer, nullable=False) + diff --git a/persistence/repositories/CityRepo.py b/persistence/repositories/CityRepo.py deleted file mode 100644 index 27367a27..00000000 --- a/persistence/repositories/CityRepo.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -City repository with database CRUD operations -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Concordia CERC group -Project Coder Peter Yefi peteryefi@gmail.com -""" - -from city_model_structure.city import City -from persistence.models import City as ModelCity -from persistence import BaseRepo -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy import select - - -class CityRepo(BaseRepo): - def __init__(self): - super().__init__() - - def insert(self, city: City): - model_city = ModelCity() - model_city.name = city.name - model_city.longitude = city.longitude - model_city.latitude = city.latitude - model_city.country_code = city.country_code - model_city.time_zone = city.time_zone - try: - self.session.add(model_city) - self.session.flush() - self.session.commit() - return model_city - except SQLAlchemyError as err: - print(f'Error while adding city: {err}') - - def get_by_id(self, city_id: int) -> ModelCity: - """ - Fetch a City based on the id - :param city_id: the city id - :return: a city - """ - try: - return self.session.execute(select(ModelCity).where(ModelCity.id == city_id)).first()[0] - except SQLAlchemyError as err: - print(f'Error while fetching city: {err}') - - def get_by_name(self, city_name: str) -> [ModelCity]: - """ - Fetch city based on the name - :param city_name: the name of the building - :return: [ModelCity] with the provided name - """ - try: - result_set = self.session.execute(select(ModelCity).where(ModelCity.name == city_name)) - return [building[0] for building in result_set] - except SQLAlchemyError as err: - print(f'Error while fetching city by name: {err}') diff --git a/persistence/repositories/BuildingRepo.py b/persistence/repositories/building_repo.py similarity index 98% rename from persistence/repositories/BuildingRepo.py rename to persistence/repositories/building_repo.py index 602ef739..bd7bbfe2 100644 --- a/persistence/repositories/BuildingRepo.py +++ b/persistence/repositories/building_repo.py @@ -15,8 +15,8 @@ from sqlalchemy import select class BuildingRepo(BaseRepo): - def __init__(self): - super().__init__() + def __init__(self, db_name): + super().__init__(db_name) self._city_util = CityUtil() def insert(self, building: CityBuilding, city_id: int) -> Building: diff --git a/persistence/repositories/city_repo.py b/persistence/repositories/city_repo.py new file mode 100644 index 00000000..2e474b16 --- /dev/null +++ b/persistence/repositories/city_repo.py @@ -0,0 +1,120 @@ +""" +City repository with database CRUD operations +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Peter Yefi peteryefi@gmail.com +""" + +from city_model_structure.city import City +from persistence import BaseRepo +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import select +from helpers.city_util import CityUtil +from persistence.models import City +import pickle +import requests +from urllib3.exceptions import HTTPError +from typing import Union, Dict + + +class CityRepo(BaseRepo): + def __init__(self, db_name): + super().__init__(db_name) + self._city_util = CityUtil() + + def insert(self, city: City) -> Union[City, Dict]: + model_city = City() + model_city.name = city.name + model_city.climate_reference_city = city.climate_reference_city + model_city.srs_name = city.srs_name + model_city.longitude = city.longitude + model_city.latitude = city.latitude + model_city.country_code = city.country_code + model_city.time_zone = city.time_zone + model_city.lower_corner = city.lower_corner.tolist() + model_city.upper_corner = city.upper_corner.tolist() + model_city.city = pickle.dumps(city) + + try: + # Retrieve hub project latest release + response = requests.get("https://rs-loy-gitlab.concordia.ca/api/v4/projects/2/repository/branches/master", + headers={"PRIVATE-TOKEN": self.config.hub_token}) + recent_commit = response.json()["commit"]["id"] + exiting_city = self._get_by_hub_version(recent_commit, city.name) + + # Do not persist the same city for the same version of Hub + if exiting_city is None: + model_city.hub_release = recent_commit + cities = self.get_by_name(city.name) + # update version for the same city but different hub versions + if len(cities) == 0: + model_city.city_version = 0 + else: + model_city.city_version = cities[-1].city_version + 1 + + # Persist city + self.session.add(model_city) + self.session.flush() + self.session.commit() + return model_city + else: + return {'message': f'Same version of {city.name} exist'} + except SQLAlchemyError as err: + print(f'Error while adding city: {err}') + except HTTPError as err: + print(f'Error retrieving Hub latest release: {err}') + + def get_by_id(self, city_id: int) -> City: + """ + Fetch a City based on the id + :param city_id: the city id + :return: a city + """ + try: + return self.session.execute(select(City).where(City.id == city_id)).first()[0] + except SQLAlchemyError as err: + print(f'Error while fetching city: {err}') + + def _get_by_hub_version(self, hub_commit: str, city_name: str) -> City: + """ + Fetch a City based on the name and hub project recent commit + :param hub_commit: the latest hub commit + :param city_name: the name of the city + :return: a city + """ + try: + return self.session.execute(select(City) + .where(City.hub_release == hub_commit, City.name == city_name)).first() + except SQLAlchemyError as err: + print(f'Error while fetching city: {err}') + + def update(self, city_id: int, city: City): + """ + Updates a city + :param city_id: the id of the city to be updated + :param city: the city object + :return: + """ + try: + self.session.query(City).filter(City.id == city_id) \ + .update({ + 'name': city.name, 'srs_name': city.srs_name, 'country_code': city.country_code, 'longitude': city.longitude, + 'latitude': city.latitude, 'time_zone': city.time_zone, 'lower_corner': city.lower_corner.tolist(), + 'upper_corner': city.upper_corner.tolist(), 'climate_reference_city': city.climate_reference_city, + }) + + self.session.commit() + except SQLAlchemyError as err: + print(f'Error while updating city: {err}') + + def get_by_name(self, city_name: str) -> [City]: + """ + Fetch city based on the name + :param city_name: the name of the building + :return: [ModelCity] with the provided name + """ + try: + result_set = self.session.execute(select(City).where(City.name == city_name)) + return [building[0] for building in result_set] + except SQLAlchemyError as err: + print(f'Error while fetching city by name: {err}')