Skip to content

Commit

Permalink
major change: batteries included
Browse files Browse the repository at this point in the history
  • Loading branch information
algonacci committed Feb 17, 2024
1 parent e10009b commit e52d134
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest test.py
pytest test_api.py
69 changes: 11 additions & 58 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import os
from flask import Flask, jsonify, request

from flask import Flask
from flask_cors import CORS
from flask_smorest import Api, Blueprint
from flask_smorest import Api

import module as md
from auth import auth
from cache import init_cache_app
from rate_limiter import init_rate_limiter
from config import CONFIG
from errors import bp as errors_bp
from index import bp as index_bp


def create_app():
Expand All @@ -16,62 +19,12 @@ def create_app():
r"/*": {"origins": ["http://localhost:3000", "https://example.com"]}
})
api = Api(app)
blp = Blueprint("ML Endpoints",
"items",
description="Operations on ML model endpoint")

@blp.route("/")
def index():
return jsonify({
"status": {
"code": 200,
"message": "Success fetching the API!"
}
}), 200

@blp.route("/post", methods=["POST"])
@auth.login_required()
def post():
if request.method == "POST":
input_data = request.get_json()
return jsonify(input_data), 200
else:
return jsonify({"message": "Invalid request method"}), 405

@app.errorhandler(400)
def bad_request(error):
return jsonify({
"status": {
"code": 400,
"message": "Client side error!"
}
}), 400

@app.errorhandler(404)
def not_found(error):
return jsonify({
"status": {
"code": 404,
"message": "URL not found!"
}
}), 404

@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({
"status": {
"code": 405,
"message": "Request method not allowed!"
}
}), 405

@app.errorhandler(500)
def internal_server_error(error):
return jsonify({
"status": {"code": 500, "message": "Server error!"}
}), 500
api.register_blueprint(index_bp)
api.register_blueprint(errors_bp)

api.register_blueprint(blp)
init_cache_app(app)
init_rate_limiter(app)

return app

Expand Down
10 changes: 10 additions & 0 deletions cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flask_caching import Cache

cache = Cache()


def init_cache_app(app):
cache.init_app(app, config={
'CACHE_TYPE': app.config.get('CACHE_TYPE', 'SimpleCache'),
'CACHE_DEFAULT_TIMEOUT': app.config.get('CACHE_DEFAULT_TIMEOUT', 300),
})
4 changes: 3 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@
}
}
}
}
},
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300
}
59 changes: 59 additions & 0 deletions errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from flask_smorest import Blueprint
from flask import jsonify

bp = Blueprint("errors", __name__)


@bp.app_errorhandler(400)
def bad_request(error):
return jsonify({
"status": {
"code": 400,
"message": "Client side error!"
},
"data": None,
}), 400


@bp.app_errorhandler(404)
def not_found(error):
return jsonify({
"status": {
"code": 404,
"message": "URL not found!"
},
"data": None,
}), 404


@bp.app_errorhandler(405)
def method_not_allowed(error):
return jsonify({
"status": {
"code": 405,
"message": "Request method not allowed!"
},
"data": None,
}), 405


@bp.app_errorhandler(429)
def rate_limit_exceeded(error):
return jsonify({
"status": {
"code": 429,
"message": "Rate limit exceeded. Please try again later."
},
"data": None
}), 429


@bp.app_errorhandler(500)
def internal_server_error(error):
return jsonify({
"status": {
"code": 500,
"message": "Server error!"
},
"data": None,
}), 500
File renamed without changes.
39 changes: 39 additions & 0 deletions index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from flask_smorest import Blueprint
from flask import jsonify, request
from auth import auth
from cache import cache
from rate_limiter import limiter


bp = Blueprint("index",
"items",
description="Operations on ML model endpoint")


@bp.route("/")
@limiter.limit("10 per day")
@cache.cached(timeout=60)
def index():
return jsonify({
"status": {
"code": 200,
"message": "Success fetching the API!"
},
"data": None
}), 200


@bp.route("/post", methods=["POST"])
@auth.login_required()
def post():
if request.method == "POST":
input_data = request.get_json()
return jsonify(input_data), 200
else:
return jsonify({
"status": {
"code": 405,
"message": "Invalid request method",
},
"data": None,
}), 405
13 changes: 13 additions & 0 deletions rate_limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from flask import jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri="memory://",
)


def init_rate_limiter(app):
limiter.init_app(app)
46 changes: 28 additions & 18 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
apispec==6.3.0
click==8.1.3
colorama==0.4.6
Flask==2.2.3
Flask-Cors==3.0.10
Flask-HTTPAuth==4.7.0
flask-smorest==0.41.0
gunicorn==20.1.0
importlib-metadata==6.5.0
apispec==6.4.0
blinker==1.7.0
cachelib==0.9.0
click==8.1.7
Deprecated==1.2.14
Flask==3.0.2
Flask-Caching==2.1.0
Flask-Limiter==3.5.1
flask-smorest==0.43.0
gunicorn==21.2.0
importlib-metadata==7.0.1
importlib-resources==6.1.1
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
marshmallow==3.19.0
packaging==23.1
python-dotenv==1.0.0
six==1.16.0
webargs==8.2.0
Werkzeug==2.2.3
zipp==3.15.0
Jinja2==3.1.3
limits==3.8.0
markdown-it-py==3.0.0
MarkupSafe==2.1.5
marshmallow==3.20.2
mdurl==0.1.2
ordered-set==4.1.0
packaging==23.2
Pygments==2.17.2
PyJWT==2.8.0
rich==13.7.0
typing_extensions==4.9.0
webargs==8.4.0
Werkzeug==3.0.1
wrapt==1.16.0
zipp==3.17.0
3 changes: 3 additions & 0 deletions test.py → test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def test_index_route(app):
assert type(res) is dict
assert res["status"]["code"] == 200
assert res["status"]["message"] == "Success fetching the API!"
assert res["data"] == None


def test_post_route_without_authorization(app):
Expand Down Expand Up @@ -48,6 +49,7 @@ def test_not_found_route(app):
assert type(res) is dict
assert res["status"]["code"] == 404
assert res["status"]["message"] == "URL not found!"
assert res["data"] == None


def test_method_not_allowed_route(app):
Expand All @@ -57,3 +59,4 @@ def test_method_not_allowed_route(app):
assert type(res) is dict
assert res["status"]["code"] == 405
assert res["status"]["message"] == "Request method not allowed!"
assert res["data"] == None
69 changes: 69 additions & 0 deletions wrappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from flask import jsonify, request
from functools import wraps
import jwt
from config import CONFIG

admin = False
SECRET_KEY = CONFIG['SECRET_KEY']


def admin_require(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if admin:
return f(*args, **kwargs)
else:
return jsonify({
"status": {
"code": 403,
"message": "Forbidden",
},
"data": None
}), 403

return decorated_function


def token_required(f):
@wraps(f)
def decorator(*args, **kwargs):
token = request.headers.get('Authorization', None)
if not token:
return jsonify({
"status": {
"code": 401,
"message": "Invalid token",
},
"data": None
}), 401
try:
token_prefix, token_value = token.split()
if token_prefix.lower() != 'bearer':
raise ValueError('Invalid token prefix')
data = jwt.decode(token_value, SECRET_KEY, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return jsonify({
"status": {
"code": 401,
"message": "Token has expired",
},
"data": None
}), 401
except jwt.InvalidTokenError:
return jsonify({
"status": {
"code": 401,
"message": "Invalid token"
},
"data": None,
}), 401
except ValueError:
return jsonify({
"status": {
"code": 401,
"message": "Invalid token format",
},
"data": None
}), 401
return f(data, *args, **kwargs)
return decorator

0 comments on commit e52d134

Please sign in to comment.