From d9828f3d5711b9de39a6167978d58ac10a6a27ac Mon Sep 17 00:00:00 2001 From: Koa Date: Mon, 23 Jan 2023 10:00:42 -0500 Subject: [PATCH 1/7] Starting the implementation of save_city --- hub_api/city_commands.py | 83 ++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/hub_api/city_commands.py b/hub_api/city_commands.py index e190ec3..ff831d9 100644 --- a/hub_api/city_commands.py +++ b/hub_api/city_commands.py @@ -7,13 +7,16 @@ Copyright © 2023 Project Author Koa Wells kekoa.wells@concordia.ca import json from flask import Response, request from flask_restful import Resource -from imports.user_factory import UserFactory -from exports.user_factory import UserFactory as ExUserFactory -import os -from hub_logger import logger from hub_api.helpers.session_helper import refresh_session -from hub_api.helpers.auth import generate_auth_token, role_required +from hub_api.helpers.auth import role_required +import os +from imports.db_factory import DBFactory +from imports.user_factory import UserFactory +from imports.geometry_factory import GeometryFactory +from hub_logger import logger from persistence.models import UserRoles +from pathlib import Path +import os #Admin user commands """ @@ -21,7 +24,7 @@ SaveCity class """ class SaveCity(Resource): def __init__(self): - print() + pass @role_required([UserRoles.Admin.value]) def put(self): @@ -36,20 +39,62 @@ UpdateCity class """ class UpdateCity(Resource): def __init__(self): - print() + self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) @role_required([UserRoles.Admin.value]) - def put(self): + def put(self, city_id, city): session = refresh_session(request) if session is None: return Response(json.dumps({'error': 'invalid session'}), status=401) headers = session.headers + + allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} + try: + city_file = request.files['city_file'] + ext = city_file.filename.rsplit('.', 1)[1].lower() + + if ext in allowed_ext: + city_file_type = ext + if ext == 'gml': + city_file_type = 'citygml' + elif ext == '3dm': + city_file_type = 'rhino' + + file_path = ( + Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() + city_file.save(file_path) + city = GeometryFactory(city_file_type, file_path).city + db_factory = DBFactory(city=city, db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + saved_city = db_factory.persist_city(1) + + if os.path.exists(file_path): + os.remove(file_path) + if type(saved_city) is not dict: + return Response(response=json.dumps({ + 'id': saved_city.id, 'name': saved_city.name, 'srs_name': saved_city.srs_name, + 'time_zone': saved_city.time_zone, 'version': saved_city.city_version, + 'country': saved_city.country_code, + 'lat': saved_city.latitude, 'lon': saved_city.longitude, 'lower_corner': saved_city.lower_corner, + 'upper_corner': saved_city.upper_corner, 'created': saved_city.created, + 'updated': saved_city.updated, + 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, + 'role': saved_city.user.role.value} + }, default=str), status=201) + return Response(response=json.dumps(saved_city), status=200) + else: + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400) """ DeleteCity class """ class DeleteCity(Resource): - def __init__(self): - print() + def __init__(self, city_id): + pass @role_required([UserRoles.Admin.value]) def delete(self): @@ -64,7 +109,7 @@ ListCities class """ class ListCities(Resource): def __init__(self): - print() + pass @role_required([UserRoles.Admin.value]) def put(self): @@ -77,22 +122,10 @@ SearchCity class """ class SearchCity(Resource): def __init__(self): - print() + pass - def get(self): + def get(self, city_id): session = refresh_session(request) if session is None: return Response(json.dumps({'error': 'invalid session'}), status=401) headers = session.headers -""" -UpdateCity class -""" -class UpdateCity(Resource): - def __init__(self): - print() - - def get(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers \ No newline at end of file From cea801a67fe2c4430d5e94405499ccae489f93a0 Mon Sep 17 00:00:00 2001 From: KoaCWells Date: Tue, 24 Jan 2023 15:21:53 -0500 Subject: [PATCH 2/7] Implement AddCity and DeleteCity API calls + comments --- hub_api/city_commands.py | 209 +++++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 95 deletions(-) diff --git a/hub_api/city_commands.py b/hub_api/city_commands.py index ff831d9..58b15c6 100644 --- a/hub_api/city_commands.py +++ b/hub_api/city_commands.py @@ -18,114 +18,133 @@ from persistence.models import UserRoles from pathlib import Path import os -#Admin user commands -""" -SaveCity class -""" -class SaveCity(Resource): - def __init__(self): - pass +# Admin user commands - @role_required([UserRoles.Admin.value]) - def put(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers +# class SaveCity(Resource): +# """ +# SaveCity class performs an admin API call to save an instantiated city into the database +# """ +# +# def __init__(self): +# self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', +# dotenv_path="{}/.env".format(os.path.expanduser('~'))) +# +# # todo: implement feature to persist instantiated city into database +# @role_required([UserRoles.Admin.value]) +# def put(self): +# session = refresh_session(request) +# if session is None: +# return Response(json.dumps({'error': 'invalid session'}), status=401) +# headers = session.headers - -""" -UpdateCity class -""" class UpdateCity(Resource): - def __init__(self): - self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + """ + UpdateCity class performs an admin API call to update a city that already exists inside the database + """ - @role_required([UserRoles.Admin.value]) - def put(self, city_id, city): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers + def __init__(self): + self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) - allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} - try: - city_file = request.files['city_file'] - ext = city_file.filename.rsplit('.', 1)[1].lower() + @role_required([UserRoles.Admin.value]) + def put(self, city_id): + session = refresh_session(request) + if session is None: + return Response(json.dumps({'error': 'invalid session'}), status=401) + headers = session.headers - if ext in allowed_ext: - city_file_type = ext - if ext == 'gml': - city_file_type = 'citygml' - elif ext == '3dm': - city_file_type = 'rhino' + allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} + try: + city_file = request.files['city_file'] + ext = city_file.filename.rsplit('.', 1)[1].lower() - file_path = ( - Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() - city_file.save(file_path) - city = GeometryFactory(city_file_type, file_path).city - db_factory = DBFactory(city=city, db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) - saved_city = db_factory.persist_city(1) + if ext in allowed_ext: + city_file_type = ext + if ext == 'gml': + city_file_type = 'citygml' + elif ext == '3dm': + city_file_type = 'rhino' - if os.path.exists(file_path): - os.remove(file_path) - if type(saved_city) is not dict: - return Response(response=json.dumps({ - 'id': saved_city.id, 'name': saved_city.name, 'srs_name': saved_city.srs_name, - 'time_zone': saved_city.time_zone, 'version': saved_city.city_version, - 'country': saved_city.country_code, - 'lat': saved_city.latitude, 'lon': saved_city.longitude, 'lower_corner': saved_city.lower_corner, - 'upper_corner': saved_city.upper_corner, 'created': saved_city.created, - 'updated': saved_city.updated, - 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, - 'role': saved_city.user.role.value} - }, default=str), status=201) - return Response(response=json.dumps(saved_city), status=200) - else: - return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + file_path = ( + Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() + city_file.save(file_path) + city = GeometryFactory(city_file_type, file_path).city + + saved_city = self.db_factory.update_city(city.city_id, city) + + if os.path.exists(file_path): + os.remove(file_path) + if type(saved_city) is not dict: + return Response(response=json.dumps({ + 'id': saved_city.id, 'name': saved_city.name, 'srs_name': saved_city.srs_name, + 'time_zone': saved_city.time_zone, 'version': saved_city.city_version, + 'country': saved_city.country_code, + 'lat': saved_city.latitude, 'lon': saved_city.longitude, + 'lower_corner': saved_city.lower_corner, + 'upper_corner': saved_city.upper_corner, 'created': saved_city.created, + 'updated': saved_city.updated, + 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, + 'role': saved_city.user.role.value} + }, default=str), status=201) + return Response(response=json.dumps(saved_city), status=200) + else: + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400) - except Exception as err: - logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400) -""" -DeleteCity class -""" class DeleteCity(Resource): - def __init__(self, city_id): - pass + """ + DeleteCity class performs an admin API call to delete an existing city stored in the database + """ - @role_required([UserRoles.Admin.value]) - def delete(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers + def __init__(self): + self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + @role_required([UserRoles.Admin.value]) + def delete(self): + session = refresh_session(request) + if session is None: + return Response(json.dumps({'error': 'invalid session'}), status=401) + + try: + payload = request.get_json() + return Response(json.dumps(response=DBFactory.delete_city(city_id=payload["city_id"]), status=201)) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while deleting city'}), status=400) + + +# Standard user commands -#Standard user commands -""" -ListCities class -""" class ListCities(Resource): - def __init__(self): - pass + """ + ListCities class performs a standard API call to list all existing cities stored in the database + """ + + def __init__(self): + self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + def get(self): + session = refresh_session(request) + if session is None: + return Response(json.dumps({'error': 'invalid session'}), status=401) + headers = session.headers - @role_required([UserRoles.Admin.value]) - def put(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers -""" -SearchCity class -""" class SearchCity(Resource): - def __init__(self): - pass + """ + SearchCity class performs a standard API call to select an existing city stored in the database + """ - def get(self, city_id): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers + def __init__(self): + self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + def get(self, city_id): + session = refresh_session(request) + if session is None: + return Response(json.dumps({'error': 'invalid session'}), status=401) + headers = session.headers From 4b322148ba169dc42f534a20466dc3fce055f5f4 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Mon, 30 Jan 2023 19:45:02 -0500 Subject: [PATCH 3/7] Removed unneeded files --- bootstrap.py | 45 +-- gamification.py | 62 ---- hub_api/city_info.py | 65 +--- hub_api/config.py | 16 +- hub_api/construction.py | 76 ----- hub_api/construction_catalog.py | 196 ------------ hub_api/docs/openapi-specs.yml | 546 +------------------------------- hub_api/energy_demand.py | 396 ----------------------- hub_api/geometry.py | 25 -- hub_api/greenery.py | 84 ----- hub_api/greenery_catalog.py | 178 ----------- hub_api/heat_pump.py | 39 --- hub_api/helpers/auth.py | 4 +- hub_api/lca.py | 128 -------- hub_api/usage.py | 154 --------- hub_api/usage_catalog.py | 156 --------- hub_api/user.py | 8 +- 17 files changed, 35 insertions(+), 2143 deletions(-) delete mode 100644 gamification.py delete mode 100644 hub_api/construction.py delete mode 100644 hub_api/construction_catalog.py delete mode 100644 hub_api/energy_demand.py delete mode 100644 hub_api/geometry.py delete mode 100644 hub_api/greenery.py delete mode 100644 hub_api/greenery_catalog.py delete mode 100644 hub_api/heat_pump.py delete mode 100644 hub_api/lca.py delete mode 100644 hub_api/usage.py delete mode 100644 hub_api/usage_catalog.py diff --git a/bootstrap.py b/bootstrap.py index 0f195b6..5c14aab 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -9,33 +9,16 @@ import flask import yaml from flask_restful import Api from hub_api.city_info import CityInfo, City -from hub_api.geometry import Geometry -from hub_api.greenery_catalog import GreeneryCatalogEntries -from hub_api.greenery_catalog import GreeneryCatalogEntry -from hub_api.greenery_catalog import GreeneryCatalogNames -from hub_api.construction_catalog import ConstructionCatalogEntries -from hub_api.construction_catalog import ConstructionCatalogEntry -from hub_api.construction_catalog import ConstructionCatalogNames -from hub_api.usage_catalog import UsageCatalogEntries -from hub_api.usage_catalog import UsageCatalogEntry -from hub_api.usage_catalog import UsageCatalogNames -from hub_api.heat_pump import HeatPump -from hub_api.lca import MaterialLCACatalog -from hub_api.lca import MaterialLCACalculations -from hub_api.construction import Construction -from hub_api.usage import Usage -from hub_api.energy_demand import EnergyDemand from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive from hub_api.uptime import Uptime -from hub_api.greenery import Greenery from hub_api.user import User, UserLogin from flasgger import LazyJSONEncoder, Swagger +from flask import Response app = flask.Flask('gamification') app.json_encoder = LazyJSONEncoder api = Api(app) - with open("hub_api/docs/openapi-specs.yml", "r") as stream: swagger_config = { "headers": [], @@ -57,23 +40,6 @@ with open("hub_api/docs/openapi-specs.yml", "r") as stream: print(exc) api.add_resource(Uptime, '/v1.4/uptime') -api.add_resource(Geometry, '/v1.4/geometry') -api.add_resource(GreeneryCatalogEntry, '/v1.4/greenery-catalog/entry') -api.add_resource(GreeneryCatalogEntries, '/v1.4/greenery-catalog/entries') -api.add_resource(GreeneryCatalogNames, '/v1.4/greenery-catalog/names') -api.add_resource(ConstructionCatalogEntry, '/v1.4/construction-catalog/entry') -api.add_resource(ConstructionCatalogEntries, '/v1.4/construction-catalog/entries') -api.add_resource(ConstructionCatalogNames, '/v1.4/construction-catalog/names') -api.add_resource(Construction, '/v1.4/construction/') -api.add_resource(UsageCatalogEntry, '/v1.4/usage-catalog/entry') -api.add_resource(UsageCatalogEntries, '/v1.4/usage-catalog/entries') -api.add_resource(UsageCatalogNames, '/v1.4/usage-catalog/names') -api.add_resource(Usage, '/v1.4/usage') -api.add_resource(EnergyDemand, '/v1.4/energy-demand/') -# api.add_resource(LCA, '/v1.4/lca') -api.add_resource(MaterialLCACatalog, '/v1.4/material_lca_catalog/entries/') -api.add_resource(MaterialLCACalculations, '/v1.4/material_lca_catalog/calculations/') -api.add_resource(HeatPump, '/v1.4/heat-pump/') api.add_resource(User, '/v1.4/user') api.add_resource(UserLogin, '/v1.4/user/login') api.add_resource(SessionStart, '/v1.4/session/start') @@ -81,4 +47,11 @@ api.add_resource(SessionEnd, '/v1.4/session/end') api.add_resource(KeepSessionAlive, '/v1.4/session/keep_alive') api.add_resource(CityInfo, '/v1.4/city/') api.add_resource(City, '/v1.4/city') -api.add_resource(Greenery, '/v1.4/greenery') + + +@app.route("/") +def home(): + return Response(headers={'Access-Control-Allow-Origin': '*'}) + + +app.run(port=15789, host="0.0.0.0", debug=False) diff --git a/gamification.py b/gamification.py deleted file mode 100644 index 211fa75..0000000 --- a/gamification.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Main -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2021 Project Author name guillermo.gutierrezmorote@concordia.ca -Project Collaborator name peteryefi@gmail.com -""" - -from catalog_factories.construction_catalog_factory import ConstructionCatalogFactory -from catalog_factories.greenery_catalog_factory import GreeneryCatalogFactory -from catalog_factories.usage_catalog_factory import UsageCatalogFactory -from imports.construction_factory import ConstructionFactory -from imports.geometry_factory import GeometryFactory -from imports.life_cycle_assessment_factory import LifeCycleAssessment -# from imports.schedules_factory import SchedulesFactory -from imports.usage_factory import UsageFactory -from imports.weather_factory import WeatherFactory -from flask import Response -import hub_api.helpers.session_helper as sh -import datetime -from pathlib import Path -from bootstrap import app - -sh.begin_time = datetime.datetime.now() -# initialize catalogs -sh.greenery_catalog = GreeneryCatalogFactory('nrel').catalog -sh.construction_catalog = ConstructionCatalogFactory('nrel').catalog -sh.usage_catalog = UsageCatalogFactory('comnet').catalog - -# Enrich the city -data_path = (Path(__file__).parent / 'data').resolve() -rihno_path = (Path(data_path / 'dompark.3dm')).resolve() -city = GeometryFactory('rhino', rihno_path).city - -for building in city.buildings: - # Rihno files have no information about the function or the year of construction - building.year_of_construction = 1995 - building.function = 'industry' - building.human_readable_name = "Dompark" - -ConstructionFactory('nrel', city).enrich() -UsageFactory('comnet', city).enrich() -# SchedulesFactory('comnet', city).enrich() -LifeCycleAssessment('material', city).enrich() -LifeCycleAssessment('machine', city).enrich() -LifeCycleAssessment('fuel', city).enrich() -LifeCycleAssessment('vehicle', city).enrich() - -montreal_weather_file = (Path(__file__).parent / './data/CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() -city.climate_file = (data_path / f'{city.climate_reference_city}.cli').resolve() -WeatherFactory('epw', city, file_name=montreal_weather_file).enrich() - -# Rihno files have no information about the building location -city.name = 'Montreal' -city.climate_reference_city = 'Montreal' - - -@app.route("/") -def home(): - return Response(headers={'Access-Control-Allow-Origin': '*'}) - - -app.run(port=15789, host="0.0.0.0", debug=False) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 4a356aa..afe2e1a 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -8,9 +8,9 @@ import json from flask import Response, request, g from flask_restful import Resource from hub_api.helpers.auth import role_required -from persistence.models import UserRoles -from hub_logger import logger -from imports.geometry_factory import GeometryFactory +from hub.persistence.models import UserRoles +from hub.hub_logger import logger +from hub.imports.geometry_factory import GeometryFactory from pathlib import Path import os from hub_api.config import Config @@ -23,54 +23,17 @@ class CityInfo(Resource, Config): @role_required([UserRoles.Admin.value]) def get(self, city_id): city = self.get_city(city_id) - - # TODO: this is only for dompark project and need to be removed in future versions. - floor_area = 0 - wall_construction = 'unknown' - floor_construction = 'unknown' - roof_construction = 'unknown' - window_type = 'unknown' - building_dic = {} - for building in city.buildings: - - usages = [] # This is only valid for dompark project as all the building have the same usage - if building.lower_corner[2] == 0: - floor_area += building.floor_area - for internal_zone in building.internal_zones: - for usage_zone in internal_zone.usage_zones: - usages.append({'percentage': usage_zone.percentage, 'usage': usage_zone.usage}) - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.type == 'Ground': - floor_construction = thermal_boundary.construction_name - elif thermal_boundary.parent_surface.type == 'Wall': - wall_construction = thermal_boundary.construction_name - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_opening.construction_name is not None: - window_type = thermal_opening.construction_name - break - else: - roof_construction = thermal_boundary.construction_name - name = building.human_readable_name - year_of_construction = str(building.year_of_construction) - building_dic = { - 'name': str(name), - 'floor_area': str(floor_area), - 'year_of_construction': str(year_of_construction), - 'usages': usages, - 'wall_construction': wall_construction, - 'floor_construction': floor_construction, - 'roof_construction': roof_construction, - 'window_type': window_type, - 'default_archetype': 'industry ASHRAE_2004:4A non_standard_dompark' - } - - buildings = [building_dic] - response = {'city_name': city.name, - 'climate_reference_city': str(city.climate_reference_city), - 'buildings': buildings - } - return Response(json.dumps(response), status=200) + print(city.name) + if city: + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404) class City(Resource, Config): diff --git a/hub_api/config.py b/hub_api/config.py index aa68fd3..a00b6ca 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -3,10 +3,9 @@ Config SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2023 Project Peter Yefi peteryefi@gmail.com """ -from exports.db_factory import DBFactory as CityExportFactory -from imports.db_factory import DBFactory +from hub.exports.db_factory import DBFactory as CityExportFactory +from hub.imports.db_factory import DBFactory import os -import pickle class Config: @@ -18,13 +17,4 @@ class Config: dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): - city_obj = self.export_db_factory.get_city(city_id) - city = pickle.loads(city_obj.city) - for building in city.buildings: - building.heated = True - building.cooled = True - building.attic_heated = 0 - building.basement_heated = 0 - for surface in building.surfaces: - surface.swr = 0.2 - return city + return self.export_db_factory.get_city(city_id) diff --git a/hub_api/construction.py b/hub_api/construction.py deleted file mode 100644 index 7d371b5..0000000 --- a/hub_api/construction.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Construction -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" -import json -import uuid - -from flask import Response, request -from flask_restful import Resource -from hub_api.config import Config -from city_model_structure.building_demand.layer import Layer -from city_model_structure.building_demand.material import Material -from persistence.models import UserRoles -from hub_api.helpers.auth import role_required - - -class Construction(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def put(self, city_id): - city = self.get_city(city_id) - try: - building_names = request.json['building_names'] - constructions = request.json['constructions'] - for construction in constructions: - construction_type = construction['type'] - layers = construction['layers'] - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - thermal_zone.additional_thermal_bridge_u_value = request.json['extra_loses_due_to_thermal_bridges'] - thermal_zone.indirect_heated_area_ratio = request.json['indirect_heated_ratio'] - thermal_zone.infiltration_rate_system_on = request.json['infiltration_rate_for_ventilation_system_on'] - thermal_zone.infiltration_rate_system_off = request.json['infiltration_rate_for_ventilation_system_off'] - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.type == construction_type: - _layers = [] - for layer in layers: - _layer = Layer() - thermal_resistance = layer['material']['thermal_resistance'] - conductivity = layer['material']['conductivity'] - density = layer['material']['density'] - specific_heat = layer['material']['specific_heat'] - _layer.thickness = layer['thickness'] - _material = Material() - _material.id = layer['material']['id'] - _material.name = layer['material']['name'] - _material.solar_absorptance = layer['material']['solar_absorptance'] - _material.thermal_absorptance = layer['material']['thermal_absorptance'] - _material.visible_absorptance = layer['material']['visible_absorptance'] - _material.no_mass = layer['material']['no_mass'] - _material.thermal_resistance = (thermal_resistance if thermal_resistance != '' else None) - _material.conductivity = (conductivity if conductivity != '' else None) - _material.density = (density if density != '' else None) - _material.specific_heat = (specific_heat if specific_heat != '' else None) - _layer.material = _material - _layers.append(_layer) - thermal_boundary.layers = _layers - if 'window' in construction.keys(): - for thermal_opening in thermal_boundary.thermal_openings: - thermal_opening.frame_ratio = construction['window']['frame_ratio'] - thermal_opening.g_value = construction['window']['g_value'] - thermal_opening.overall_u_value = construction['window']['overall_u_value'] - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - - return Response(json.dumps(response), status=200) diff --git a/hub_api/construction_catalog.py b/hub_api/construction_catalog.py deleted file mode 100644 index 8607026..0000000 --- a/hub_api/construction_catalog.py +++ /dev/null @@ -1,196 +0,0 @@ -""" -Construction catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json - -from flask import request, Response -from flask_restful import Resource - -from catalog_factories.data_models.construction.archetype import Archetype -from catalog_factories.data_models.construction.construction import Construction -from catalog_factories.data_models.construction.material import Material -from catalog_factories.data_models.construction.window import Window -from hub_api.helpers.session_helper import refresh_session -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles - - -class ToJson: - @staticmethod - def archetype_to_json(archetype): - constructions = [] - for construction in archetype.constructions: - constructions.append(ToJson.construction_to_json(construction)) - dictionary = { - 'name': archetype.name, - 'function': archetype.function, - 'construction_period': archetype.construction_period, - 'average_storey_height': archetype.average_storey_height, - 'thermal_capacity': archetype.thermal_capacity, - 'extra_loses_due_to_thermal_bridges': archetype.extra_loses_due_to_thermal_bridges, - 'indirect_heated_ratio': archetype.indirect_heated_ratio, - 'infiltration_rate_for_ventilation_system_off': archetype.infiltration_rate_for_ventilation_system_off, - 'infiltration_rate_for_ventilation_system_on': archetype.infiltration_rate_for_ventilation_system_on, - 'constructions': constructions - } - return dictionary - - @staticmethod - def construction_to_json(construction): - layers = [] - for layer in construction.layers: - layers.append(ToJson.layer_to_json(layer)) - dictionary = {'name': construction.name, - 'type': construction.type, - 'layers': layers - } - if construction.window is not None: - dictionary['window_ratio'] = construction.window_ratio - dictionary['window'] = ToJson.window_to_json(construction.window) - return dictionary - - @staticmethod - def window_to_json(window): - if window is None: - return {} - dictionary = { - 'name': window.name, - 'frame_ratio': window.frame_ratio, - 'g_value': window.g_value, - 'overall_u_value': str(window.overall_u_value) - } - return dictionary - - @staticmethod - def layer_to_json(layer): - dictionary = {'name': layer.name, - 'thickness': layer.thickness, - 'material': ToJson.material_to_json(layer.material), - } - return dictionary - - @staticmethod - def material_to_json(material): - dictionary = {'id': material.id, - 'name': material.name, - 'solar_absorptance': material.solar_absorptance, - 'thermal_absorptance': material.thermal_absorptance, - 'visible_absorptance': material.visible_absorptance, - 'no_mass': str(material.no_mass), - 'thermal_resistance': '', - 'conductivity': '', - 'density': '', - 'specific_heat': '' - } - if material.no_mass: - dictionary['thermal_resistance'] = material.thermal_resistance - else: - dictionary['conductivity'] = material.conductivity - dictionary['density'] = material.density - dictionary['specific_heat'] = material.specific_heat - return dictionary - - -class ConstructionCatalogEntry(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.construction_catalog - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {} - if isinstance(entry, Archetype): - output['archetypes'] = ToJson.archetype_to_json(entry) - if isinstance(entry, Construction): - output['constructions'] = ToJson.construction_to_json(entry) - if isinstance(entry, Material): - output['materials'] = ToJson.material_to_json(entry) - if isinstance(entry, Window): - output['windows'] = ToJson.window_to_json(entry) - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class ConstructionCatalogEntries(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.construction_catalog - category = None - if request.data != b'': - category = request.json['category'] - output = {} - if category is None: - output = {'archetypes': [], 'constructions':[], 'materials':[], 'windows':[]} - content = catalog.entries() - for archetype in content.archetypes: - output['archetypes'].append(ToJson.archetype_to_json(archetype)) - for construction in content.constructions: - output['constructions'].append(ToJson.construction_to_json(construction)) - for material in content.materials: - output['materials'].append(ToJson.material_to_json(material)) - for window in content.windows: - output['windows'].append(ToJson.window_to_json(window)) - else: - try: - output[category] = [] - content = catalog.entries(category) - for entry in content: - if isinstance(entry, Archetype): - output[category].append(ToJson.archetype_to_json(entry)) - if isinstance(entry, Construction): - output[category].append(ToJson.construction_to_json(entry)) - if isinstance(entry, Material): - output[category].append(ToJson.material_to_json(entry)) - if isinstance(entry, Window): - output[category].append(ToJson.window_to_json(entry)) - except ValueError: - output = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(output), headers=headers, status=400) - return Response(json.dumps(output), headers=headers) - - -class ConstructionCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.construction_catalog - category = None - if request.data != b'': - category = request.json['category'] - if category is None: - return Response(json.dumps(catalog.names()), headers=headers) - else: - try: - return Response(json.dumps(catalog.names(category)), headers=headers) - except ValueError: - response = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index 0eed27a..e15dadb 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -3,7 +3,7 @@ info: description: NextGen Cities Institute Gamification API termsOfService: http://swagger.io/terms/ contact: - email: peteryefi@gmail.com + email: nextgen-cities@gmail.com version: 1.4 externalDocs: description: Find out more about Swagger @@ -78,72 +78,6 @@ paths: $ref: '#/components/schemas/ApiResponse' security: - BearerAuth: [] - /v1.4/heat-pump/{city_id}: - post: - tags: - - heatpump - summary: Create a heat pump simulation - operationId: createHeatpump - description: heatpump simulation with existing catalog data - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - requestBody: - content: - application/json: - schema: - type: object - $ref: '#/components/schemas/HeatPump' - required: true - responses: - '201': - description: Heatpump simulation created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/HeatPumpRes' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '404': - description: Not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] /v1.4/city/{city_id}: get: tags: @@ -166,7 +100,7 @@ paths: description: Numeric ID of the city to get responses: '200': - description: City created successfully + description: City retrieved successfully content: application/json: schema: @@ -342,233 +276,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponse' - /v1.4/energy-demand/{city_id}: - get: - tags: - - energy-demand - summary: Get energy demand - description: Retrieve energy demand data - operationId: getEnergyDemand - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - responses: - '200': - description: Successfully retrieved energy demand data - content: - application/json: - schema: - $ref: '#/components/schemas/EnergyDemand' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction/{city_id}: - put: - tags: - - construction - summary: updates a city with building construction - description: updates a city with building construction - operationId: updateConstruction - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/Construction' - application/xml: - schema: - $ref: '#/components/schemas/Construction' - responses: - '201': - description: city updated with building construction successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Construction' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction-catalog/entries: - post: - tags: - - construction-catalog - summary: creates building construction catalog entries - description: create the construction catalog entries - operationId: catalogEntries - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - application/xml: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - responses: - '201': - description: Construction catalog entries created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction-catalog/entry: - post: - tags: - - construction-catalog - summary: creates building construction catalog - description: create the construction catalog of a building - operationId: catalogEntry - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - application/xml: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - responses: - '201': - description: Construction catalog created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] components: schemas: City: @@ -619,211 +326,6 @@ components: updated: type: string example: 2023-01-15 18:40:54.64877 - HeatPump: - type: object - properties: - StartYear: - type: integer - format: int64 - example: 10 - EndYear: - type: integer - format: int64 - example: 10 - MaximumHPEnergyInput: - type: float - example: 9.8382 - HoursOfStorageAtMaxDemand: - type: integer - format: int64 - example: 9 - BuildingSuppTemp: - type: integer - format: int64 - example: 40 - TemperatureDifference: - type: float - example: 9.8382 - FuelLHV: - type: float - example: 9.8382 - FuelPrice: - type: float - example: 9.8382 - FuelEF: - type: integer - format: int64 - example: 40 - FuelDensity: - type: float - example: 9.8382 - HPSupTemp: - type: float - example: 9.8382 - HeatPumpType: - type: string - example: Water to Water HP - enum: - - Water to Water HP - - Air Source HP - HeatPumpModel: - type: string - example: Water to Water HP - enum: - - ClimateMaster 156 kW - - ClimateMaster 256 kW - - ClimateMaster 335 kW - - 012 - - 015 - - 018 - - 023 - - 030 - - 033 - - 037 - - 044 - - 047 - - 057 - - 070 - - 087 - - 097 - - 102 - - 120 - - 130 - - 140 - SimType: - type: int - example: 1 - format: int64 - EnergyDemand: - type: array - items: - type: float - example: [ 610.610, 754.746, 288.338 ] - required: - - SimType - - EnergyDemand - - HeatPumpModel - - HeatPumpType - - FuelEF - - FuelDensity - - FuelPrice - - FuelLHV - - BuildingSuppTemp - - TemperatureDifference - - HoursOfStorageAtMaxDemand - - MaximumHPEnergyInput - - StartYear - - EndYear - ConstructionCatalogEntry: - type: object - properties: - name: - type: string - example: 189.1-2009 Nonres 4B Ext Wall Mass - required: - - name - ConstructionCatalogEntries: - type: object - properties: - category: - type: string - example: archetypes - required: - - category - Construction: - type: object - properties: - building_names: - type: array - items: - type: string - example: ["Dompark"] - extra_loses_due_to_thermal_bridges: - type: float - example: 0.15 - indirect_heated_ratio: - type: float - example: 0.15 - infiltration_rate_for_ventilation_system_off: - type: float - example: 0.5 - infiltration_rate_for_ventilation_system_on: - type: float - example: 0.0 - constructions: - type: array - items: - type: object - properties: - name: - type: string - example: 189.1-2009 Res 4B Ext Wall Steel-Framed - type: - type: string - example: Wall - layers: - type: array - items: - type: object - $ref: '#components/schemas/Layers' - required: - - building_names - - extra_loses_due_to_thermal_bridges - - indirect_heated_ratio - - infiltration_rate_for_ventilation_system_off - - infiltration_rate_for_ventilation_system_on - - constructions - Layers: - type: object - properties: - name: - type: string - example: Layer 1 - thickness: - type: float - example: 0.0 - material: - type: object - $ref: '#/components/schemas/Material' - Material: - type: object - properties: - id: - type: integer - format: int64 - example: 12 - name: - type: string - example: MAT-SHEAT - solar_absorptance: - type: float - example: 0.7 - thermal_absorptance: - type: float - example: 0.9 - visible_absorptance: - type: float - example: 0.7 - no_mass: - type: string - example: "True" - thermal_resistance: - type: float - example: 0.36256 - conductivity: - type: string - density: - type: string - specific_heat: - type: string - required: - - id - - name - - solar_absorptance - - thermal_absorptance - - visible_absorptance - - no_mass - - thermal_resistance User: type: object properties: @@ -851,48 +353,6 @@ components: - email - password - role - HeatPumpRes: - type: object - properties: - hourly_electricity_demand: - type: array - items: - type: object - daily_electricity_demand: - type: array - items: - type: object - monthly_electricity_demand: - type: array - items: - type: object - daily_fossil_consumption: - type: array - items: - type: object - monthly_fossil_consumption: - type: array - items: - type: object - EnergyDemand: - type: object - properties: - heating_demand: - type: array - items: - type: object - cooling_demand: - type: array - items: - type: object - lighting_demand: - type: array - items: - type: object - appliances_demand: - type: array - items: - type: object Login: type: object properties: @@ -924,7 +384,7 @@ components: type: string requestBodies: User: - description: Pet object that needs to be added to the store + description: User object that is to be created content: application/json: schema: diff --git a/hub_api/energy_demand.py b/hub_api/energy_demand.py deleted file mode 100644 index 5685c3a..0000000 --- a/hub_api/energy_demand.py +++ /dev/null @@ -1,396 +0,0 @@ -import json -from flask import request, Response -from flask_restful import Resource -from pathlib import Path -from geomeppy import IDF -import os -import glob -import hub_api.helpers.session_helper as sh -import helpers.constants as cte -import csv -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles -from hub_api.config import Config - - -class EnergyDemand(Resource, Config): - _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' - _IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM' - _SURFACE = 'BUILDINGSURFACE:DETAILED' - _WINDOW_SURFACE = 'FENESTRATIONSURFACE:DETAILED' - _CONSTRUCTION = 'CONSTRUCTION' - _MATERIAL = 'MATERIAL' - _MATERIAL_NOMASS = 'MATERIAL:NOMASS' - _ROUGHNESS = 'MediumRough' - _HOURLY_SCHEDULE = 'SCHEDULE:DAY:HOURLY' - _COMPACT_SCHEDULE = 'SCHEDULE:COMPACT' - _FILE_SCHEDULE = 'SCHEDULE:FILE' - _ZONE = 'ZONE' - _LIGHTS = 'LIGHTS' - _PEOPLE = 'PEOPLE' - _APPLIANCES = 'OTHEREQUIPMENT' - _HEATING_COOLING = 'THERMOSTATSETPOINT:DUALSETPOINT' - _INFILTRATION = 'ZONEINFILTRATION:DESIGNFLOWRATE' - _BUILDING_SURFACE = 'BuildingSurfaceDetailed' - _SCHEDULE_LIMIT = 'SCHEDULETYPELIMITS' - _ON_OFF = 'On/Off' - _FRACTION = 'Fraction' - _ANY_NUMBER = 'Any Number' - _CONTINUOUS = 'Continuous' - _DISCRETE = 'Discrete' - _BUILDING = 'BUILDING' - _SIZING_PERIODS = 'SIZINGPERIOD:DESIGNDAY' - _LOCATION = 'SITE:LOCATION' - _WINDOW_MATERIAL_SIMPLE = 'WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM' - _WINDOW = 'WINDOW' - _MATERIAL_ROOFVEGETATION = 'MATERIAL:ROOFVEGETATION' - _SIMPLE = 'Simple' - - idf_surfaces = { - # todo: make an enum for all the surface types - cte.WALL: 'wall', - cte.GROUND: 'floor', - cte.ROOF: 'roof' - } - idf_usage = { - # todo: make an enum for all the usage types - cte.RESIDENTIAL: 'residential_building' - } - idf_type_limits = { - cte.ON_OFF: 'on/off', - cte.FRACTION: 'Fraction', - cte.ANY_NUMBER: 'Any Number', - cte.CONTINUOUS: 'Continuous', - cte.DISCRETE: 'Discrete', - cte.TEMPERATURE: 'Any Number' - } - idf_day_types = { - cte.MONDAY: 'Monday', - cte.TUESDAY: 'Tuesday', - cte.WEDNESDAY: 'Wednesday', - cte.THURSDAY: 'Thursday', - cte.FRIDAY: 'Friday', - cte.SATURDAY: 'Saturday', - cte.SUNDAY: 'Sunday', - cte.HOLIDAY: 'Holidays', - cte.WINTER_DESIGN_DAY: 'WinterDesignDay', - cte.SUMMER_DESIGN_DAY: 'SummerDesignDay' - } - idf_schedule_types = { - 'compact': 'Compact', - cte.DAY: 'Day', - cte.WEEK: 'Week', - cte.YEAR: 'Year', - 'file': 'File' - } - idf_schedule_data_type = { - 'compact': 'Compact', - 'hourly': 'Hourly', - 'daily': 'Daily', - 'interval': 'Interval', - 'list': 'List', - } - - def __init__(self): - # this class is mostly hardcoded, as is intended to be used only for Dompark project, - # other projects should use the normal idf workflow instead. - super().__init__() - self._output_path = Path(Path(__file__).parent.parent / 'tmp').resolve() - self._data_path = Path(Path(__file__).parent.parent / 'data').resolve() - self._city = None - self._greenery_percentage = 0 - - def _set_layers(self, _idf, name, layers, vegetation=None): - if vegetation is not None: - _kwargs = {'Name': name, 'Outside_Layer': vegetation.name} - for i in range(0, len(layers)): - _kwargs[f'Layer_{i + 2}'] = layers[i].material.name - else: - _kwargs = {'Name': name, 'Outside_Layer': layers[0].material.name} - for i in range(1, len(layers)): - _kwargs[f'Layer_{i + 1}'] = layers[i].material.name - _idf.newidfobject(self._CONSTRUCTION, **_kwargs) - - def _update_constructions(self, _idf, ground, roof, wall, vegetation): - for construction in _idf.idfobjects[self._CONSTRUCTION]: - if construction.Name == 'Project ground floor': - # floor - self._set_layers(_idf, 'user_floor', ground) - elif construction.Name == 'Dompark Roof': - # roof - self._set_layers(_idf, 'user_roof', roof) - elif construction.Name == 'Dompark Roof Vegetation': - # roof - self._set_layers(_idf, 'user_roof_vegetation', roof, vegetation) - elif construction.Name == 'Dompark Wall': - # wall - self._set_layers(_idf, 'user_wall', wall) - else: - continue - for surface in _idf.idfobjects[self._SURFACE]: - if surface.Construction_Name == 'Project ground floor': - # floor - surface.Construction_Name = 'user_floor' - elif surface.Construction_Name == 'Dompark Wall': - # wall - surface.Construction_Name = 'user_wall' - elif surface.Construction_Name == 'Dompark Roof' or surface.Construction_Name == 'Dompark Roof Vegetation': - # roof - surface.Construction_Name = 'user_roof' - if self._greenery_percentage > 0: - if surface.Name in sh.roofs_associated_to_percentage[str(self._greenery_percentage)]: - surface.Construction_Name = 'user_roof_vegetation' - else: - continue - - for window in _idf.idfobjects[self._WINDOW_SURFACE]: - window.Construction_Name = 'window_construction_1' - - def _add_material(self, _idf, layers): - for layer in layers: - for material in _idf.idfobjects[self._MATERIAL]: - if material.Name == layer.material.name: - return - for material in _idf.idfobjects[self._MATERIAL_NOMASS]: - if material.Name == layer.material.name: - return - if str(layer.material.no_mass) == 'True': - _idf.newidfobject(self._MATERIAL_NOMASS, - Name=layer.material.name, - Roughness=self._ROUGHNESS, - Thermal_Resistance=layer.material.thermal_resistance, - Thermal_Absorptance=layer.material.thermal_absorptance, - Solar_Absorptance=layer.material.solar_absorptance, - Visible_Absorptance=layer.material.visible_absorptance - ) - else: - _idf.newidfobject(self._MATERIAL, - Name=layer.material.name, - Roughness=self._ROUGHNESS, - Thickness=layer.thickness, - Conductivity=layer.material.conductivity, - Density=layer.material.density, - Specific_Heat=layer.material.specific_heat, - Thermal_Absorptance=layer.material.thermal_absorptance, - Solar_Absorptance=layer.material.solar_absorptance, - Visible_Absorptance=layer.material.visible_absorptance - ) - - def _add_vegetation_material(self, _idf, vegetation): - for vegetation_material in _idf.idfobjects[self._MATERIAL_ROOFVEGETATION]: - if vegetation_material.Name == vegetation.name: - return - soil = vegetation.soil - height = 0 - leaf_area_index = 0 - leaf_reflectivity = 0 - leaf_emissivity = 0 - minimal_stomatal_resistance = 0 - for plant in vegetation.plants: - percentage = float(plant.percentage) / 100 - height += percentage * float(plant.height) - leaf_area_index += percentage * float(plant.leaf_area_index) - leaf_reflectivity += percentage * float(plant.leaf_reflectivity) - leaf_emissivity += percentage * float(plant.leaf_emissivity) - minimal_stomatal_resistance += percentage * float(plant.minimal_stomatal_resistance) - _idf.newidfobject(self._MATERIAL_ROOFVEGETATION, - Name=vegetation.name, - Height_of_Plants=height, - Leaf_Area_Index=leaf_area_index, - Leaf_Reflectivity=leaf_reflectivity, - Leaf_Emissivity=leaf_emissivity, - Minimum_Stomatal_Resistance=minimal_stomatal_resistance, - Soil_Layer_Name=soil.name, - Roughness=soil.roughness, - Thickness=vegetation.soil_thickness, - Conductivity_of_Dry_Soil=soil.dry_conductivity, - Density_of_Dry_Soil=soil.dry_density, - Specific_Heat_of_Dry_Soil=soil.dry_specific_heat, - Thermal_Absorptance=soil.thermal_absorptance, - Solar_Absorptance=soil.solar_absorptance, - Visible_Absorptance=soil.visible_absorptance, - Saturation_Volumetric_Moisture_Content_of_the_Soil_Layer= - soil.saturation_volumetric_moisture_content, - Residual_Volumetric_Moisture_Content_of_the_Soil_Layer= - soil.residual_volumetric_moisture_content, - Initial_Volumetric_Moisture_Content_of_the_Soil_Layer= - soil.initial_volumetric_moisture_content, - Moisture_Diffusion_Calculation_Method=self._SIMPLE - ) - - def _add_window_construction_and_material(self, _idf, window): - name = 'glazing_1' - _kwargs = {'Name': name, 'UFactor': window.overall_u_value, - 'Solar_Heat_Gain_Coefficient': window.g_value} - _idf.newidfobject(self._WINDOW_MATERIAL_SIMPLE, **_kwargs) - window_construction_name = 'window_construction_1' - _kwargs = {'Name': window_construction_name, 'Outside_Layer': name} - return _idf.newidfobject(self._CONSTRUCTION, **_kwargs) - - def _add_materials(self, _idf): - building = self._city.buildings[0] - ground_surface = building.grounds[0] - roof_surface = building.roofs[0] - wall_surface = building.walls[0] - internal_zone = building.internal_zones[0] - thermal_zone = internal_zone.thermal_zones[0] - ground = None - roof = None - roof_vegetation = None - wall = None - window = None - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.id == wall_surface.id: - wall = thermal_boundary.layers - if thermal_boundary.parent_surface.id == roof_surface.id: - roof = thermal_boundary.layers - roof_vegetation = thermal_boundary.vegetation - if thermal_boundary.parent_surface.id == ground_surface.id: - ground = thermal_boundary.layers - if thermal_boundary.thermal_openings is not None and len(thermal_boundary.thermal_openings) > 0: - window = thermal_boundary.thermal_openings[0] - if ground is not None and roof is not None and wall is not None and window is not None: - # we have all the needed surfaces type - break - self._add_material(_idf, ground) - self._add_material(_idf, roof) - self._add_material(_idf, wall) - if roof_vegetation is not None: - self._add_vegetation_material(_idf, roof_vegetation) - self._update_constructions(_idf, ground, roof, wall, roof_vegetation) - self._add_window_construction_and_material(_idf, window) - - def _add_standard_compact_hourly_schedule(self, _idf, schedule_name, schedules): - _kwargs = {'Name': f'{schedule_name}', - 'Schedule_Type_Limits_Name': self.idf_type_limits[schedules[0].data_type], - 'Field_1': 'Through: 12/31'} - for j, schedule in enumerate(schedules): - _val = schedule.values - _new_field = '' - for day_type in schedule.day_types: - _new_field += f' {self.idf_day_types[day_type]}' - _kwargs[f'Field_{j * 25 + 2}'] = f'For:{_new_field}' - for i in range(0, len(_val)): - _kwargs[f'Field_{j * 25 + 3 + i}'] = f'Until: {i + 1:02d}:00,{_val[i]}' - _idf.newidfobject(self._COMPACT_SCHEDULE, **_kwargs) - - def _add_schedules(self, _idf, thermal_zone): - self._add_standard_compact_hourly_schedule(_idf, 'user_occupancy_schedule', - thermal_zone.occupancy.occupancy_schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_lighting_schedule', - thermal_zone.lighting.schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_appliances_schedule', - thermal_zone.appliances.schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_heating_schedule', - thermal_zone.thermal_control.heating_set_point_schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_cooling_schedule', - thermal_zone.thermal_control.cooling_set_point_schedules) - - def _add_usage(self, _idf): - _thermal_zone = None - for building in self._city.buildings: - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - _thermal_zone = thermal_zone - # Dompark project share schedules and usages among all the buildings so we could add just the first one - break - break - break - self._add_schedules(_idf, _thermal_zone) - fraction_radiant = _thermal_zone.occupancy.sensible_radiative_internal_gain / \ - (_thermal_zone.occupancy.sensible_radiative_internal_gain + - _thermal_zone.occupancy.sensible_convective_internal_gain) - for idf_object in _idf.idfobjects[self._PEOPLE]: - idf_object['Number_of_People_Schedule_Name'] = 'user_occupancy_schedule' - idf_object['People_per_Zone_Floor_Area'] = _thermal_zone.occupancy.occupancy_density - idf_object['Fraction_Radiant'] = fraction_radiant - - for idf_object in _idf.idfobjects[self._LIGHTS]: - idf_object['Schedule_Name'] = 'user_lighting_schedule' - idf_object['Watts_per_Zone_Floor_Area'] = _thermal_zone.lighting.density - for idf_object in _idf.idfobjects[self._APPLIANCES]: - idf_object['Schedule_Name'] = 'user_appliances_schedule' - idf_object['Power_per_Zone_Floor_Area'] = _thermal_zone.appliances.density - for idf_object in _idf.idfobjects[self._HEATING_COOLING]: - idf_object['Heating_Setpoint_Temperature_Schedule_Name'] = 'user_heating_schedule' - idf_object['Cooling_Setpoint_Temperature_Schedule_Name'] = 'user_cooling_schedule' - - return - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self, city_id): - payload = request.get_json() - self._city = self.get_city(city_id) - - self._greenery_percentage = round(float(payload['greenery_percentage']) / 10) * 10 - output_file = str((self._output_path / 'dompark.idf').resolve()) - idd_file = str((self._data_path / 'energy+.idd').resolve()) - epw_file = str((self._data_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve()) - idf_file = str((self._data_path / 'dompark.idf').resolve()) - IDF.setiddname(idd_file) - _idf = IDF(idf_file, epw_file) - self._add_materials(_idf) - self._add_usage(_idf) - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy", - Reporting_Frequency="Hourly", - ) - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Zone Ideal Loads Supply Air Total Cooling Energy", - Reporting_Frequency="Hourly", - ) - # From EnergyPlus documentation: Lights Electric Energy [J] - # The lighting electrical consumption including ballasts, if present. These will have the same value as Lights - # Total Heating Energy (above). - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Lights Total Heating Energy", - Reporting_Frequency="Hourly", - ) - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Other Equipment Total Heating Energy", - Reporting_Frequency="Hourly", - ) - - _idf.match() - _idf.saveas(str(output_file)) - _idf.run(expandobjects=True, readvars=True, output_directory=self._output_path, output_prefix='dompark_') - # Todo: set the heating and cooling - heating = [] - cooling = [] - lighting = [] - appliances = [] - with open((self._output_path / f'dompark_out.csv').resolve()) as f: - reader = csv.reader(f, delimiter=',') - for row in reader: - if '00:00' in row[0]: - cooling_value = 0.0 - heating_value = 0.0 - lighting_value = 0.0 - appliances_value = 0.0 - for i in range(1, 38): - lighting_value += float(row[i]) - for i in range(38, 73): - appliances_value += float(row[i]) - for i in range(73, 133, 2): - heating_value += float(row[i]) - cooling_value += float(row[i + 1]) - cooling.append(cooling_value) - heating.append(heating_value) - lighting.append(lighting_value) - appliances.append(appliances_value) - - files = glob.glob(f'{self._output_path}/dompark*') - for file in files: - os.remove(file) - continue - - response = {'heating_demand': heating, - 'cooling_demand': cooling, - 'lighting_demand': lighting, - 'appliances_demand': appliances - } - return Response(json.dumps(response), status=200) diff --git a/hub_api/geometry.py b/hub_api/geometry.py deleted file mode 100644 index 9fdd68d..0000000 --- a/hub_api/geometry.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Geometry -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -from flask import make_response, send_file -from flask_restful import Resource -from pathlib import Path -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles - - -class Geometry(Resource): - def __init__(self): - data_path = (Path(__file__).parent.parent / 'data').resolve() - self._gtlf_path = (Path(data_path / 'DomparkBuilding.gltf')).resolve() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self): - response = make_response(send_file(self._gtlf_path, - as_attachment=True, - mimetype='model/gltf+json, model/gltf-binary')) - return response diff --git a/hub_api/greenery.py b/hub_api/greenery.py deleted file mode 100644 index 96dc84e..0000000 --- a/hub_api/greenery.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Greenery -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -import json - -from flask import Response, request -from flask_restful import Resource -from hub_api.config import Config -from city_model_structure.greenery.vegetation import Vegetation -from city_model_structure.greenery.soil import Soil -from city_model_structure.greenery.plant import Plant -import helpers.constants as cte -from persistence.models import UserRoles -from hub_api.helpers.auth import role_required - - -class Greenery(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value]) - def put(self, city_id): - - city = self.get_city(city_id) - try: - greenery_percentage = request.json['greenery_percentage'] - if greenery_percentage == 0: - response = {'result': 'succeed'} - return Response(json.dumps(response), status=200) - - building_names = request.json['building_names'] - vegetation_requested = request.json['vegetation'] - vegetation_name = vegetation_requested['vegetation_name'] - soil_thickness = vegetation_requested['soil_thickness'] - soil_name = vegetation_requested['soil_name'] - roughness = vegetation_requested['soil_roughness'] - dry_conductivity = vegetation_requested['dry_soil_conductivity'] - dry_density = vegetation_requested['dry_soil_density'] - dry_specific_heat = vegetation_requested['dry_soil_specific_heat'] - thermal_absorptance = vegetation_requested['soil_thermal_absorptance'] - solar_absorptance = vegetation_requested['soil_solar_absorptance'] - visible_absorptance = vegetation_requested['soil_visible_absorptance'] - saturation_volumetric_moisture_content = vegetation_requested['soil_saturation_volumetric_moisture_content'] - residual_volumetric_moisture_content = vegetation_requested['soil_residual_volumetric_moisture_content'] - soil = Soil(soil_name, roughness, dry_conductivity, dry_density, dry_specific_heat, thermal_absorptance, - solar_absorptance, visible_absorptance, saturation_volumetric_moisture_content, - residual_volumetric_moisture_content) - soil.initial_volumetric_moisture_content = '0.1' - plant_percentages = vegetation_requested['plant_percentages'] - plants = [] - for plant_percentage in plant_percentages: - plant_name = plant_percentage['plant_name'] - height = plant_percentage['plant_height'] - leaf_area_index = plant_percentage['plant_leaf_area_index'] - leaf_reflectivity = plant_percentage['plant_leaf_reflectivity'] - leaf_emissivity = plant_percentage['plant_leaf_emissivity'] - minimal_stomatal_resistance = plant_percentage['plant_minimal_stomatal_resistance'] - co2_sequestration = plant_percentage['plant_co2_sequestration'] - grows_on_soils = plant_percentage['plant_grows_on'] - plant = Plant(plant_name, height, leaf_area_index, leaf_reflectivity, leaf_emissivity, - minimal_stomatal_resistance, co2_sequestration, grows_on_soils) - plant.percentage = plant_percentage['plant_percentage'] - plants.append(plant) - vegetation = Vegetation(vegetation_name, soil, soil_thickness, plants) - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.type == cte.ROOF: - thermal_boundary.vegetation = vegetation - - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - - return Response(json.dumps(response)) diff --git a/hub_api/greenery_catalog.py b/hub_api/greenery_catalog.py deleted file mode 100644 index 504b86a..0000000 --- a/hub_api/greenery_catalog.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Greenery catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json -from flask import request, Response -from flask_restful import Resource -from catalog_factories.data_models.greenery.plant import Plant -from catalog_factories.data_models.greenery.soil import Soil -from catalog_factories.data_models.greenery.vegetation import Vegetation -from hub_api.helpers.session_helper import refresh_session - - -class ToJson: - @staticmethod - def plant_to_json(plant, percentage=None): - dictionary = { - 'plant_percentage': percentage, - 'plant_name': plant.name, - 'plant_category': plant.category, - 'plant_height': plant.height, - 'plant_leaf_area_index': plant.leaf_area_index, - 'plant_leaf_reflectivity': plant.leaf_reflectivity, - 'plant_leaf_emissivity': plant.leaf_emissivity, - 'plant_minimal_stomatal_resistance': plant.minimal_stomatal_resistance, - 'plant_co2_sequestration': plant.co2_sequestration, - 'plant_grows_on': [] - } - if percentage is None: - dictionary.pop('plant_percentage') - soils = [] - for soil in plant.grows_on: - soil_dic = ToJson.soil_to_json(soil) - soils.append(soil_dic) - dictionary['plant_grows_on'] = soils - return dictionary - - @staticmethod - def soil_to_json(soil): - dictionary = {'soil_name': soil.name, - 'soil_roughness': str(soil.roughness), - 'soil_dry_conductivity': soil.dry_conductivity, - 'soil_dry_density': soil.dry_density, - 'soil_dry_specific_heat': soil.dry_specific_heat, - 'soil_thermal_absortance': soil.thermal_absorptance, - 'soil_solar_absortance': soil.solar_absorptance, - 'soil_visible_absortance': soil.visible_absorptance, - 'soil_saturation_volumetric_moisture_content': soil.saturation_volumetric_moisture_content, - 'soil_residual_volumetric_moisture_content': soil.residual_volumetric_moisture_content, - 'soil_initial_volumetric_moisture_content': soil.initial_volumetric_moisture_content - } - return dictionary - - @staticmethod - def vegetation_to_json(vegetation): - - dictionary = {'vegetation_name': vegetation.name, - 'vegetation_category': vegetation.category, - 'soil_thickness': vegetation.soil_thickness, - 'management': str(vegetation.management), - 'air_gap': vegetation.air_gap, - 'soil_name': vegetation.soil_name, - 'soil_roughness': str(vegetation.soil_roughness), - 'dry_soil_conductivity': vegetation.dry_soil_conductivity, - 'dry_soil_density': vegetation.dry_soil_density, - 'dry_soil_specific_heat': vegetation.dry_soil_specific_heat, - 'soil_thermal_absorptance': vegetation.soil_thermal_absorptance, - 'soil_solar_absorptance': vegetation.soil_solar_absorptance, - 'soil_visible_absorptance': vegetation.soil_visible_absorptance, - 'soil_saturation_volumetric_moisture_content': vegetation.soil_saturation_volumetric_moisture_content, - 'soil_residual_volumetric_moisture_content': vegetation.soil_residual_volumetric_moisture_content, - 'soil_initial_volumetric_moisture_content': vegetation.soil_initial_volumetric_moisture_content, - 'plant_percentages': [] - } - percentages = [] - for percentage in vegetation.plant_percentages: - percentage_dic = ToJson.plant_to_json(percentage, percentage.percentage) - percentages.append(percentage_dic) - dictionary['plant_percentages'] = percentages - return dictionary - - -class GreeneryCatalogEntry(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.greenery_catalog - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {} - if isinstance(entry, Vegetation): - output['vegetations'] = ToJson.vegetation_to_json(entry) - if isinstance(entry, Plant): - output['plants'] = ToJson.plant_to_json(entry) - if isinstance(entry, Soil): - output['soils'] = ToJson.soil_to_json(entry) - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class GreeneryCatalogEntries(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.greenery_catalog - category = None - if request.data != b'': - category = request.json['category'] - output = {} - if category is None: - content = catalog.entries() - output = {'vegetations': [], 'plants': [], 'soils': []} - for vegetation in content.vegetations: - output['vegetations'].append(ToJson.vegetation_to_json(vegetation)) - for plant in content.plants: - output['plants'].append(ToJson.plant_to_json(plant)) - for soil in content.soils: - output['soils'].append(ToJson.soil_to_json(soil)) - else: - try: - content = catalog.entries(category) - output[category] = [] - for entry in content: - if isinstance(entry, Vegetation): - output[category].append(ToJson.vegetation_to_json(entry)) - if isinstance(entry, Plant): - output[category].append(ToJson.plant_to_json(entry)) - if isinstance(entry, Soil): - output[category].append(ToJson.soil_to_json(entry)) - except ValueError: - output = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(output), headers=headers, status=400) - return Response(json.dumps(output), headers=headers) - - -class GreeneryCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.greenery_catalog - category = None - if request.data != b'': - category = request.json['category'] - if category is None: - return Response(json.dumps(catalog.names()), headers=headers) - else: - try: - return Response(json.dumps(catalog.names(category)), headers=headers) - except ValueError: - response = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) diff --git a/hub_api/heat_pump.py b/hub_api/heat_pump.py deleted file mode 100644 index 54aa214..0000000 --- a/hub_api/heat_pump.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -HeatPump Service -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com -""" -import json - -from flask import request, Response -from hub_api.config import Config -from flask_restful import Resource -from hub_api.helpers.auth import role_required -from utils import HeatPumpSimulator -from utils import validate_hp_model -from persistence.models import UserRoles -from utils import expand_energy_demand -from hub_logger import logger - - -class HeatPump(Config, Resource): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self, city_id): - payload = request.get_json() - city = self.get_city(city_id) - if validate_hp_model(payload['HeatPumpType'], payload['HeatPumpModel']): - # expand energy demand values - expand_energy_demand(payload['EnergyDemand']) - try: - # Run simulation and return output file here - hp_simulator = HeatPumpSimulator(city, payload) - results = hp_simulator.run_hp_simulation() - return Response(json.dumps(results), status=200) - except Exception as err: - logger.error(err) - return Response(json.dumps({'error_message': 'Sorry an error occurred while running HP Simulation'})) - else: - return Response(json.dumps({'error_message': 'Wrong heat pump type/model combination'}), status=400) diff --git a/hub_api/helpers/auth.py b/hub_api/helpers/auth.py index 76fd2bd..0a04845 100644 --- a/hub_api/helpers/auth.py +++ b/hub_api/helpers/auth.py @@ -11,8 +11,8 @@ import os from jwt.utils import get_int_from_datetime from functools import wraps from flask import request, g -from hub_logger import logger -from persistence.models import UserRoles +from hub.hub_logger import logger +from hub.persistence.models import UserRoles from jwt.exceptions import JWTException instance = JWT() diff --git a/hub_api/lca.py b/hub_api/lca.py deleted file mode 100644 index b4e516a..0000000 --- a/hub_api/lca.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -LCA -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2021 Project Author name Atiya -Code contributors: Peter Yefi peteryefi@gmail.com -""" -import json -from flask import Response -from flask_restful import Resource -from lca_calculations import LcaCalculations -from itertools import groupby -from operator import itemgetter -from hub_api.helpers.auth import role_required -from hub_api.config import Config -from persistence.models import UserRoles - - - -class MaterialLCACatalog(Resource, Config): - def __init__(self): - super().__init__() - - @staticmethod - def get_lca_value(city, nrel_id = None): - nrel_material = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "14", "15", "16", "18", "20", "21", - "22", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114"] - lca_material = ["12", "32", "21", "25", "11", "20", "40", "27", "29", "28", "19", "40", "41", "25", "32", "32", "32", "21", "41", - "21", "11", "33", "28", "11", "40", "1", "28", "41", "0", "0", "0", "0", "0", ] - keys = ['nrel_material_id', 'lca_material_id'] - material_mapping = [dict(zip(keys, i)) for i in zip(nrel_material, lca_material)] - - if nrel_id is None: - materials_lca = {"material_lca_catalog": []} - for mat in material_mapping: - - if mat["nrel_material_id"] not in ("110", "111", "112", "113", "114"): - for material in city.lca_materials: - if(int(mat["lca_material_id"]) == material.id): - material_lca_catalog = {} - material_lca_catalog['nrel_material_id'] = mat["nrel_material_id"] - material_lca_catalog['embodied_carbon'] = material.embodied_carbon - mat_end_of_life = LcaCalculations(city).end_life_carbon(material.type) - material_lca_catalog['end_of_life_carbon'] = mat_end_of_life - materials_lca["material_lca_catalog"].append(material_lca_catalog) - - else: - material_lca_catalog = {} - material_lca_catalog['nrel_material_id'] = mat["nrel_material_id"] - material_lca_catalog['embodied_carbon'] = 0.0 - material_lca_catalog['end_of_life_carbon'] = 0.0 - materials_lca["material_lca_catalog"].append(material_lca_catalog) - - return materials_lca - - else: - for mat in material_mapping: - if mat["nrel_material_id"] == str(nrel_id): - for material in city.lca_materials: - if (material.id == int(mat["lca_material_id"])): - return material.embodied_carbon, material.id, material.type, material.name, material.density - # return material.embodied_carbon - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self, city_id): - city = self.get_city(city_id) - try: - return Response(json.dumps(self.get_lca_value(city)), status=200) - except ValueError: - response = {'err_msg': f'No Catalog Available'} - return Response(json.dumps(response), status=400) - - -class MaterialLCACalculations(Resource, Config): - """ - LCA class - """ - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self, city_id): - """ - Auto-method for processing the lca request - :return: lca demand - """ - city = self.get_city(city_id) - - materials_lca = {'Wall': [], 'Ground': [], 'Roof': []} - for building in city.buildings: - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - for i, layer in enumerate(thermal_boundary.layers): - material_layers_lca = {} - if layer.material.no_mass == False: - embodied_carbon, mat_id, mat_type, mat_name, mat_density = MaterialLCACatalog.get_lca_value(city, layer.material.id) - material_layers_lca['layer_name'] = f'Layer {i+1}' - material_layers_lca['nrel_material_id'] = layer.material.id - material_layers_lca['lca_material_id'] = mat_id - material_layers_lca['embodied_carbon'] = (layer.thickness * thermal_boundary.opaque_area) * mat_density * embodied_carbon - mat_end_of_life = LcaCalculations(city).end_life_carbon(mat_type) - material_end_of_life = mat_end_of_life * (layer.thickness * thermal_boundary.opaque_area) * mat_density - material_layers_lca['end_of_life_per_layer'] = material_end_of_life - materials_lca[thermal_boundary.type].append(material_layers_lca) - else: - material_layers_lca['layer_name'] = f'Layer {i+1}' - material_layers_lca['nrel_material_id'] = layer.material.id - material_layers_lca['lca_material_id'] = mat_id - material_layers_lca['embodied_carbon'] = (layer.thickness * thermal_boundary.opaque_area) * mat_density * embodied_carbon - material_layers_lca['end_of_life_per_layer'] = 0.0 - materials_lca[thermal_boundary.type].append(material_layers_lca) - - materials_embodied_carbon = {'Wall': [], 'Ground': [], 'Roof': []} - for key, value in materials_lca.items(): - boundary_layers = sorted(value, key=itemgetter('layer_name')) - for b_layer, layer_properties in groupby(boundary_layers, key=itemgetter('layer_name')): - sum_embodied = 0.0 - sum_end_of_life = 0.0 - total_embodied_carbon = {} - for k in layer_properties: - sum_embodied += k["embodied_carbon"] - sum_end_of_life += k["end_of_life_per_layer"] - total_embodied_carbon['layer_name'] = b_layer - total_embodied_carbon['embodied_carbon'] = sum_embodied - total_embodied_carbon['end_of_life_carbon'] = sum_end_of_life - materials_embodied_carbon[key].append(total_embodied_carbon) - - return Response(json.dumps(materials_embodied_carbon), status=200) diff --git a/hub_api/usage.py b/hub_api/usage.py deleted file mode 100644 index 8d127f7..0000000 --- a/hub_api/usage.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Usage -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -import json -from flask import Response, request -from flask_restful import Resource - -from city_model_structure.attributes.schedule import Schedule -from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.lighting import Lighting -from city_model_structure.building_demand.occupancy import Occupancy -from city_model_structure.building_demand.thermal_control import ThermalControl -from city_model_structure.building_demand.usage_zone import UsageZone -import helpers.constants as cte -from hub_api.helpers.auth import role_required -from hub_api.config import Config -from persistence.models import UserRoles - - -class Usage(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def put(self, city_id): - - city = self.get_city(city_id) - catalog = request.json['usage_catalog'] - - usage_name = None - try: - building_names = request.json['building_names'] - usages = request.json['usages'] - seconds_in_hour = cte.MINUTES_TO_SECONDS * cte.HOUR_TO_MINUTES - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - internal_zone.usage_zones = [] - for usage in usages: - usage_entry = catalog.get_entry(usage['name']) - occupancy_schedules = [] - for schedule in usage_entry.occupancy.schedules: - occupancy_schedule = Schedule() - occupancy_schedule.type = schedule.type - occupancy_schedule.values = schedule.values - occupancy_schedule.data_type = schedule.data_type - occupancy_schedule.time_step = schedule.time_step - occupancy_schedule.time_range = schedule.time_range - occupancy_schedule.day_types = schedule.day_types - occupancy_schedules.append(occupancy_schedule) - occupancy = Occupancy() - occupancy.occupancy_density = usage_entry.occupancy.occupancy_density - occupancy.sensible_convective_internal_gain = usage_entry.occupancy.sensible_convective_internal_gain - occupancy.sensible_radiative_internal_gain = usage_entry.occupancy.sensible_radiative_internal_gain - occupancy.latent_internal_gain = usage_entry.occupancy.latent_internal_gain - occupancy.occupancy_schedules = occupancy_schedules - - lighting_schedules = [] - for schedule in usage_entry.lighting.schedules: - lighting_schedule = Schedule() - lighting_schedule.type = schedule.type - lighting_schedule.values = schedule.values - lighting_schedule.data_type = schedule.data_type - lighting_schedule.time_step = schedule.time_step - lighting_schedule.time_range = schedule.time_range - lighting_schedule.day_types = schedule.day_types - lighting_schedules.append(lighting_schedule) - lighting = Lighting() - lighting.density = usage_entry.lighting.density - lighting.convective_fraction = usage_entry.lighting.convective_fraction - lighting.radiative_fraction = usage_entry.lighting.radiative_fraction - lighting.latent_fraction = usage_entry.lighting.latent_fraction - lighting.schedules = lighting_schedules - appliances_schedules = [] - for schedule in usage_entry.appliances.schedules: - appliances_schedule = Schedule() - appliances_schedule.type = schedule.type - appliances_schedule.values = schedule.values - appliances_schedule.data_type = schedule.data_type - appliances_schedule.time_step = schedule.time_step - appliances_schedule.time_range = schedule.time_range - appliances_schedule.day_types = schedule.day_types - appliances_schedules.append(appliances_schedule) - appliances = Appliances() - appliances.density = usage_entry.appliances.density - appliances.convective_fraction = usage_entry.appliances.convective_fraction - appliances.radiative_fraction = usage_entry.appliances.radiative_fraction - appliances.latent_fraction = usage_entry.appliances.latent_fraction - appliances.schedules = appliances_schedules - hvac_schedules = [] - for schedule in usage_entry.thermal_control.hvac_availability_schedules: - hvac_schedule = Schedule() - hvac_schedule.type = schedule.type - hvac_schedule.values = schedule.values - hvac_schedule.data_type = schedule.data_type - hvac_schedule.time_step = schedule.time_step - hvac_schedule.time_range = schedule.time_range - hvac_schedule.day_types = schedule.day_types - hvac_schedules.append(hvac_schedule) - heating_schedules = [] - for schedule in usage_entry.thermal_control.heating_set_point_schedules: - heating_schedule = Schedule() - heating_schedule.type = schedule.type - heating_schedule.values = schedule.values - heating_schedule.data_type = schedule.data_type - heating_schedule.time_step = schedule.time_step - heating_schedule.time_range = schedule.time_range - heating_schedule.day_types = schedule.day_types - heating_schedules.append(heating_schedule) - cooling_schedules = [] - for schedule in usage_entry.thermal_control.cooling_set_point_schedules: - cooling_schedule = Schedule() - cooling_schedule.type = schedule.type - cooling_schedule.values = schedule.values - cooling_schedule.data_type = schedule.data_type - cooling_schedule.time_step = schedule.time_step - cooling_schedule.time_range = schedule.time_range - cooling_schedule.day_types = schedule.day_types - cooling_schedules.append(cooling_schedule) - thermal_control = ThermalControl() - thermal_control.mean_heating_set_point = usage_entry.thermal_control.mean_heating_set_point - thermal_control.heating_set_back = usage_entry.thermal_control.heating_set_back - thermal_control.mean_cooling_set_point = usage_entry.thermal_control.mean_cooling_set_point - thermal_control.hvac_availability_schedules = hvac_schedules - thermal_control.heating_set_point_schedules = heating_schedules - thermal_control.cooling_set_point_schedules = cooling_schedules - usage_zone = UsageZone() - usage_zone.usage = usage_entry.usage - usage_zone.percentage = usage['percentage'] - usage_zone.hours_day = usage_entry.hours_day - usage_zone.days_year = usage_entry.days_year - usage_zone.mechanical_air_change = usage_entry.mechanical_air_change - if usage_entry.mechanical_air_change is None: - usage_zone.mechanical_air_change = ((usage_entry.ventilation_rate * internal_zone.area) / - internal_zone.volume) * seconds_in_hour - usage_zone.occupancy = occupancy - usage_zone.lighting = lighting - usage_zone.appliances = appliances - usage_zone.thermal_control = thermal_control - internal_zone.usage_zones.append(usage_zone) - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - except IndexError: - response = {'error': f'Name "{usage_name}" unknown'} - return Response(json.dumps(response), status=400) - return Response(json.dumps(response)) diff --git a/hub_api/usage_catalog.py b/hub_api/usage_catalog.py deleted file mode 100644 index 026df07..0000000 --- a/hub_api/usage_catalog.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Construction catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json - -from flask import request, Response -from flask_restful import Resource - -from hub_api.helpers.session_helper import refresh_session - - -class ToJson: - @staticmethod - def usage_to_json(usage): - dictionary = { - 'usage': usage.usage, - 'hours_day': usage.hours_day, - 'days_year': usage.days_year, - 'mechanical_air_change': usage.mechanical_air_change if usage.mechanical_air_change is not None else '', - 'ventilation_rate': usage.ventilation_rate if usage.ventilation_rate is not None else '', - 'occupancy': ToJson.occupancy_to_json(usage.occupancy), - 'lighting': ToJson.lighting_to_json(usage.lighting), - 'appliances': ToJson.appliances_to_json(usage.appliances), - 'thermal_control': ToJson.thermal_control_to_json(usage.thermal_control) - } - return dictionary - - @staticmethod - def occupancy_to_json(occupancy): - dictionary = { - 'occupancy_density': occupancy.occupancy_density, - 'sensible_convective_internal_gain': occupancy.sensible_convective_internal_gain, - 'sensible_radiative_internal_gain': occupancy.sensible_radiative_internal_gain, - 'latent_internal_gain': occupancy.latent_internal_gain, - 'schedules': [] - } - for schedule in occupancy.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def lighting_to_json(lighting): - dictionary = { - 'density': lighting.density, - 'convective_fraction': lighting.convective_fraction, - 'radiative_fraction': lighting.radiative_fraction, - 'latent_fraction': lighting.latent_fraction, - 'schedules': [] - } - for schedule in lighting.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def appliances_to_json(appliances): - dictionary = { - 'density': appliances.density, - 'convective_fraction': appliances.convective_fraction, - 'radiative_fraction': appliances.radiative_fraction, - 'latent_fraction': appliances.latent_fraction, - 'schedules': [] - } - for schedule in appliances.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def thermal_control_to_json(thermal_control): - dictionary = { - 'mean_heating_set_point': thermal_control.mean_heating_set_point, - 'heating_set_back': thermal_control.heating_set_back, - 'mean_cooling_set_point': thermal_control.mean_cooling_set_point, - 'hvac_availability_schedules': [], - 'heating_set_point_schedules': [], - 'cooling_set_point_schedules': [], - } - for schedule in thermal_control.hvac_availability_schedules: - dictionary['hvac_availability_schedules'].append(ToJson.schedule_to_json(schedule)) - for schedule in thermal_control.heating_set_point_schedules: - dictionary['heating_set_point_schedules'].append(ToJson.schedule_to_json(schedule)) - for schedule in thermal_control.cooling_set_point_schedules: - dictionary['cooling_set_point_schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def schedule_to_json(schedule): - schedule_dictionary = { - 'type': schedule.type, - 'data_type': schedule.data_type, - 'time_step': schedule.time_step, - 'time_range': schedule.time_range, - 'day_types': schedule.day_types, - 'values': schedule.values, - } - return schedule_dictionary - - -class UsageCatalogEntry(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {'usages': [ToJson.usage_to_json(entry)]} - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class UsageCatalogEntries(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - output = {'usages': []} - content = catalog.entries() - for usage in content.usages: - output['usages'].append(ToJson.usage_to_json(usage)) - return Response(json.dumps(output), headers=headers) - - -class UsageCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - return Response(json.dumps(catalog.names()), headers=headers) diff --git a/hub_api/user.py b/hub_api/user.py index e2e97b5..d7e0936 100644 --- a/hub_api/user.py +++ b/hub_api/user.py @@ -6,12 +6,12 @@ Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com import json from flask import Response, request from flask_restful import Resource -from imports.user_factory import UserFactory -from exports.user_factory import UserFactory as ExUserFactory +from hub.imports.user_factory import UserFactory +from hub.exports.user_factory import UserFactory as ExUserFactory import os -from hub_logger import logger +from hub.hub_logger import logger from hub_api.helpers.auth import generate_auth_token, role_required -from persistence.models import UserRoles +from hub.persistence.models import UserRoles class User(Resource): From d96124107967801b8a9c46be15a0d208b3ff6789 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Wed, 1 Feb 2023 15:48:14 -0500 Subject: [PATCH 4/7] Added tests for user --- bootstrap.py | 3 +- hub_api/city_info.py | 12 +++-- hub_api/config.py | 20 ++++++- hub_api/user.py | 36 ++++++------- tests/__init__.py | 0 tests/base_test.py | 58 ++++++++++++++++++++ tests/test_user.py | 124 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 227 insertions(+), 26 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/base_test.py create mode 100644 tests/test_user.py diff --git a/bootstrap.py b/bootstrap.py index 5c14aab..a2353ec 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -54,4 +54,5 @@ def home(): return Response(headers={'Access-Control-Allow-Origin': '*'}) -app.run(port=15789, host="0.0.0.0", debug=False) +if __name__ == '__main__': + app.run(port=15789, host="0.0.0.0", debug=False) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index afe2e1a..292eb33 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -15,6 +15,8 @@ from pathlib import Path import os from hub_api.config import Config +headers = {'Content-Type': 'application/json'} + class CityInfo(Resource, Config): def __init__(self): @@ -32,8 +34,8 @@ class CityInfo(Resource, Config): 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, 'role': city.user.role.value} - }, default=str), status=200) - return Response(response=json.dumps({'err_msg': 'City not found'}), status=404) + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404, headers=headers) class City(Resource, Config): @@ -71,9 +73,9 @@ class City(Resource, Config): 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, 'role': saved_city.user.role.value} }, default=str), status=201) - return Response(response=json.dumps(saved_city), status=200) + return Response(response=json.dumps(saved_city), status=200, headers=headers) else: - return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400, headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400, headers=headers) diff --git a/hub_api/config.py b/hub_api/config.py index a00b6ca..be8e79a 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -6,15 +6,31 @@ Copyright © 2023 Project Peter Yefi peteryefi@gmail.com from hub.exports.db_factory import DBFactory as CityExportFactory from hub.imports.db_factory import DBFactory import os +from hub.imports.user_factory import UserFactory +from hub.exports.user_factory import UserFactory as ExUserFactory class Config: def __init__(self): - self.export_db_factory = CityExportFactory(db_name='hub_prod', app_env='PROD', + db_name = None + app_env = None + if os.getenv("FLASK_ENV") == 'production': + db_name = 'hub_prod' + app_env = 'PROD' + elif os.getenv("FLASK_ENV") == 'testing': + db_name = 'test_db' + app_env = 'TEST' + + self.export_db_factory = CityExportFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) - self.import_db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + self.import_db_factory = DBFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) + self.user_factory = UserFactory(db_name=db_name, app_env=app_env, + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + self.ex_user_factory = ExUserFactory(db_name=db_name, app_env=app_env, + dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): return self.export_db_factory.get_city(city_id) diff --git a/hub_api/user.py b/hub_api/user.py index d7e0936..3f5d01f 100644 --- a/hub_api/user.py +++ b/hub_api/user.py @@ -6,18 +6,17 @@ Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com import json from flask import Response, request from flask_restful import Resource -from hub.imports.user_factory import UserFactory -from hub.exports.user_factory import UserFactory as ExUserFactory -import os from hub.hub_logger import logger from hub_api.helpers.auth import generate_auth_token, role_required from hub.persistence.models import UserRoles +from hub_api.config import Config + +headers = {'Content-Type': 'application/json'} -class User(Resource): +class User(Resource, Config): def __init__(self): - self.user_factory = UserFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + super().__init__() @role_required([UserRoles.Admin.value]) def post(self): @@ -26,12 +25,14 @@ class User(Resource): user = self.user_factory.create_user(name=payload["name"], email=payload["email"], password=payload["password"], role=payload["role"]) if type(user) is dict: - return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps(user), status=400, headers=headers) return Response(response=json.dumps({'user': {'id': user.id, 'name': user.name, 'email': user.email, - 'password': user.password, 'role': user.role.value}}), status=201) + 'password': user.password, 'role': user.role.value}}), status=201, + headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400, + headers=headers) @role_required([UserRoles.Admin.value]) def put(self): @@ -40,25 +41,24 @@ class User(Resource): res = self.user_factory.update_user(user_id=payload['id'], name=payload['name'], password=payload['password'], role=payload['role'], email=payload['email']) if res: - return Response(response=json.dumps(res), status=400) - return Response(response=json.dumps({'success': 'user updated successfully'}), status=200) + return Response(response=json.dumps(res), status=400, headers=headers) + return Response(response=json.dumps({'success': 'user updated successfully'}), status=200, headers=headers) except Exception as err: logger.error(err) return Response(response=json.dumps({'err_msg': 'Sorry, an error occurred while updating user'}), - status=400) + status=400, headers=headers) -class UserLogin(Resource): +class UserLogin(Resource, Config): def __init__(self): - self.user_factory = ExUserFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + super().__init__() def post(self): try: payload = request.get_json() - user = self.user_factory.login_user(email=payload["email"], password=payload["password"]) + user = self.ex_user_factory.login_user(email=payload["email"], password=payload["password"]) if type(user) is dict: - return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps(user), status=400, headers=headers) user = user[0] user_dict = { 'user': { @@ -70,7 +70,7 @@ class UserLogin(Resource): } } user_dict['token'] = generate_auth_token(user_dict) - return Response(response=json.dumps(user_dict), status=200) + return Response(response=json.dumps(user_dict), status=200, headers=headers) except Exception as err: logger.error(err) return Response(response=json.dumps({'err_msg': 'An error occurred while authenticating user'}), status=400) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/base_test.py b/tests/base_test.py new file mode 100644 index 0000000..cdc25b5 --- /dev/null +++ b/tests/base_test.py @@ -0,0 +1,58 @@ +import os +from unittest import TestCase +from bootstrap import app +from hub.persistence.base_repo import BaseRepo +from sqlalchemy import create_engine +from hub.persistence.models import City +from hub.persistence.models import User +from sqlalchemy.exc import ProgrammingError + + +# function to ensure tests run in order shown in fle +def arrange(): + order = {} + + def ordered(f): + order[f.__name__] = len(order) + return f + + def compare(a, b): + return [1, -1][order[a] < order[b]] + + return ordered, compare + + +class BaseTest(TestCase): + """ + Tests for payment resource + """ + + @classmethod + def setUpClass(cls): + os.environ['FLASK_ENV'] = 'testing' + cls.app = app + cls.client = cls.app.test_client() + + # Create test database + repo = BaseRepo(db_name='test_db', app_env='TEST', dotenv_path="{}/.env".format(os.path.expanduser('~'))) + eng = create_engine(f'postgresql://{repo.config.get_db_user()}@/{repo.config.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') + + cnn = eng.connect() + cnn.execute('commit') + cnn.execute("CREATE DATABASE test_db") + cnn.close() + User.__table__.create(bind=repo.engine, checkfirst=True) + City.__table__.create(bind=repo.engine, checkfirst=True) + + @classmethod + def tearDownClass(cls) -> None: + pass diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..0993b24 --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,124 @@ +import json +from .base_test import BaseTest, arrange +import unittest +from hub_api.helpers.auth import generate_auth_token + +ordered, compare = arrange() +unittest.defaultTestLoader.sortTestMethodsUsing = compare + + +class UserTest(BaseTest): + """ + Tests for User API endpoints + """ + + @classmethod + def setUpClass(cls): + # Call setUp from parent + super().setUpClass() + cls.user_dict = {"user": { + "name": "Test User", + "email": "testuser@gmail.com", + "password": "TestUser@12345", + "role": "Admin", + }} + cls.token = generate_auth_token(cls.user_dict) + + @ordered + def test_create_user_by_non_admin(self): + # When + res = self.client.post('/v1.4/user', data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('Invalid payload', res.json['messages']) + self.assertEqual(400, res.status_code) + + + @ordered + def test_create_user_by_admin(self): + # When + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + user = res.json + self.assertEqual(201, res.status_code) + self.assertEqual(type(user['user']), dict) + self.assertEqual(user['user']['email'], self.user_dict['user']['email']) + self.assertEqual(user['user']['role'], self.user_dict['user']['role']) + + @ordered + def test_create_user_with_existing_email(self): + # When + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('user with testuser@gmail.com email already exists', res.json['message']) + self.assertEqual(400, res.status_code) + + @ordered + def test_create_user_with_weak_password(self): + # When + self.user_dict['user']['password'] = '1234' + self.user_dict['user']['email'] = 'new@gmail.com' + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('Sorry an error occurred while creating user', res.json['err_msg']) + self.assertEqual(400, res.status_code) + + + @ordered + def test_login_user_with_wrong_credential(self): + # When + res = self.client.post('/v1.4/user/login', + headers={ + 'Content-Type': 'application/json' + }, + data=json.dumps({'email': 'wrong@gmail.com', 'password': 'wrong'})) + + # Then + message = res.json + self.assertEqual('user not found', message['message']) + self.assertEqual(400, res.status_code) + + @ordered + def test_login_user_with_correct_credential(self): + # When + self.user_dict['user']['password'] = 'TestUser@12345' + self.user_dict['user']['email'] = 'testuser@gmail.com' + login_data = { + "email": self.user_dict['user']['email'], + "password": self.user_dict['user']['password'] + } + res = self.client.post('/v1.4/user/login', headers={'Content-Type': 'application/json'}, + data=json.dumps(login_data)) + + # Then + user = res.json + self.assertEqual(user['user']['email'], self.user_dict['user']['email']) + self.assertEqual(user['user']['role'], self.user_dict['user']['role']) + self.assertIsNotNone(user['token']) + self.assertEqual(200, res.status_code) + + @classmethod + def tearDownClass(cls) -> None: + # Call tearDown from parent + super().tearDownClass() + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(unittest.TestLoader( + ).loadTestsFromTestCase(UserTest)) + + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) From 0aef0c32fcb2b7e44d0e8cad203cd6bc9c0d69d6 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Wed, 1 Feb 2023 15:58:20 -0500 Subject: [PATCH 5/7] Removed deprecated FLASK_ENV --- hub_api/config.py | 4 ++-- tests/base_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hub_api/config.py b/hub_api/config.py index be8e79a..626563e 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -15,10 +15,10 @@ class Config: def __init__(self): db_name = None app_env = None - if os.getenv("FLASK_ENV") == 'production': + if os.getenv("FLASK_DEBUG") == 'production': db_name = 'hub_prod' app_env = 'PROD' - elif os.getenv("FLASK_ENV") == 'testing': + elif os.getenv("FLASK_DEBUG") == 'testing': db_name = 'test_db' app_env = 'TEST' diff --git a/tests/base_test.py b/tests/base_test.py index cdc25b5..627aef1 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -29,7 +29,7 @@ class BaseTest(TestCase): @classmethod def setUpClass(cls): - os.environ['FLASK_ENV'] = 'testing' + os.environ['FLASK_DEBUG'] = 'testing' cls.app = app cls.client = cls.app.test_client() From 7b6620eca079d73c68bd9817c0a681fbd6c0d48f Mon Sep 17 00:00:00 2001 From: Koa Date: Thu, 2 Feb 2023 14:30:06 -0500 Subject: [PATCH 6/7] Update bootstrap.py --- bootstrap.py | 5 ++-- hub_api/city_commands.py | 43 +++++----------------------------- hub_api/docs/openapi-specs.yml | 2 +- 3 files changed, 9 insertions(+), 41 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index de25852..02e549e 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -12,9 +12,9 @@ from hub_api.city_info import CityInfo, City from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive from hub_api.uptime import Uptime from hub_api.user import User, UserLogin -from hub_api.city_commands import SaveCity, UpdateCity, DeleteCity, ListCities, SearchCity -from flasgger import LazyJSONEncoder, Swagger from flask import Response +from hub_api.city_commands import UpdateCity, DeleteCity, ListCities, SearchCity +from flasgger import LazyJSONEncoder, LazyString, Swagger app = flask.Flask('gamification') app.json_encoder = LazyJSONEncoder @@ -48,7 +48,6 @@ api.add_resource(SessionEnd, '/v1.4/session/end') api.add_resource(KeepSessionAlive, '/v1.4/session/keep_alive') api.add_resource(CityInfo, '/v1.4/city/') api.add_resource(City, '/v1.4/city') -api.add_resource(SaveCity, '/v1.4/city/save_city') api.add_resource(UpdateCity, '/v1.4/city/update_city') api.add_resource(DeleteCity, '/v1.4/city/delete_city') api.add_resource(ListCities, '/v1.4/city/list_cities') diff --git a/hub_api/city_commands.py b/hub_api/city_commands.py index 58b15c6..2c3025e 100644 --- a/hub_api/city_commands.py +++ b/hub_api/city_commands.py @@ -1,5 +1,5 @@ """ -HeatPump Service +City Commands SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2023 Project Author Koa Wells kekoa.wells@concordia.ca """ @@ -7,11 +7,10 @@ Copyright © 2023 Project Author Koa Wells kekoa.wells@concordia.ca import json from flask import Response, request from flask_restful import Resource +from hub_api import session from hub_api.helpers.session_helper import refresh_session from hub_api.helpers.auth import role_required -import os from imports.db_factory import DBFactory -from imports.user_factory import UserFactory from imports.geometry_factory import GeometryFactory from hub_logger import logger from persistence.models import UserRoles @@ -20,26 +19,9 @@ import os # Admin user commands -# class SaveCity(Resource): -# """ -# SaveCity class performs an admin API call to save an instantiated city into the database -# """ -# -# def __init__(self): -# self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', -# dotenv_path="{}/.env".format(os.path.expanduser('~'))) -# -# # todo: implement feature to persist instantiated city into database -# @role_required([UserRoles.Admin.value]) -# def put(self): -# session = refresh_session(request) -# if session is None: -# return Response(json.dumps({'error': 'invalid session'}), status=401) -# headers = session.headers - class UpdateCity(Resource): """ - UpdateCity class performs an admin API call to update a city that already exists inside the database + UpdateCity class performs an API call to update a city that already exists inside the database """ def __init__(self): @@ -48,9 +30,6 @@ class UpdateCity(Resource): @role_required([UserRoles.Admin.value]) def put(self, city_id): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) headers = session.headers allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} @@ -70,7 +49,7 @@ class UpdateCity(Resource): city_file.save(file_path) city = GeometryFactory(city_file_type, file_path).city - saved_city = self.db_factory.update_city(city.city_id, city) + saved_city = self.db_factory.update_city(city_id, city) if os.path.exists(file_path): os.remove(file_path) @@ -105,10 +84,6 @@ class DeleteCity(Resource): @role_required([UserRoles.Admin.value]) def delete(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - try: payload = request.get_json() return Response(json.dumps(response=DBFactory.delete_city(city_id=payload["city_id"]), status=201)) @@ -129,10 +104,7 @@ class ListCities(Resource): dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers + pass class SearchCity(Resource): """ @@ -144,7 +116,4 @@ class SearchCity(Resource): dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get(self, city_id): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers + pass diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index e15dadb..486a35e 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -15,7 +15,7 @@ paths: - city summary: Create a city operationId: createCity - description: Create a new city with a file upoload + description: Create a new city with a file upload parameters: - in: header name: appId From 347345b34390dc322a931dd168ac6c420a1a909d Mon Sep 17 00:00:00 2001 From: Koa Date: Thu, 9 Feb 2023 09:45:25 -0500 Subject: [PATCH 7/7] Add update, delete, search city API calls and other bug fixes/code refactors --- bootstrap.py | 23 +++--- hub_api/city_commands.py | 119 ---------------------------- hub_api/city_info.py | 122 ++++++++++++++++++++++++++-- hub_api/config.py | 13 ++- hub_api/docs/openapi-specs.yml | 141 +++++++++++++++++++++++++++++++++ hub_api/uptime.py | 3 +- requirements.txt | 1 + 7 files changed, 282 insertions(+), 140 deletions(-) delete mode 100644 hub_api/city_commands.py diff --git a/bootstrap.py b/bootstrap.py index 02e549e..7315fce 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -4,17 +4,20 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2021 Project Author name guillermo.gutierrezmorote@concordia.ca Project Collaborator name peteryefi@gmail.com """ - +import datetime +import hub_api.helpers.session_helper as sh import flask import yaml from flask_restful import Api -from hub_api.city_info import CityInfo, City +from hub_api.city_info import CityInfo, CitySearch, City from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive from hub_api.uptime import Uptime from hub_api.user import User, UserLogin from flask import Response -from hub_api.city_commands import UpdateCity, DeleteCity, ListCities, SearchCity from flasgger import LazyJSONEncoder, LazyString, Swagger +#from persistence.db_setup import DBSetup + +import os app = flask.Flask('gamification') app.json_encoder = LazyJSONEncoder @@ -48,18 +51,14 @@ api.add_resource(SessionEnd, '/v1.4/session/end') api.add_resource(KeepSessionAlive, '/v1.4/session/keep_alive') api.add_resource(CityInfo, '/v1.4/city/') api.add_resource(City, '/v1.4/city') -api.add_resource(UpdateCity, '/v1.4/city/update_city') -api.add_resource(DeleteCity, '/v1.4/city/delete_city') -api.add_resource(ListCities, '/v1.4/city/list_cities') -api.add_resource(SearchCity, '/v1.4/city/search_city') -api.add_resource(Greenery, '/v1.4/greenery') - +#api.add_resource(Greenery, '/v1.4/greenery') +sh.begin_time = datetime.datetime.now() @app.route("/") def home(): return Response(headers={'Access-Control-Allow-Origin': '*'}) - if __name__ == '__main__': - app.run(port=15789, host="0.0.0.0", debug=False) ->>>>>>> main + + #DBSetup(db_name='hub_test', app_env='TEST', dotenv_path=r"C:\Users\k_ls\.env") + app.run(port=15789, host="0.0.0.0", debug=True) diff --git a/hub_api/city_commands.py b/hub_api/city_commands.py deleted file mode 100644 index 2c3025e..0000000 --- a/hub_api/city_commands.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -City Commands -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2023 Project Author Koa Wells kekoa.wells@concordia.ca -""" - -import json -from flask import Response, request -from flask_restful import Resource -from hub_api import session -from hub_api.helpers.session_helper import refresh_session -from hub_api.helpers.auth import role_required -from imports.db_factory import DBFactory -from imports.geometry_factory import GeometryFactory -from hub_logger import logger -from persistence.models import UserRoles -from pathlib import Path -import os - -# Admin user commands - -class UpdateCity(Resource): - """ - UpdateCity class performs an API call to update a city that already exists inside the database - """ - - def __init__(self): - self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) - - @role_required([UserRoles.Admin.value]) - def put(self, city_id): - headers = session.headers - - allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} - try: - city_file = request.files['city_file'] - ext = city_file.filename.rsplit('.', 1)[1].lower() - - if ext in allowed_ext: - city_file_type = ext - if ext == 'gml': - city_file_type = 'citygml' - elif ext == '3dm': - city_file_type = 'rhino' - - file_path = ( - Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() - city_file.save(file_path) - city = GeometryFactory(city_file_type, file_path).city - - saved_city = self.db_factory.update_city(city_id, city) - - if os.path.exists(file_path): - os.remove(file_path) - if type(saved_city) is not dict: - return Response(response=json.dumps({ - 'id': saved_city.id, 'name': saved_city.name, 'srs_name': saved_city.srs_name, - 'time_zone': saved_city.time_zone, 'version': saved_city.city_version, - 'country': saved_city.country_code, - 'lat': saved_city.latitude, 'lon': saved_city.longitude, - 'lower_corner': saved_city.lower_corner, - 'upper_corner': saved_city.upper_corner, 'created': saved_city.created, - 'updated': saved_city.updated, - 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, - 'role': saved_city.user.role.value} - }, default=str), status=201) - return Response(response=json.dumps(saved_city), status=200) - else: - return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) - - except Exception as err: - logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400) - -class DeleteCity(Resource): - """ - DeleteCity class performs an admin API call to delete an existing city stored in the database - """ - - def __init__(self): - self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) - - @role_required([UserRoles.Admin.value]) - def delete(self): - try: - payload = request.get_json() - return Response(json.dumps(response=DBFactory.delete_city(city_id=payload["city_id"]), status=201)) - except Exception as err: - logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while deleting city'}), status=400) - - -# Standard user commands - -class ListCities(Resource): - """ - ListCities class performs a standard API call to list all existing cities stored in the database - """ - - def __init__(self): - self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) - - def get(self): - pass - -class SearchCity(Resource): - """ - SearchCity class performs a standard API call to select an existing city stored in the database - """ - - def __init__(self): - self.db_factory = DBFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) - - def get(self, city_id): - pass diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 292eb33..2249931 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -22,11 +22,13 @@ class CityInfo(Resource, Config): def __init__(self): super().__init__() - @role_required([UserRoles.Admin.value]) def get(self, city_id): + """ + API call to search database for city by city_id + :return: json, status code, headers + """ city = self.get_city(city_id) - print(city.name) - if city: + if city is not None: return Response(response=json.dumps({ 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, @@ -37,14 +39,123 @@ class CityInfo(Resource, Config): }, default=str), status=200, headers=headers) return Response(response=json.dumps({'err_msg': 'City not found'}), status=404, headers=headers) + @role_required([UserRoles.Admin.value]) + def put(self, city_id): + """ + API call to update city in database with new city_file by city_id + :return: json, status code, headers + """ + allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} + try: + city_file = request.files['city_file'] + ext = city_file.filename.rsplit('.', 1)[1].lower() -class City(Resource, Config): + if ext in allowed_ext: + city_file_type = ext + if ext == 'gml': + city_file_type = 'citygml' + elif ext == '3dm': + city_file_type = 'rhino' + file_path = ( + Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() + city_file.save(file_path) + city = GeometryFactory(city_file_type, file_path).city + + saved_city = self.import_db_factory.update_city(city_id, city) + if os.path.exists(file_path): + os.remove(file_path) + if saved_city is None: + saved_city = self.export_db_factory.get_city(city_id) + return Response(headers=headers, response=json.dumps({ + 'id': saved_city.id, 'name': saved_city.name, 'srs_name': saved_city.srs_name, + 'time_zone': saved_city.time_zone, 'version': saved_city.city_version, + 'country': saved_city.country_code, + 'lat': saved_city.latitude, 'lon': saved_city.longitude, + 'lower_corner': saved_city.lower_corner, + 'upper_corner': saved_city.upper_corner, 'created': saved_city.created, + 'updated': saved_city.updated, + 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, + 'role': saved_city.user.role.value} + }, default=str), status=201) + return Response(response=json.dumps(saved_city), status=200) + else: + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400) + + @role_required([UserRoles.Admin.value]) + def delete(self, city_id): + """ + API call to delete city from database by city_id + :return: json, status code, headers + """ + try: + return Response(headers=headers, response=json.dumps( + self.import_db_factory.delete_city(city_id=city_id)), status=201) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while deleting city'}), status=400) + +class CitySearch(Resource, Config): + """ + CitySearch class for searching for cities via an API call + """ def __init__(self): super().__init__() + def get(self, search_term, query): + """ + API call to search city depending on search_term and specified query + :return: json, status code, headers + """ + if search_term == "city_name": + city = self.get_city_by_name(query) + + if city is not None: + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404, headers=headers) + + elif search_term == "user_id": + cities = self.get_city_by_user(query) + + if cities is not None: + for city in cities: + #TODO - iterate through cities packaging them into a list and return the packaged JSON + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'No cities found by user'}), status=404, headers=headers) + + elif search_term == "list_all": + #TODO - implement API call to query all define cities in the database + pass + + + +class City(Resource, Config): + + @role_required([UserRoles.Admin.value]) def post(self): + """ + API call to create city in database with newcity_file by city_id + :return: json, status code, headers + """ allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} try: city_file = request.files['city_file'] @@ -78,4 +189,5 @@ class City(Resource, Config): return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400, headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400, headers=headers) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), + status=400, headers=headers) \ No newline at end of file diff --git a/hub_api/config.py b/hub_api/config.py index 626563e..efa2985 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -3,9 +3,9 @@ Config SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2023 Project Peter Yefi peteryefi@gmail.com """ +import os from hub.exports.db_factory import DBFactory as CityExportFactory from hub.imports.db_factory import DBFactory -import os from hub.imports.user_factory import UserFactory from hub.exports.user_factory import UserFactory as ExUserFactory @@ -19,7 +19,7 @@ class Config: db_name = 'hub_prod' app_env = 'PROD' elif os.getenv("FLASK_DEBUG") == 'testing': - db_name = 'test_db' + db_name = 'hub_test' app_env = 'TEST' self.export_db_factory = CityExportFactory(db_name=db_name, app_env=app_env, @@ -28,9 +28,16 @@ class Config: dotenv_path="{}/.env".format(os.path.expanduser('~'))) self.user_factory = UserFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) - self.ex_user_factory = ExUserFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): return self.export_db_factory.get_city(city_id) + + def get_city_by_name(self, city_name): + return self.export_db_factory.get_city_by_name(city_name) + + def get_city_by_user(self, user_id): + return self.export_db_factory.get_city_by_user(user_id) + + diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index 486a35e..56384b3 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -135,6 +135,142 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponse' + put: + tags: + - city + summary: Update a city + operationId: updateCity + description: Create a new city with a file upload + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the Id of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to update + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + city_file: + type: string + format: binary + required: true + responses: + '201': + description: City updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/City' + '200': + description: City not updated + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '404': + description: City not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] + delete: + tags: + - city + summary: Delete a city + operationId: deleteCity + description: Delete city with specified city_id + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the ID of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to delete + responses: + '201': + description: City deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/City' + '200': + description: City not deleted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '404': + description: City not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' security: - BearerAuth: [ ] /v1.4/user: @@ -382,6 +518,11 @@ components: format: int32 message: type: string + Uptime: + type: object + properties: + uptime: + type: string requestBodies: User: description: User object that is to be created diff --git a/hub_api/uptime.py b/hub_api/uptime.py index 746012f..ce36d40 100644 --- a/hub_api/uptime.py +++ b/hub_api/uptime.py @@ -11,6 +11,7 @@ from flask_restful import Resource import hub_api.helpers.session_helper as sh +headers = {'Content-Type': 'application/json'} class Uptime(Resource): def __init__(self): @@ -19,4 +20,4 @@ class Uptime(Resource): @staticmethod def get(): uptime = {"uptime": f"{datetime.datetime.now() - sh.begin_time}"} - return Response(json.dumps(uptime)) + return Response(response=json.dumps(uptime), headers=headers) diff --git a/requirements.txt b/requirements.txt index fa21cdb..4a541e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ pyecore==0.12.2 jwt==1.3.1 flagger==3.1.0 flasgger +psycopg2 \ No newline at end of file