Persistence refactory

This commit is contained in:
Guille Gutierrez 2023-02-01 06:05:12 -05:00
parent 5978757348
commit 672b9874f2
17 changed files with 143 additions and 247 deletions

View File

@ -1,6 +1,5 @@
from .base_repo import BaseRepo
from .repositories.city_repo import CityRepo
from .repositories.heat_pump_simulation_repo import HeatPumpSimulationRepo
from .repository import Repository
from .repositories.city import City
from .db_setup import DBSetup
from .repositories.user_repo import UserRepo
from .repositories.user import User
from .models.user import UserRoles

View File

@ -13,9 +13,9 @@ from hub.hub_logger import logger
Base = declarative_base()
class BaseConfiguration(object):
class Configuration:
"""
Base configuration class to hold common persistence configuration
Configuration class to hold common persistence configuration
"""
def __init__(self, db_name: str, dotenv_path: str, app_env='TEST'):

View File

@ -1,13 +1,19 @@
"""
Database setup
SPDX - License - Identifier: LGPL - 3.0 - or -later
Copyright © 2022 Concordia CERC group
Project Coder Peter Yefi peteryefi@gmail.com
"""
from hub.persistence import BaseRepo
from hub.persistence import Repository
from hub.persistence.models import Application
from hub.persistence.models import City
from hub.persistence.models import HeatPumpSimulation
from hub.persistence.models import CityObject
from hub.persistence.models import User
from hub.persistence.models import UserRoles
from hub.persistence.models import Application
from hub.persistence.models import UserApplications
from hub.persistence.repositories import UserRepo
from hub.persistence.repositories import ApplicationRepo
from hub.persistence.models import SimulationResults
from hub.persistence.repositories import User as UserRepository
from hub.persistence.repositories import Application as ApplicationRepository
from hub.hub_logger import logger
@ -20,14 +26,17 @@ class DBSetup:
:param app_env:
:param dotenv_path:
"""
repo = BaseRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
User.__table__.create(bind=repo.engine, checkfirst=True)
City.__table__.create(bind=repo.engine, checkfirst=True)
Application.__table__.create(bind=repo.engine, checkfirst=True)
UserApplications.__table__.create(bind=repo.engine, checkfirst=True)
HeatPumpSimulation.__table__.create(bind=repo.engine, checkfirst=True)
self._user_repo = UserRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
self._application_repo = ApplicationRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
repository = Repository(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
# Create the tables using the models
Application.__table__.create(bind=repository.engine, checkfirst=True)
City.__table__.create(bind=repository.engine, checkfirst=True)
CityObject.__table__.create(bind=repository.engine, checkfirst=True)
SimulationResults.__table__.create(bind=repository.engine, checkfirst=True)
User.__table__.create(bind=repository.engine, checkfirst=True)
self._user_repo = UserRepository(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
self._application_repo = ApplicationRepository(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
self._create_admin_user(self._user_repo, admin_password)
self._create_admin_app(self._application_repo, application_uuid)

View File

@ -1,8 +1,5 @@
from .city import City
from .heat_pump_simulation import HeatPumpSimulation
from .heat_pump_simulation import SimulationTypes
from .heat_pump_simulation import HeatPumpTypes
from .user import User, UserRoles
from .user_applications import UserApplications
from .application import Application
from .city import City
from .city_object import CityObject
from .simulation_results import SimulationResults
from .user import User, UserRoles

View File

@ -11,14 +11,12 @@ from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy import DateTime
from hub.persistence.db_config import Base
class Application(Base):
class Application:
"""
A model representation of an application
"""
__tablename__ = "application"
__table__ = "application"
id = Column(Integer, Sequence('application_id_seq'), primary_key=True)
name = Column(String, nullable=False)
description = Column(String, nullable=False)

View File

@ -10,13 +10,11 @@ import datetime
from sqlalchemy import Column, Integer, String, Sequence, ForeignKey
from sqlalchemy import DateTime, PickleType
from hub.persistence.db_config import Base
class City(Base):
class City:
"""A model representation of a city
"""
__tablename__ = "city"
__table__ = "city"
id = Column(Integer, Sequence('city_id_seq'), primary_key=True)
city = Column(PickleType, nullable=False)
name = Column(String, nullable=False)

View File

@ -10,14 +10,11 @@ import datetime
from sqlalchemy import Column, Integer, String, Sequence, ForeignKey, Float
from sqlalchemy import DateTime
from hub.persistence.db_config import Base
class CityObject(Base):
class CityObject:
"""
A model representation of an application
"""
__tablename__ = "city_object"
__table__ = "city_object"
id = Column(Integer, Sequence('application_id_seq'), primary_key=True)
city_id = Column(Integer, ForeignKey('city.id'), nullable=False)
name = Column(String, nullable=False)
@ -41,4 +38,3 @@ class CityObject(Base):
self.usage = usage
self.volume = volume
self.area = area

View File

@ -7,18 +7,15 @@ Project Coder Guille Gutierrez Guillermo.GutierrezMorote@concordia.ca
import datetime
from sqlalchemy import Column, Integer, String, Sequence, ForeignKey, Float
from sqlalchemy import Column, Integer, String, Sequence, ForeignKey
from sqlalchemy import DateTime
from sqlalchemy.dialects.postgresql import JSONB
from hub.persistence.db_config import Base
class CityObject(Base):
class SimulationResults:
"""
A model representation of an application
"""
__tablename__ = "simulation_results"
__table__ = "simulation_results"
id = Column(Integer, Sequence('application_id_seq'), primary_key=True)
city_id = Column(Integer, ForeignKey('city.id'), nullable=True)
city_object_id = Column(Integer, ForeignKey('city_object.id'), nullable=True)

View File

@ -5,14 +5,11 @@ Copyright © 2022 Concordia CERC group
Project Coder Peter Yefi peteryefi@gmail.com
"""
import datetime
import enum
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy import DateTime, Enum
from hub.persistence.db_config import Base
import datetime
from sqlalchemy.orm import validates
import re
import enum
from sqlalchemy.orm import relationship
class UserRoles(enum.Enum):
@ -20,10 +17,11 @@ class UserRoles(enum.Enum):
Hub_Reader = 'Hub_Reader'
class User(Base):
"""A model representation of a city
class User:
"""
__tablename__ = "user"
A model representation of a city
"""
__table__ = "user"
id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
name = Column(String, nullable=False)
password = Column(String, nullable=False)

View File

@ -1,2 +1,2 @@
from .user_repo import UserRepo
from .application_repo import ApplicationRepo
from .user import User
from .application import Application

View File

@ -12,11 +12,11 @@ from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from hub.hub_logger import logger
from hub.persistence import BaseRepo
from hub.persistence.models import Application
from hub.persistence import Repository
from hub.persistence.models import Application as Model
class ApplicationRepo(BaseRepo):
class Application(Repository):
_instance = None
def __init__(self, db_name: str, dotenv_path: str, app_env: str):
@ -27,14 +27,14 @@ class ApplicationRepo(BaseRepo):
Implemented for a singleton pattern
"""
if cls._instance is None:
cls._instance = super(ApplicationRepo, cls).__new__(cls)
cls._instance = super(Application, cls).__new__(cls)
return cls._instance
def insert(self, name: str, description: str, application_uuid: str) -> Union[Application, Dict]:
def insert(self, name: str, description: str, application_uuid: str) -> Union[Model, Dict]:
application = self.get_by_uuid(application_uuid)
if application is None:
try:
application = Application(name=name, description=description, application_uuid=application_uuid)
application = Model(name=name, description=description, application_uuid=application_uuid)
self.session.add(application)
self.session.flush()
self.session.commit()
@ -53,21 +53,24 @@ class ApplicationRepo(BaseRepo):
:return:
"""
try:
self.session.query(Application).filter(Application.application_uuid == application_uuid) \
.update({'name': name, 'description': description, 'updated': datetime.datetime.utcnow()})
self.session.query(Model).filter(
Model.application_uuid == application_uuid
).update({'name': name, 'description': description, 'updated': datetime.datetime.utcnow()})
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while updating application: {err}')
return {'err_msg': 'Error occurred while updating application'}
def get_by_uuid(self, application_uuid: str) -> [Application]:
def get_by_uuid(self, application_uuid: str) -> [Model]:
"""
Fetch Application based on the application uuid
:param application_uuid: the application uuid
:return: [Application] with the provided application_uuid
"""
try:
return self.session.execute(select(Application).where(Application.application_uuid == application_uuid)).first()
return self.session.execute(select(Model).where(
Model.application_uuid == application_uuid)
).first()
except SQLAlchemyError as err:
logger.error(f'Error while fetching application by application_uuid: {err}')
@ -78,7 +81,7 @@ class ApplicationRepo(BaseRepo):
:return: None
"""
try:
self.session.query(Application).filter(Application.application_uuid == application_uuid).delete()
self.session.query(Model).filter(Model.application_uuid == application_uuid).delete()
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while deleting application: {err}')

View File

@ -12,14 +12,14 @@ from typing import Union, Dict
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from hub.city_model_structure.city import City
from hub.city_model_structure.city import City as CityHub
from hub.hub_logger import logger
from hub.persistence import BaseRepo
from hub.persistence.models import City as DBCity
from hub.persistence import Repository
from hub.persistence.models import City as Model
from hub.version import __version__
class CityRepo(BaseRepo):
class City(Repository):
_instance = None
def __init__(self, db_name: str, dotenv_path: str, app_env: str):
@ -30,10 +30,10 @@ class CityRepo(BaseRepo):
Implemented for a singleton pattern
"""
if cls._instance is None:
cls._instance = super(CityRepo, cls).__new__(cls)
cls._instance = super(City, cls).__new__(cls)
return cls._instance
def insert(self, city: City, application_id, user_id: int) -> Union[City, Dict]:
def insert(self, city: CityHub, application_id, user_id: int) -> Union[Model, Dict]:
"""
Insert a city
:param city: The complete city instance
@ -42,9 +42,14 @@ class CityRepo(BaseRepo):
:return: City and Dictionary
"""
try:
release = __version__
db_city = DBCity(pickle.dumps(city), city.name, city.level_of_detail, city.climate_file, application_id, user_id,
release)
db_city = Model(
pickle.dumps(city),
city.name,
city.level_of_detail,
city.climate_file,
application_id,
user_id,
__version__)
self.session.add(db_city)
self.session.flush()
@ -53,18 +58,18 @@ class CityRepo(BaseRepo):
except SQLAlchemyError as err:
logger.error(f'An error occurred while creating city: {err}')
def get_by_id(self, city_id: int) -> DBCity:
def get_by_id(self, city_id: int) -> Model:
"""
Fetch a City based on the id
:param city_id: the city id
:return: a city
"""
try:
return self.session.execute(select(DBCity).where(DBCity.id == city_id)).first()[0]
return self.session.execute(select(Model).where(Model.id == city_id)).first()[0]
except SQLAlchemyError as err:
logger.error(f'Error while fetching city: {err}')
def _get_by_hub_version(self, hub_release: str, city_name: str) -> City:
def _get_by_hub_version_and_name(self, hub_release: str, city_name: str) -> Model:
"""
Fetch a City based on the name and hub project
:param hub_release: the hub release
@ -72,55 +77,50 @@ class CityRepo(BaseRepo):
:return: a city
"""
try:
return self.session.execute(select(DBCity)
.where(DBCity.hub_release == hub_release, DBCity.name == city_name)).first()
return self.session.execute(select(Model)
.where(Model.hub_release == hub_release, Model.name == city_name)
).first()
except SQLAlchemyError as err:
logger.error(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(DBCity).filter(DBCity.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,
'updated': datetime.datetime.utcnow()
})
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while updating city: {err}')
def get_by_name(self, city_name: str) -> [DBCity]:
def get_by_name(self, city_name: str) -> [Model]:
"""
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(DBCity).where(DBCity.name == city_name))
result_set = self.session.execute(select(Model).where(Model.name == city_name))
return [building[0] for building in result_set]
except SQLAlchemyError as err:
logger.error(f'Error while fetching city by name: {err}')
def get_by_user(self, user_id: int) -> [DBCity]:
def get_by_user(self, user_id: int) -> [Model]:
"""
Fetch city based on the user who created it
:param user_id: the id of the user
:return: [ModelCity] with the provided name
"""
try:
result_set = self.session.execute(select(DBCity).where(DBCity.user_id == user_id))
result_set = self.session.execute(select(Model).where(Model.user_id == user_id))
return [building[0] for building in result_set]
except SQLAlchemyError as err:
logger.error(f'Error while fetching city by name: {err}')
def update(self, city_id: int, city: CityHub):
"""
Updates a city name (other updates makes no sense)
:param city_id: the id of the city to be updated
:param city: the city object
:return:
"""
try:
now = datetime.datetime.utcnow()
self.session.query(Model).filter(Model.id == city_id).update({'name': city.name,'updated': now})
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while updating city: {err}')
def delete_city(self, city_id: int):
"""
Deletes a City with the id
@ -128,7 +128,7 @@ class CityRepo(BaseRepo):
:return: a city
"""
try:
self.session.query(DBCity).filter(DBCity.id == city_id).delete()
self.session.query(Model).filter(Model.id == city_id).delete()
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while fetching city: {err}')

View File

@ -1,108 +0,0 @@
"""
Heat pump simulation 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 hub.persistence import BaseRepo, CityRepo
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import select
from hub.persistence.models import HeatPumpSimulation
from typing import Union, Dict
from hub.hub_logger import logger
class HeatPumpSimulationRepo(BaseRepo):
_instance = None
def __init__(self, db_name, dotenv_path, app_env):
super().__init__(db_name, dotenv_path, app_env)
self._city_repo = CityRepo(db_name, dotenv_path, app_env)
def __new__(cls, db_name, dotenv_path, app_env):
"""
Implemented for a singleton pattern
"""
if cls._instance is None:
cls._instance = super(HeatPumpSimulationRepo, cls).__new__(cls)
return cls._instance
def insert(self, hp_sim_data: Dict, city_id: int) -> Union[HeatPumpSimulation, Dict]:
"""
Inserts the results of heat pump simulation
:param hp_sim_data: dictionary with heatpump the simulation inputs and output
:param city_id: the city that was used in running the simulation
:return: HeatPumpSimulation
"""
city = self._city_repo.get_by_id(city_id)
if city is None:
return {'message': 'city not found in database'}
try:
hp_simulation = HeatPumpSimulation(city_id, hp_sim_data["HourlyElectricityDemand"],
hp_sim_data["DailyElectricityDemand"], hp_sim_data["MonthlyElectricityDemand"],
hp_sim_data["DailyFossilFuelConsumption"],
hp_sim_data["MonthlyFossilFuelConsumption"])
hp_simulation.city_id = city_id
hp_simulation.end_year = hp_sim_data["EndYear"]
hp_simulation.start_year = hp_sim_data["StartYear"]
hp_simulation.max_demand_storage_hour = hp_sim_data["HoursOfStorageAtMaxDemand"]
hp_simulation.max_hp_energy_input = hp_sim_data["MaximumHPEnergyInput"]
hp_simulation.building_supply_temp = hp_sim_data["BuildingSuppTemp"]
hp_simulation.temp_difference = hp_sim_data["TemperatureDifference"]
hp_simulation.fuel_lhv = hp_sim_data["FuelLHV"]
hp_simulation.fuel_price = hp_sim_data["FuelPrice"]
hp_simulation.fuel_efficiency = hp_sim_data["FuelEF"]
hp_simulation.fuel_density = hp_sim_data["FuelDensity"]
hp_simulation.hp_supply_temp = hp_sim_data["HPSupTemp"]
hp_simulation.simulation_type = hp_sim_data["SimulationType"]
hp_simulation.heat_pump_model = hp_sim_data["HeatPumpModel"]
hp_simulation.heat_pump_type = hp_sim_data["HeatPumpType"]
# Persist heat pump simulation data
self.session.add(hp_simulation)
self.session.flush()
self.session.commit()
return hp_simulation
except SQLAlchemyError as err:
logger.error(f'Error while saving heat pump simulation data: {err}')
except KeyError as err:
logger.error(f'A required field is missing in your heat pump simulation dictionary: {err}')
def get_by_id(self, hp_simulation_id: int) -> HeatPumpSimulation:
"""
Fetches heat pump simulation data
:param hp_simulation_id: the city id
:return: a HeatPumpSimulation
"""
try:
return self.session.execute(select(HeatPumpSimulation).where(HeatPumpSimulation.id == hp_simulation_id)).first()[
0]
except SQLAlchemyError as err:
logger.error(f'Error while fetching city: {err}')
def get_by_city(self, city_id: int) -> [HeatPumpSimulation]:
"""
Fetch heat pump simulation results by city
:param city_id: the name of the building
:return: [HeatPumpSimulation] with the provided name
"""
try:
result_set = self.session.execute(select(HeatPumpSimulation).where(HeatPumpSimulation.city_id == city_id))
return [sim_data[0] for sim_data in result_set]
except SQLAlchemyError as err:
logger.error(f'Error while fetching city by name: {err}')
def delete_hp_simulation(self, hp_simulation_id: int):
"""
Deletes a heat pump simulation results
:param hp_simulation_id: the heat pump simulation results id
:return:
"""
try:
self.session.query(HeatPumpSimulation).filter(HeatPumpSimulation.id == hp_simulation_id).delete()
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while fetching city: {err}')

View File

@ -5,10 +5,10 @@ Copyright © 2022 Concordia CERC group
Project Coder Peter Yefi peteryefi@gmail.com
"""
from hub.persistence import BaseRepo
from hub.persistence import Repository
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import select
from hub.persistence.models import User
from hub.persistence.models import User as Model
from hub.persistence.models import UserRoles
from hub.helpers.auth import Auth
from typing import Union, Dict
@ -16,7 +16,7 @@ from hub.hub_logger import logger
import datetime
class UserRepo(BaseRepo):
class User(Repository):
_instance = None
def __init__(self, db_name: str, dotenv_path: str, app_env: str):
@ -27,14 +27,14 @@ class UserRepo(BaseRepo):
Implemented for a singleton pattern
"""
if cls._instance is None:
cls._instance = super(UserRepo, cls).__new__(cls)
cls._instance = super(User, cls).__new__(cls)
return cls._instance
def insert(self, name: str, password: str, role: UserRoles, application_id: int) -> Union[User, Dict]:
def insert(self, name: str, password: str, role: UserRoles, application_id: int) -> Union[Model, Dict]:
user = self.get_by_name_and_application(name, application_id)
if user is None:
try:
user = User(name=name, password=Auth.hash_password(password), role=role, application_id=application_id)
user = Model(name=name, password=Auth.hash_password(password), role=role, application_id=application_id)
self.session.add(user)
self.session.flush()
self.session.commit()
@ -42,38 +42,42 @@ class UserRepo(BaseRepo):
except SQLAlchemyError as err:
logger.error(f'An error occurred while creating user: {err}')
else:
return {'message': f'user with {email} email already exists'}
return {'message': f'user {name} already exists for that application'}
def update(self, user_id: int, name: str, email: str, password: str, role: UserRoles) -> Union[Dict, None]:
def update(self, user_id: int, name: str, password: str, role: UserRoles) -> Union[Dict, None]:
"""
Updates a user
:param user_id: the id of the user to be updated
: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:
"""
try:
if Auth.validate_password(password):
self.session.query(User).filter(User.id == user_id) \
.update({'name': name, 'email': email, 'password': Auth.hash_password(password), 'role': role,
'updated': datetime.datetime.utcnow()})
self.session.query(Model).filter(Model.id == user_id).update({
'name': name,
'password': Auth.hash_password(password),
'role': role,
'updated': datetime.datetime.utcnow()
})
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while updating user: {err}')
return {'err_msg': 'Error occurred while updated user'}
def get_by_email(self, email: str) -> [User]:
def get_by_name_and_application(self, name: str, application_id: int) -> [Model]:
"""
Fetch user based on the email address
:param email: the email of the user
:return: [User] with the provided email
:param name: User name
:param application_id: User application name
:return: [User] matching the search criteria
"""
try:
return self.session.execute(select(User).where(User.email == email)).first()
return self.session.execute(
select(Model).where(Model.name == name and Model.application_id == application_id)
).first()
except SQLAlchemyError as err:
logger.error(f'Error while fetching user by email: {err}')
logger.error(f'Error while fetching user by name and application: {err}')
def delete_user(self, user_id: int):
"""
@ -82,25 +86,27 @@ class UserRepo(BaseRepo):
:return: None
"""
try:
self.session.query(User).filter(User.id == user_id).delete()
self.session.query(Model).filter(Model.id == user_id).delete()
self.session.commit()
except SQLAlchemyError as err:
logger.error(f'Error while fetching user: {err}')
def get_user_by_email_and_password(self, email: str, password: str) -> [User]:
def get_user_by_name_application_and_password(self, name: str, password: str, application_id: int) -> [Model]:
"""
Fetch user based on the email and password
:param email: the email of the user
:param password: the password of the user
:param name: User name
:param password: User password
:param application_id: User password
:return: [User] with the provided email and password
"""
try:
user = self.session.execute(select(User).where(User.email == email)).first()
user = self.session.execute(
select(Model).where(Model.name == name and Model.application_id == application_id)
).first()
if user:
if Auth.check_password(password, user[0].password):
return user
else:
return {'message': 'Wrong email/password combination'}
return {'message': 'user not found'}
return {'message': 'invalid login information'}
except SQLAlchemyError as err:
logger.error(f'Error while fetching user by email: {err}')

View File

@ -5,17 +5,17 @@ Copyright © 2022 Concordia CERC group
Project Coder Peter Yefi peteryefi@gmail.com
"""
from hub.persistence.db_config import BaseConfiguration
from hub.persistence.configuration import Configuration
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
class BaseRepo:
class Repository:
def __init__(self, db_name, dotenv_path: str, app_env='TEST'):
try:
self.config = BaseConfiguration(db_name, dotenv_path, app_env)
self.engine = create_engine(self.config.conn_string())
self.configuration = Configuration(db_name, dotenv_path, app_env)
self.engine = create_engine(self.configuration.conn_string())
self.session = Session(self.engine)
except ValueError as err:
print(f'Missing value for credentials: {err}')

View File

@ -13,7 +13,10 @@ setup(
description="CERC Hub consist in a set of classes (Central data model), importers and exporters to help researchers "
"to create better and sustainable cities",
long_description="CERC Hub consist in a set of classes (Central data model), importers and exporters to help "
"researchers to create better and sustainable cities",
"researchers to create better and sustainable cities.\n\nDevelop at Concordia university in canada "
"as part of the research group from the next generation cities institute our aim among others it's "
"to provide a comprehensive set of tools to help researchers and urban developers to make decisions "
"to improve the livability and efficiency of our cities",
classifiers=[
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Programming Language :: Python",