Merge remote-tracking branch 'origin/city_commands' into api_guille

# Conflicts:
#	bootstrap.py
#	hub_api/city_commands.py
#	hub_api/config.py
#	hub_api/construction_catalog.py
#	hub_api/energy_demand.py
#	hub_api/greenery_catalog.py
#	hub_api/uptime.py
#	hub_api/user.py
#	requirements.txt
This commit is contained in:
Guille Gutierrez 2023-02-13 07:15:10 -05:00
commit 1adfe6519f
15 changed files with 457 additions and 1125 deletions

View File

@ -1,86 +0,0 @@
"""
HeatPump Service
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.persistence.models import UserRoles
from hub_api.helpers.auth import role_required
from hub_api.helpers.session_helper import refresh_session
# Admin user commands
"""
Save class
"""
class Save(Resource):
def __init__(self):
pass
@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
"""
Update class
"""
class Update(Resource):
def __init__(self):
print()
@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
"""
Delete class
"""
class Delete(Resource):
def __init__(self):
pass
@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
# Standard user commands
"""
ListCities class
"""
class List(Resource):
def __init__(self):
pass
@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 Search(Resource):
def __init__(self):
pass
def get(self):
session = refresh_session(request)
if session is None:
return Response(json.dumps({'error': 'invalid session'}), status=401)
headers = session.headers

View File

@ -15,71 +15,147 @@ from pathlib import Path
import os import os
from hub_api.config import Config from hub_api.config import Config
headers = {'Content-Type': 'application/json'}
class CityInfo(Resource, Config): class CityInfo(Resource, Config):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@role_required([UserRoles.Admin.value])
def get(self, city_id): 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) city = self.get_city(city_id)
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)
# TODO: this is only for dompark project and need to be removed in future versions. @role_required([UserRoles.Admin.value])
floor_area = 0 def put(self, city_id):
wall_construction = 'unknown' """
floor_construction = 'unknown' API call to update city in database with new city_file by city_id
roof_construction = 'unknown' :return: json, status code, headers
window_type = 'unknown' """
building_dic = {} allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'}
for building in city.buildings: try:
city_file = request.files['city_file']
ext = city_file.filename.rsplit('.', 1)[1].lower()
usages = [] # This is only valid for dompark project as all the building have the same usage if ext in allowed_ext:
if building.lower_corner[2] == 0: city_file_type = ext
floor_area += building.floor_area if ext == 'gml':
for internal_zone in building.internal_zones: city_file_type = 'citygml'
for usage_zone in internal_zone.usage_zones: elif ext == '3dm':
usages.append({'percentage': usage_zone.percentage, 'usage': usage_zone.usage}) city_file_type = 'rhino'
for thermal_zone in internal_zone.thermal_zones:
for thermal_boundary in thermal_zone.thermal_boundaries: file_path = (
if thermal_boundary.parent_surface.type == 'Ground': Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve()
floor_construction = thermal_boundary.construction_name city_file.save(file_path)
elif thermal_boundary.parent_surface.type == 'Wall': city = GeometryFactory(city_file_type, file_path).city
wall_construction = thermal_boundary.construction_name
for thermal_opening in thermal_boundary.thermal_openings: saved_city = self.import_db_factory.update_city(city_id, city)
if thermal_opening.construction_name is not None: if os.path.exists(file_path):
window_type = thermal_opening.construction_name os.remove(file_path)
break 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: else:
roof_construction = thermal_boundary.construction_name return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400)
name = building.human_readable_name
year_of_construction = str(building.year_of_construction) except Exception as err:
building_dic = { logger.error(err)
'name': str(name), return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400)
'floor_area': str(floor_area),
'year_of_construction': str(year_of_construction), @role_required([UserRoles.Admin.value])
'usages': usages, def delete(self, city_id):
'wall_construction': wall_construction, """
'floor_construction': floor_construction, API call to delete city from database by city_id
'roof_construction': roof_construction, :return: json, status code, headers
'window_type': window_type, """
'default_archetype': 'industry ASHRAE_2004:4A non_standard_dompark' 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
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)
class City(Resource, Config): class City(Resource, Config):
def __init__(self):
super().__init__()
@role_required([UserRoles.Admin.value]) @role_required([UserRoles.Admin.value])
def post(self): 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'} allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'}
try: try:
city_file = request.files['city_file'] city_file = request.files['city_file']
@ -108,9 +184,10 @@ class City(Resource, Config):
'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email,
'role': saved_city.user.role.value} 'role': saved_city.user.role.value}
}, default=str), status=201) }, 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: 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: except Exception as err:
logger.error(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)

View File

@ -3,28 +3,41 @@ Config
SPDX - License - Identifier: LGPL - 3.0 - or -later SPDX - License - Identifier: LGPL - 3.0 - or -later
Copyright © 2023 Project Peter Yefi peteryefi@gmail.com Copyright © 2023 Project Peter Yefi peteryefi@gmail.com
""" """
import os
from hub.exports.db_factory import DBFactory as CityExportFactory from hub.exports.db_factory import DBFactory as CityExportFactory
from hub.imports.db_factory import DBFactory from hub.imports.db_factory import DBFactory
import os from hub.imports.user_factory import UserFactory
import pickle from hub.exports.user_factory import UserFactory as ExUserFactory
class Config: class Config:
def __init__(self): 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_DEBUG") == 'production':
db_name = 'hub_prod'
app_env = 'PROD'
elif os.getenv("FLASK_DEBUG") == 'testing':
db_name = 'hub_test'
app_env = 'TEST'
self.export_db_factory = CityExportFactory(db_name=db_name, app_env=app_env,
dotenv_path="{}/.env".format(os.path.expanduser('~'))) 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('~'))) dotenv_path="{}/.env".format(os.path.expanduser('~')))
def get_city(self, city_id): def get_city(self, city_id):
city_obj = self.export_db_factory.get_city(city_id) return self.export_db_factory.get_city(city_id)
city = pickle.loads(city_obj.city)
for building in city.buildings: def get_city_by_name(self, city_name):
building.heated = True return self.export_db_factory.get_city_by_name(city_name)
building.cooled = True
building.attic_heated = 0 def get_city_by_user(self, user_id):
building.basement_heated = 0 return self.export_db_factory.get_city_by_user(user_id)
for surface in building.surfaces:
surface.swr = 0.2
return city

View File

@ -3,7 +3,7 @@ info:
description: NextGen Cities Institute API description: NextGen Cities Institute API
termsOfService: http://swagger.io/terms/ termsOfService: http://swagger.io/terms/
contact: contact:
email: peteryefi@gmail.com email: nextgen-cities@gmail.com
version: 1.4 version: 1.4
externalDocs: externalDocs:
description: Find out more about Swagger description: Find out more about Swagger
@ -100,13 +100,69 @@ paths:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
security: security:
- BearerAuth: [] - BearerAuth: []
/v1.4/heat-pump/{city_id}: /v1.4/city/{city_id}:
post: get:
tags: tags:
- heatpump - city
summary: Create a heat pump simulation summary: Get a city
operationId: createHeatpump operationId: getCity
description: heatpump simulation with existing catalog data description: Retrieve a city with a given 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 get
responses:
'200':
description: City retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/City'
'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'
put:
tags:
- city
summary: Update a city
operationId: updateCity
description: Create a new city with a file upload
parameters: parameters:
- in: header - in: header
name: appId name: appId
@ -119,21 +175,30 @@ paths:
schema: schema:
type: integer type: integer
required: true required: true
description: Numeric ID of the city to get description: Numeric ID of the city to update
requestBody: requestBody:
content: content:
application/json: multipart/form-data:
schema: schema:
type: object type: object
$ref: '#/components/schemas/HeatPump' properties:
city_file:
type: string
format: binary
required: true required: true
responses: responses:
'201': '201':
description: Heatpump simulation created successfully description: City updated successfully
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/HeatPumpRes' $ref: '#/components/schemas/City'
'200':
description: City not updated
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'400': '400':
description: Bad Request description: Bad Request
content: content:
@ -141,7 +206,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
'404': '404':
description: Not found description: City not found
content: content:
application/json: application/json:
schema: schema:
@ -166,13 +231,12 @@ paths:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
security: security:
- BearerAuth: [ ] - BearerAuth: [ ]
/v1.4/city/{city_id}: delete:
get:
tags: tags:
- city - city
summary: Get a city summary: Delete a city
operationId: getCity operationId: deleteCity
description: Retrieve a city with a given city ID description: Delete city with specified city_id
parameters: parameters:
- in: header - in: header
name: appId name: appId
@ -185,14 +249,20 @@ paths:
schema: schema:
type: integer type: integer
required: true required: true
description: Numeric ID of the city to get description: Numeric ID of the city to delete
responses: responses:
'200': '201':
description: City created successfully description: City deleted successfully
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/City' $ref: '#/components/schemas/City'
'200':
description: City not deleted
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'400': '400':
description: Bad Request description: Bad Request
content: content:
@ -364,233 +434,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $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: components:
schemas: schemas:
Uptime: Uptime:
@ -648,211 +491,6 @@ components:
updated: updated:
type: string type: string
example: 2023-01-15 18:40:54.64877 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: User:
type: object type: object
properties: properties:
@ -880,48 +518,6 @@ components:
- email - email
- password - password
- role - 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: Login:
type: object type: object
properties: properties:
@ -951,9 +547,14 @@ components:
format: int32 format: int32
message: message:
type: string type: string
Uptime:
type: object
properties:
uptime:
type: string
requestBodies: requestBodies:
User: User:
description: Pet object that needs to be added to the store description: User object that is to be created
content: content:
application/json: application/json:
schema: schema:

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -12,6 +12,8 @@ from flask_restful import Resource
import hub_api.helpers.session_helper as sh import hub_api.helpers.session_helper as sh
headers = {'Content-Type': 'application/json'}
class Uptime(Resource): class Uptime(Resource):
def __init__(self): def __init__(self):
pass pass
@ -19,4 +21,4 @@ class Uptime(Resource):
@staticmethod @staticmethod
def get(): def get():
uptime = {"uptime": f"{datetime.datetime.now() - sh.begin_time}"} uptime = {"uptime": f"{datetime.datetime.now() - sh.begin_time}"}
return Response(json.dumps(uptime)) return Response(response=json.dumps(uptime), headers=headers)

View File

@ -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))

View File

@ -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)

View File

@ -6,18 +6,17 @@ Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com
import json import json
from flask import Response, request from flask import Response, request
from flask_restful import Resource 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.hub_logger import logger
from hub_api.helpers.auth import generate_auth_token, role_required from hub_api.helpers.auth import generate_auth_token, role_required
from hub.persistence.models import UserRoles 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): def __init__(self):
self.user_factory = UserFactory(db_name='hub_prod', app_env='PROD', super().__init__()
dotenv_path="{}/.env".format(os.path.expanduser('~')))
@role_required([UserRoles.Admin.value]) @role_required([UserRoles.Admin.value])
def post(self): 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"], user = self.user_factory.create_user(name=payload["name"], email=payload["email"], password=payload["password"],
role=payload["role"]) role=payload["role"])
if type(user) is dict: 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, 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: except Exception as err:
logger.error(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]) @role_required([UserRoles.Admin.value])
def put(self): 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'], res = self.user_factory.update_user(user_id=payload['id'], name=payload['name'], password=payload['password'],
role=payload['role'], email=payload['email']) role=payload['role'], email=payload['email'])
if res: if res:
return Response(response=json.dumps(res), status=400) return Response(response=json.dumps(res), status=400, headers=headers)
return Response(response=json.dumps({'success': 'user updated successfully'}), status=200) return Response(response=json.dumps({'success': 'user updated successfully'}), status=200, headers=headers)
except Exception as err: except Exception as err:
logger.error(err) logger.error(err)
return Response(response=json.dumps({'err_msg': 'Sorry, an error occurred while updating user'}), 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): def __init__(self):
self.user_factory = ExUserFactory(db_name='hub_prod', app_env='PROD', super().__init__()
dotenv_path="{}/.env".format(os.path.expanduser('~')))
def post(self): def post(self):
try: try:
payload = request.get_json() 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: 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 = user[0]
user_dict = { user_dict = {
'user': { 'user': {
@ -70,7 +70,7 @@ class UserLogin(Resource):
} }
} }
user_dict['token'] = generate_auth_token(user_dict) 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: except Exception as err:
logger.error(err) logger.error(err)
return Response(response=json.dumps({'err_msg': 'An error occurred while authenticating user'}), status=400) return Response(response=json.dumps({'err_msg': 'An error occurred while authenticating user'}), status=400)

View File

@ -25,3 +25,4 @@ jwt==1.3.1
flagger==3.1.0 flagger==3.1.0
flasgger flasgger
cerc-hub cerc-hub
psycopg2

0
tests/__init__.py Normal file
View File

58
tests/base_test.py Normal file
View File

@ -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_DEBUG'] = '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

124
tests/test_user.py Normal file
View File

@ -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())