Skip to content

Commit 7727004

Browse files
committed
Initial commit
0 parents  commit 7727004

16 files changed

+289
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
*.log
3+
.idea
4+
venv
5+
tmp
6+
__pycache__

.pylintrc

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
[MASTER]
2+
# Specify a score threshold to be exceeded before program exits with error.
3+
fail-under=10.0
4+
5+
# Disable the message, report, category or checker with the given id(s). You
6+
# can either give multiple identifiers separated by comma (,) or put this
7+
# option multiple times (only on the command line, not in the configuration
8+
# file where it should appear only once). You can also use "--disable=all" to
9+
# disable everything first and then reenable specific checks. For example, if
10+
# you want to run only the similarities checker, you can use "--disable=all
11+
# --enable=similarities". If you want to run only the classes checker, but have
12+
# no Warning level messages displayed, use "--disable=all --enable=classes
13+
# --disable=W".
14+
# disable=too-few-public-methods
15+
16+
# Files or directories to be skipped. They should be base names, not paths.
17+
# ignore=schema_common.graphql, schema_api.graphql, schema_admin.graphql
18+
19+
# List of plugins (as comma separated values of python module names) to load,
20+
# usually to register additional checkers.
21+
# load-plugins=
22+
23+
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
24+
# number of processors available to use.
25+
jobs=0
26+
27+
# When enabled, pylint would attempt to guess common misconfiguration and emit
28+
# user-friendly hints instead of false-positive error messages.
29+
suggestion-mode=yes
30+
31+
[MESSAGES CONTROL]
32+
disable=missing-docstring
33+
34+
[FORMAT]
35+
max-line-length=120
36+
37+
[tool.pylint.SIMILARITIES]
38+
min-similarity-lines=5
39+
40+
# Good variable names which should always be accepted, separated by a comma
41+
good-names=e,i,j,k,p,t,v,fn,db,tz
42+
43+
# [TYPECHECK]
44+
# ignored-classes=scoped_session
45+
46+
[SIMILARITIES]
47+
# Signatures are removed from the similarity computation
48+
ignore-signatures=yes
49+
50+
[LOGGING]
51+
logging-format-style=old
52+
53+
# Logging modules to check that the string format arguments are in logging
54+
# function parameter format.
55+
logging-modules=logging
56+
57+
[REPORTS]
58+
output-format=colorized

Dockerfile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM python:3.9
2+
3+
# Install the function's dependencies using file requirements.txt
4+
# from your project folder.
5+
WORKDIR /api
6+
COPY requirements.txt .
7+
RUN pip3 install --upgrade pip
8+
RUN pip3 install -r requirements.txt
9+
10+
# Copy function code
11+
COPY tasks.py .
12+
COPY prod_tasks.py .
13+
COPY app/ ./app
14+
15+
# Clean up the Python compilation cache
16+
RUN find ./ -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete
17+
18+
CMD invoke -c prod_tasks prodserver -p $PORT

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Simple Hello World REST API

app/__init__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import logging as log
2+
import sys
3+
4+
from flask import Flask
5+
6+
from app.controller import restx
7+
8+
app = Flask(__name__)
9+
10+
root = log.getLogger()
11+
root.setLevel(log.DEBUG)
12+
log_handler = log.StreamHandler(sys.stdout)
13+
log_handler.setLevel(log.DEBUG)
14+
formatter = log.Formatter('%(asctime)s %(levelname)s (%(filename)s:%(lineno)d) - %(message)s')
15+
log_handler.setFormatter(formatter)
16+
root.addHandler(log_handler)

app/controller/hello_controller.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging as log
2+
3+
from flask import request
4+
from flask_restx import Namespace, Resource, fields
5+
6+
from app.service import hello_world_service
7+
from app.util.flask_decorators import require_auth
8+
9+
ns = Namespace('hello', description='Hello api')
10+
11+
resource_fields = ns.model('Resource', {
12+
'name': fields.String(example="John")
13+
})
14+
15+
16+
@ns.route('/')
17+
class HelloController(Resource):
18+
19+
@ns.response(200, 'OK')
20+
def get(self): # pylint: disable=no-self-use
21+
try:
22+
return hello_world_service.say_hello(), 200
23+
except Exception: # pylint: disable=broad-except
24+
log.exception("Error calling service")
25+
return "Bad Request", 400
26+
27+
28+
@ns.route('/<string:api_key>')
29+
class ProtectedHelloController(Resource):
30+
31+
@ns.response(200, 'OK')
32+
@ns.doc(body=resource_fields)
33+
@require_auth()
34+
def post(self, api_key: str): # pylint: disable=no-self-use
35+
try:
36+
payload = request.get_json()
37+
name = payload.get('name', None)
38+
if name is None:
39+
return "No name defined in request body", 400
40+
return f"Hello {name}!", 200
41+
except Exception: # pylint: disable=broad-except
42+
log.exception("Error echoing name")
43+
return "Bad Request", 400

app/controller/restx.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import logging as log
2+
3+
from flask_restx import Api
4+
from flask_swagger_ui import get_swaggerui_blueprint
5+
6+
api = Api(version='1.0', title='Python REST API',
7+
description='Template for building Python REST APIs')
8+
9+
10+
@api.errorhandler
11+
def default_error_handler(_):
12+
message = 'An unhandled exception occurred.'
13+
log.exception(message)
14+
return {'message': message}, 500
15+
16+
17+
def init(app):
18+
# app.config['SERVER_NAME'] = server_name + ':' + str(cfg.get('port', 5000))
19+
app.config['SWAGGER_UI_DOC_EXPANSION'] = 'list'
20+
app.config['RESTPLUS_VALIDATE'] = True
21+
app.config['RESTPLUS_MASK_SWAGGER'] = False
22+
app.config['ERROR_404_HELP'] = False
23+
24+
api_root = ''
25+
swagger_url = f"{api_root}/api/docs" # URL for exposing Swagger UI (without trailing '/')
26+
log.debug("Registering swagger ui: {}".format(swagger_url))
27+
api_url = f"{api_root}/swagger.json" # Our API url (can of course be a local resource)
28+
log.debug("Registering swagger api: %s", api_url)
29+
30+
# Call factory function to create our blueprint
31+
swaggerui_blueprint = get_swaggerui_blueprint(
32+
swagger_url, # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/'
33+
api_url,
34+
config={ # Swagger UI config overrides
35+
'app_name': "Python REST API"
36+
}
37+
)
38+
39+
# Register blueprint at URL
40+
# (URL must match the one given to factory function above)
41+
app.register_blueprint(swaggerui_blueprint, url_prefix=swagger_url)

app/controller/status_controller.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from flask_restx import Namespace, Resource
2+
3+
ns = Namespace('', description='Status reporting')
4+
5+
6+
@ns.route('/')
7+
class StatusController(Resource):
8+
9+
@ns.response(200, 'OK')
10+
def get(self): # pylint: disable=no-self-use
11+
return "OK", 200

app/server.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python
2+
import sys
3+
4+
from flask import Blueprint
5+
6+
from app import app
7+
from app.controller import restx, hello_controller, status_controller
8+
from app.controller.restx import api
9+
10+
restx.init(app=app)
11+
blueprint = Blueprint('api', __name__, url_prefix=None)
12+
api.init_app(blueprint)
13+
api.add_namespace(status_controller.ns)
14+
api.add_namespace(hello_controller.ns)
15+
app.register_blueprint(blueprint)
16+
17+
18+
def start_dev_server():
19+
app.run("0.0.0.0", port=8080, debug=True)
20+
21+
22+
if __name__ == "__main__":
23+
if len(sys.argv) <= 1:
24+
print("Usage: server.py [command]")
25+
print("Commands:")
26+
print("\tdevserver")
27+
print("\t\t run Flask server in debug mode")
28+
sys.exit(1)
29+
30+
cmd = sys.argv[1]
31+
if cmd == "devserver":
32+
start_dev_server()
33+
else:
34+
raise Exception(f"Unknown command '{sys.argv[1]}'")

app/service/hello_world_service.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def say_hello():
2+
return "Hello World!"

app/util/flask_decorators.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os
2+
from functools import wraps
3+
4+
5+
def check_api_key(**kwargs):
6+
api_key = os.environ.get('API_KEY', None)
7+
if api_key is None or ('api_key' in kwargs and kwargs['api_key'] == api_key):
8+
return True
9+
return False
10+
11+
12+
def require_auth():
13+
def decorator(fn):
14+
@wraps(fn)
15+
def wrapper(*args, **kwargs):
16+
if not check_api_key(**kwargs):
17+
return 'Forbidden', 403
18+
return fn(*args, **kwargs)
19+
return wrapper
20+
return decorator

docker-compose-dev.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: '3.9'
2+
3+
services:
4+
app:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile
8+
ports:
9+
- 8080:8080
10+
environment:
11+
API_KEY: foo
12+
PORT: 8080

prod_tasks.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from invoke import run as invoke_run, task
2+
3+
4+
@task
5+
def prodserver(context, daemon=False, unbuffered=True, host='0.0.0.0', port=8080, workers=2, timeout=3600):
6+
daemon = ' --daemon' if daemon is True else ''
7+
unbuffered = 'TRUE' if unbuffered is True else 'FALSE'
8+
app = 'server:app'
9+
cmd = f"gunicorn --bind {host}:{port}{daemon} --workers {workers} --timeout {timeout} {app}"
10+
invoke_run(cmd, env={'PYTHONPATH': './app', 'PYTHONUNBUFFERED': unbuffered})

requirements.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
invoke
2+
requests
3+
flask
4+
flask-restx
5+
flask-swagger-ui
6+
gunicorn

requirements_dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pylint

tasks.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from os import path
2+
3+
from invoke import run as invoke_run, task
4+
5+
6+
@task
7+
def devserver(context):
8+
CURRENT_DIR = path.abspath(path.dirname(__file__))
9+
print(CURRENT_DIR)
10+
invoke_run('app/server.py devserver', env={'PYTHONPATH': './', 'PYTHONUNBUFFERED': 'TRUE'})

0 commit comments

Comments
 (0)