Skip to content

Commit

Permalink
Merge pull request #5 from dinesh-aot/EPICSYSTEM-178-swagger_integration
Browse files Browse the repository at this point in the history
EPICSYSTEM-178-Swagger integrated
  • Loading branch information
jadmsaadaot authored Jul 16, 2024
2 parents c7676ef + 5a6ec37 commit e526850
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 65 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Submit API",
"type": "python",
"cwd": "${workspaceFolder}/submit-api/",
"request": "launch",
"program": "${workspaceFolder}/submit-api/wsgi.py",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
2 changes: 1 addition & 1 deletion submit-api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pylint: ## Linting with pylint
. venv/bin/activate && pylint --rcfile=setup.cfg src/$(PROJECT_NAME)

flake8: ## Linting with flake8
. venv/bin/activate && flake8 src/$(PROJECT_NAME) tests
. venv/bin/activate && flake8 --extend-ignore=Q000,D400,D401,I005,W503 src/$(PROJECT_NAME) tests

lintfix: ## Linting fix
. venv/bin/activate && autopep8 -i -a src/**/*.py tests/**/*.py
Expand Down
1 change: 1 addition & 0 deletions submit-api/requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pytest-mock
requests
flake8==4.0.1
flake8-blind-except
Flask-JWT-Extended=4.6.0
flake8-debugger
flake8-docstrings
flake8-isort
Expand Down
12 changes: 11 additions & 1 deletion submit-api/src/submit_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import os
from http import HTTPStatus

import secure
from flask import Flask, current_app, g, request
Expand Down Expand Up @@ -49,7 +50,7 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'development')):
# All configuration are in config file
app.config.from_object(get_named_config(run_mode))

CORS(app, origins=allowedorigins(), supports_credentials=True)
CORS(app, resources={r"/*": {"origins": allowedorigins()}}, supports_credentials=True)

# Register blueprints
app.register_blueprint(API_BLUEPRINT)
Expand Down Expand Up @@ -82,6 +83,14 @@ def set_secure_headers(response):
response.headers['Cross-Origin-Embedder-Policy'] = 'unsafe-none'
return response

@app.errorhandler(Exception)
def handle_error(err):
if run_mode != "production":
# To get stacktrace in local development for internal server errors
raise err
current_app.logger.error(str(err))
return "Internal server error", HTTPStatus.INTERNAL_SERVER_ERROR

# Return App for run in run.py file
return app

Expand All @@ -90,6 +99,7 @@ def build_cache(app):
"""Build cache."""
cache.init_app(app)


def setup_jwt_manager(app_context, jwt_manager):
"""Use flask app to configure the JWTManager to work for a particular Realm."""

Expand Down
70 changes: 69 additions & 1 deletion submit-api/src/submit_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import sys

from dotenv import find_dotenv, load_dotenv
from flask import g

# this will load all the envars from a .env file located in the project root (api)
load_dotenv(find_dotenv())
Expand Down Expand Up @@ -103,6 +102,75 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods
DEBUG = True
TESTING = True

# POSTGRESQL
DB_USER = os.getenv('DATABASE_TEST_USERNAME', 'postgres')
DB_PASSWORD = os.getenv('DATABASE_TEST_PASSWORD', 'postgres')
DB_NAME = os.getenv('DATABASE_TEST_NAME', 'testdb')
DB_HOST = os.getenv('DATABASE_TEST_HOST', 'localhost')
DB_PORT = os.getenv('DATABASE_TEST_PORT', '5432')
SQLALCHEMY_DATABASE_URI = f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}'

JWT_OIDC_TEST_MODE = True
# JWT_OIDC_ISSUER = _get_config('JWT_OIDC_TEST_ISSUER')
JWT_OIDC_TEST_AUDIENCE = os.getenv('JWT_OIDC_TEST_AUDIENCE')
JWT_OIDC_TEST_CLIENT_SECRET = os.getenv('JWT_OIDC_TEST_CLIENT_SECRET')
JWT_OIDC_TEST_ISSUER = os.getenv('JWT_OIDC_TEST_ISSUER')
JWT_OIDC_WELL_KNOWN_CONFIG = os.getenv('JWT_OIDC_WELL_KNOWN_CONFIG')
JWT_OIDC_TEST_ALGORITHMS = os.getenv('JWT_OIDC_TEST_ALGORITHMS')
JWT_OIDC_TEST_JWKS_URI = os.getenv('JWT_OIDC_TEST_JWKS_URI', default=None)
JWT_OIDC_TEST_MODE = True

JWT_OIDC_TEST_KEYS = {
'keys': [
{
'kid': 'epictrack',
'kty': 'RSA',
'alg': 'RS256',
'use': 'sig',
'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-'
'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR',
'e': 'AQAB'
}
]
}

JWT_OIDC_TEST_PRIVATE_KEY_JWKS = {
'keys': [
{
'kid': 'forms-flow-ai',
'kty': 'RSA',
'alg': 'RS256',
'use': 'sig',
'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-'
'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR',
'e': 'AQAB',
'd': 'C0G3QGI6OQ6tvbCNYGCqq043YI_8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhskURaDwk4-'
'8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh_'
'xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0',
'p': 'APXcusFMQNHjh6KVD_hOUIw87lvK13WkDEeeuqAydai9Ig9JKEAAfV94W6Aftka7tGgE7ulg1vo3eJoLWJ1zvKM',
'q': 'AOjX3OnPJnk0ZFUQBwhduCweRi37I6DAdLTnhDvcPTrrNWuKPg9uGwHjzFCJgKd8KBaDQ0X1rZTZLTqi3peT43s',
'dp': 'AN9kBoA5o6_Rl9zeqdsIdWFmv4DB5lEqlEnC7HlAP-3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhc',
'dq': 'ANtbSY6njfpPploQsF9sU26U0s7MsuLljM1E8uml8bVJE1mNsiu9MgpUvg39jEu9BtM2tDD7Y51AAIEmIQex1nM',
'qi': 'XLE5O360x-MhsdFXx8Vwz4304-MJg-oGSJXCK_ZWYOB_FGXFRTfebxCsSYi0YwJo-oNu96bvZCuMplzRI1liZw'
}
]
}

JWT_OIDC_TEST_PRIVATE_KEY_PEM = """-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDfn1nKQshOSj8xw44oC2klFWSNLmK3BnHONCJ1bZfq0EQ5gIfg
tlvB+Px8Ya+VS3OnK7Cdi4iU1fxO9ktN6c6TjmmmFevk8wIwqLthmCSF3r+3+h4e
ddj7hucMsXWv05QUrCPoL6YUUz7Cgpz7ra24rpAmK5z7lsV+f3BEvXkrUQIDAQAB
AoGAC0G3QGI6OQ6tvbCNYGCqq043YI/8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhs
kURaDwk4+8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh/
xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0CQQD13LrBTEDR44ei
lQ/4TlCMPO5bytd1pAxHnrqgMnWovSIPSShAAH1feFugH7ZGu7RoBO7pYNb6N3ia
C1idc7yjAkEA6Nfc6c8meTRkVRAHCF24LB5GLfsjoMB0tOeEO9w9Ous1a4o+D24b
AePMUImAp3woFoNDRfWtlNktOqLel5PjewJBAN9kBoA5o6/Rl9zeqdsIdWFmv4DB
5lEqlEnC7HlAP+3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhcCQQDb
W0mOp436T6ZaELBfbFNulNLOzLLi5YzNRPLppfG1SRNZjbIrvTIKVL4N/YxLvQbT
NrQw+2OdQACBJiEHsdZzAkBcsTk7frTH4yGx0VfHxXDPjfTj4wmD6gZIlcIr9lZg
4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn
-----END RSA PRIVATE KEY-----"""

class DockerConfig(_Config): # pylint: disable=too-few-public-methods
"""In support of testing only.used by the py.test suite."""
Expand Down
4 changes: 2 additions & 2 deletions submit-api/src/submit_api/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ class BaseModel(db.Model):
updated_date = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=True)

@declared_attr
def created_by(cls): # pylint:disable=no-self-argument, # noqa: N805
def created_by(cls): # pylint:disable=no-self-argument, no-self-use, # noqa: N805
"""Return foreign key for created by."""
return Column(db.String(50))

@declared_attr
def updated_by(cls): # pylint:disable=no-self-argument, # noqa: N805
def updated_by(cls): # pylint:disable=no-self-argument, no-self-use, # noqa: N805
"""Return foreign key for modified by."""
return Column(db.String(50))

Expand Down
57 changes: 53 additions & 4 deletions submit-api/src/submit_api/resources/apihelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
to support swagger on http
"""
from functools import wraps
from flask import url_for
from flask_restx import Api as BaseApi
from flask_restx import Api as BaseApi, fields
from flask_restx.apidoc import apidoc
from marshmallow import fields as ma_fields


class Api(BaseApi):
Expand All @@ -26,10 +28,57 @@ class Api(BaseApi):
@property
def specs_url(self):
"""Return URL for endpoint."""
scheme = 'http' if '5000' in self.base_url else 'https'
return url_for(self.endpoint('specs'), _external=True, _scheme=scheme)
scheme = "http" if "3200" in self.base_url else "https"
return url_for(self.endpoint("specs"), _external=True, _scheme=scheme)

@classmethod
def swagger_decorators(cls, api, endpoint_description):
"""Common decorators for the resources"""

def decorator(func):
@wraps(func)
@api.doc(description=endpoint_description)
@api.response(401, "Unauthorized")
@api.response(500, "Internal Server Error")
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

return wrapper

return decorator

@classmethod
def convert_ma_schema_to_restx_model(cls, api, schema, name):
"""
Converts a Marshmallow schema to a Flask-RESTX model.
:param api: The Flask-RESTX API instance
:param schema: The Marshmallow schema instance
:param name: The name of the Flask-RESTX model
:return: A Flask-RESTX model
"""
type_mapping = {
ma_fields.Integer: fields.Integer,
ma_fields.String: fields.String,
ma_fields.Float: fields.Float,
ma_fields.Boolean: fields.Boolean,
ma_fields.DateTime: fields.DateTime,
# Add more field types as needed
}
model_fields = {}
for field_name, field in schema.fields.items():
field_type = type(field)
restx_field = type_mapping.get(field_type)
if restx_field:
model_fields[field_name] = restx_field(
required=field.required,
description=field.metadata.get("description", ""),
)
# Add more field types as needed

return api.model(name, model_fields)


# Make a global change setting the URL prefix for the swaggerui at the module level
# This solves the issue where the swaggerui does not pick up the url prefix
apidoc.url_prefix = '/api/'
apidoc.url_prefix = "/api/"
2 changes: 1 addition & 1 deletion submit-api/src/submit_api/resources/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from flask_restx import Namespace, Resource
from sqlalchemy import exc, text

from api.models import db
from submit_api.models import db


API = Namespace('OPS', description='Service - OPS checks')
Expand Down
82 changes: 51 additions & 31 deletions submit-api/src/submit_api/resources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,89 @@

from http import HTTPStatus

from flask import jsonify, request

from submit_api.auth import auth
from submit_api.services.user_service import UserService
from flask_cors import cross_origin
from flask_restx import Namespace, Resource
from submit_api.services.user_service import UserService
from submit_api.utils.util import cors_preflight
from submit_api.schemas.user import UserSchema, UserRequestSchema
from submit_api.exceptions import ResourceNotFoundError
from .apihelper import Api as ApiHelper

from submit_api.utils.util import allowedorigins, cors_preflight

API = Namespace('user', description='Endpoints for User Management')
API = Namespace("users", description="Endpoints for User Management")
"""Custom exception messages
"""

user_request_model = ApiHelper.convert_ma_schema_to_restx_model(
API, UserRequestSchema(), "User"
)
user_list_model = ApiHelper.convert_ma_schema_to_restx_model(
API, UserSchema(), "UserListItem"
)


@cors_preflight('GET, OPTIONS, POST')
@API.route('', methods=["POST", "GET", "OPTIONS"])
@cors_preflight("GET, OPTIONS, POST")
@API.route("", methods=["POST", "GET", "OPTIONS"])
class Users(Resource):
"""Resource for managing users."""

@staticmethod
@cross_origin(origins=allowedorigins())
@auth.require
@API.response(code=200, description="Success", model=[user_list_model])
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch all users")
def get():
"""Fetch all users."""
users = UserService.get_all_users()
return jsonify(users), HTTPStatus.OK
user_list_schema = UserSchema(many=True)
return user_list_schema.dump(users), HTTPStatus.OK

@staticmethod
@cross_origin(origins=allowedorigins())
@auth.require
@ApiHelper.swagger_decorators(API, endpoint_description="Create a user")
@API.expect(user_request_model)
@API.response(code=201, model=user_request_model, description="UserCreated")
@API.response(400, "Bad Request")
def post():
"""Create a user."""
user_data = request.get_json()
user_data = UserRequestSchema().load(API.payload)
created_user = UserService.create_user(user_data)
return created_user, HTTPStatus.CREATED
return UserSchema().dump(created_user), HTTPStatus.CREATED


@cors_preflight('GET, OPTIONS, PUT, DELETE')
@API.route('/<user_id>', methods=["PUT", "GET", "OPTIONS", "DELETE"])
@cors_preflight("GET, OPTIONS, PATCH, DELETE")
@API.route("/<user_id>", methods=["PATCH", "GET", "OPTIONS", "DELETE"])
@API.doc(params={"user_id": "The user identifier"})
class User(Resource):
"""Resource for managing a single user"""

@staticmethod
@cross_origin(origins=allowedorigins())
@auth.require
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch a user by id")
@API.response(code=200, model=user_list_model, description="Success")
@API.response(404, "Not Found")
def get(user_id):
"""Fetch a user by id."""
user = UserService.get_user_by_id(user_id)
return user, HTTPStatus.OK
if not user:
raise ResourceNotFoundError(f"User with {user_id} not found")
return UserSchema().dump(user), HTTPStatus.OK

@staticmethod
@cross_origin(origins=allowedorigins())
@auth.require
def put(user_id):
@ApiHelper.swagger_decorators(API, endpoint_description="Update a user by id")
@API.expect(user_request_model)
@API.response(code=200, model=user_list_model, description="Success")
@API.response(400, "Bad Request")
@API.response(404, "Not Found")
def patch(user_id):
"""Update a user by id."""
user_data = request.get_json()
user_data = UserRequestSchema().load(API.payload)
updated_user = UserService.update_user(user_id, user_data)
return updated_user, HTTPStatus.OK
if not updated_user:
raise ResourceNotFoundError(f"User with {user_id} not found")
return UserSchema().dump(updated_user), HTTPStatus.OK

@staticmethod
@cross_origin(origins=allowedorigins())
@auth.require
@ApiHelper.swagger_decorators(API, endpoint_description="Delete a user by id")
@API.response(code=200, model=user_list_model, description="Deleted")
@API.response(404, "Not Found")
def delete(user_id):
"""Delete a user by id."""
deleted_user_id = UserService.delete_user(user_id)
return deleted_user_id, HTTPStatus.OK
deleted_user = UserService.delete_user(user_id)
if not deleted_user:
raise ResourceNotFoundError(f"User with {user_id} not found")
return UserSchema().dump(deleted_user), HTTPStatus.OK
Loading

0 comments on commit e526850

Please sign in to comment.