From 347345b34390dc322a931dd168ac6c420a1a909d Mon Sep 17 00:00:00 2001 From: Koa Date: Thu, 9 Feb 2023 09:45:25 -0500 Subject: [PATCH] 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