Skip to content
Snippets Groups Projects
Commit 2483831b authored by Saket Agrahari's avatar Saket Agrahari
Browse files

COMP-11: Inital flask web skeleton

parent 0ebf2349
No related branches found
No related tags found
No related merge requests found
Showing
with 617 additions and 0 deletions
*.egg-info
__pycache__
coverage.xml
.coverage
htmlcov
.tox
dist
.idea
node_modules
docs/build
# Changelog
All notable changes to this project will be documented in this file.
## [0.1] - yyyy-mm-dd
- initial skeleton
recursive-include compendium_v2/static *
#Installation Process
git clone https://gitlab.geant.net/live-projects/compendium-v2.git
python3 -m venv compendium-v2
. tmp_virtualenv/bin/activate
pip install tox
cd compendium-v2
tox -e py39
# Skeleton Web App
## Overview
This module implements a skeleton Flask-based webservice
and in-browser React front-end.
The webservice communicates with the front end over HTTP.
Responses to valid requests are returned as JSON messages.
The server will therefore return an error unless
`application/json` is in the `Accept` request header field.
HTTP communication and JSON grammar details are
beyond the scope of this document.
Please refer to [RFC 2616](https://tools.ietf.org/html/rfc2616)
and www.json.org for more details.
## Configuration
This app allows specification of a few
example configuration parameters. These
parameters should stored in a file formatted
similarly to `config.json.example`, and the name
of this file should be stored in the environment
variable `SETTINGS_FILENAME` when running the service.
## Building the web application
The initial repository doesn't contain the required web application.
For instructions on building this see `webapp/README.md`.
## Running this module
This module has been tested in the following execution environments:
- As an embedded Flask application.
For example, the application could be launched as follows:
```bash
$ export FLASK_APP=compendium_v2.app
$ export SETTINGS_FILENAME=config-example.json
$ flask run
```
See https://flask.palletsprojects.com/en/2.1.x/deploying/
for best practices about running in production environments.
### resources
Any non-empty responses are JSON formatted messages.
#### /data/version
* /version
The response will be an object
containing the module and protocol versions of the
running server and will be formatted as follows:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"api": {
"type": "string",
"pattern": r'\d+\.\d+'
},
"module": {
"type": "string",
"pattern": r'\d+\.\d+'
}
},
"required": ["api", "module"],
"additionalProperties": False
}
```
#### /test/test1
The response will be some json data, as an example ...
"""
automatically invoked app factory
"""
import logging
import os
from flask import Flask
from flask_cors import CORS # for debugging
from compendium_v2 import environment
from compendium_v2 import config
def create_app():
"""
overrides default settings with those found
in the file read from env var SETTINGS_FILENAME
:return: a new flask app instance
"""
assert 'SETTINGS_FILENAME' in os.environ
with open(os.environ['SETTINGS_FILENAME']) as f:
app_config = config.load(f)
app = Flask(__name__)
CORS(app)
app.secret_key = 'super secret session key'
app.config['CONFIG_PARAMS'] = app_config
from compendium_v2.routes import default
app.register_blueprint(default.routes, url_prefix='/')
from compendium_v2.routes import api
app.register_blueprint(api.routes, url_prefix='/api')
logging.info('Flask app initialized')
environment.setup_logging()
return app
"""
default app creation
"""
import compendium_v2
from compendium_v2 import environment
environment.setup_logging()
app = compendium_v2.create_app()
if __name__ == "__main__":
app.run(host="::", port="33333")
import json
import jsonschema
CONFIG_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'properties': {
'str-param': {'type': 'string'},
'int-param': {'type': 'integer'}
},
'required': ['str-param', 'int-param'],
'additionalProperties': False
}
def load(f):
"""
loads, validates and returns configuration parameters
:param f: file-like object that produces the config file
:return:
"""
config = json.loads(f.read())
jsonschema.validate(config, CONFIG_SCHEMA)
return config
import json
import logging.config
import os
LOGGING_DEFAULT_CONFIG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(asctime)s - %(name)s '
'(%(lineno)d) - %(levelname)s - %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'simple',
'stream': 'ext://sys.stdout'
}
},
'loggers': {
'compendium_v2': {
'level': 'DEBUG',
'handlers': ['console'],
'propagate': False
}
},
'root': {
'level': 'WARNING',
'handlers': ['console']
}
}
def setup_logging():
"""
set up logging using the configured filename
if LOGGING_CONFIG is defined in the environment, use this for
the filename, otherwise use LOGGING_DEFAULT_CONFIG
"""
logging_config = LOGGING_DEFAULT_CONFIG
if 'LOGGING_CONFIG' in os.environ:
filename = os.environ['LOGGING_CONFIG']
with open(filename) as f:
logging_config = json.loads(f.read())
logging.config.dictConfig(logging_config)
"""
API Endpoints
=========================
.. contents:: :local:
/api/things
---------------------
.. autofunction:: compendium_v2.routes.api.things
"""
import binascii
import hashlib
import random
import time
from flask import Blueprint, jsonify
from compendium_v2.routes import common
routes = Blueprint("compendium-v2-api", __name__)
THING_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'thing': {
'type': 'object',
'properties': {
'id': {'type': 'string'},
'time': {'type': 'number'},
'state': {'type': 'boolean'},
'data1': {'type': 'string'},
'data2': {'type': 'string'},
'data3': {'type': 'string'}
},
'required': ['id', 'time', 'state', 'data1', 'data2', 'data3'],
'additionalProperties': False
}
},
'type': 'array',
'items': {'$ref': '#/definitions/thing'}
}
@routes.after_request
def after_request(resp):
return common.after_request(resp)
@routes.route("/things", methods=['GET', 'POST'])
@common.require_accepts_json
def things():
"""
handler for /api/things requests
response will be formatted as:
.. asjson::
compendium_v2.routes.api.THING_LIST_SCHEMA
:return:
"""
def _hash(s, length):
m = hashlib.sha256()
m.update(s.encode('utf-8'))
digest = binascii.b2a_hex(m.digest()).decode('utf-8')
return digest[-length:].upper()
def _make_thing(idx):
six_months = 24 * 3600 * 180
return {
'id': _hash(f'id-{idx}', 4),
'time': int(time.time() + random.randint(-six_months, six_months)),
'state': bool(idx % 2),
'data1': _hash(f'data1-{idx}', 2),
'data2': _hash(f'data2-{idx}', 8),
'data3': _hash(f'data3-{idx}', 32)
}
response = map(_make_thing, range(20))
return jsonify(list(response))
"""
Utilities used by multiple route blueprints.
"""
import functools
import logging
from flask import request, Response
logger = logging.getLogger(__name__)
_DECODE_TYPE_XML = 'xml'
_DECODE_TYPE_JSON = 'json'
def require_accepts_json(f):
"""
used as a route handler decorator to return an error
unless the request allows responses with type "application/json"
:param f: the function to be decorated
:return: the decorated function
"""
@functools.wraps(f)
def decorated_function(*args, **kwargs):
# TODO: use best_match to disallow */* ...?
if not request.accept_mimetypes.accept_json:
return Response(
response="response will be json",
status=406,
mimetype="text/html")
return f(*args, **kwargs)
return decorated_function
def after_request(response):
"""
Generic function to do additional logging of requests & responses.
:param response:
:return:
"""
if response.status_code != 200:
try:
data = response.data.decode('utf-8')
except Exception:
# never expected to happen, but we don't want any failures here
logging.exception('INTERNAL DECODING ERROR')
data = 'decoding error (see logs)'
logger.warning('"%s %s" "%s" %s' % (
request.method,
request.path,
data,
str(response.status_code)))
return response
"""
Default Endpoints
=========================
.. contents:: :local:
/version
---------------------
.. autofunction:: compendium_v2.routes.default.version
"""
import pkg_resources
from flask import Blueprint, jsonify
from compendium_v2.routes import common
routes = Blueprint("compendium-v2-default", __name__)
API_VERSION = '0.1'
VERSION_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'properties': {
'api': {
'type': 'string',
'pattern': r'\d+\.\d+'
},
'module': {
'type': 'string',
'pattern': r'\d+\.\d+'
}
},
'required': ['api', 'module'],
'additionalProperties': False
}
@routes.after_request
def after_request(resp):
return common.after_request(resp)
@routes.route("/version", methods=['GET', 'POST'])
@common.require_accepts_json
def version():
"""
handler for /version requests
response will be formatted as:
.. asjson::
compendium_v2.routes.default.VERSION_SCHEMA
:return:
"""
version_params = {
'api': API_VERSION,
'module':
pkg_resources.get_distribution('compendium-v2').version
}
return jsonify(version_params)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
\ No newline at end of file
{
"str-param": "some string",
"int-param": -1234
}
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.. api intro
API Protocol
===============
This module implements a Flask-based webservice which
communicates with clients over HTTP.
Responses to valid requests are returned as JSON messages.
The server will therefore return an error unless
`application/json` is in the `Accept` request header field.
HTTP communication and JSON grammar details are
beyond the scope of this document.
Please refer to [RFC 2616](https://tools.ietf.org/html/rfc2616)
and www.json.org for more details.
.. contents:: :local:
.. automodule:: compendium_v2.routes.default
.. automodule:: compendium_v2.routes.api
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
from importlib import import_module
from docutils.parsers.rst import Directive
from docutils import nodes
from sphinx import addnodes
import json
import os
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(
os.path.dirname(__file__),
'..', '..', 'compendium_v2')))
class RenderAsJSON(Directive):
# cf. https://stackoverflow.com/a/59883833
required_arguments = 1
def run(self):
module_path, member_name = self.arguments[0].rsplit('.', 1)
member_data = getattr(import_module(module_path), member_name)
code = json.dumps(member_data, indent=2)
literal = nodes.literal_block(code, code)
literal['language'] = 'json'
return [
addnodes.desc_name(text=member_name),
addnodes.desc_content('', literal)
]
def setup(app):
app.add_directive('asjson', RenderAsJSON)
# -- Project information -----------------------------------------------------
# TODO: give this a better project name
project = 'compendium-v2'
copyright = '2022, GÉANT Vereniging'
author = 'swd@geant.org'
# The full version, including alpha/beta/rc tags
release = '0.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx_rtd_theme',
'sphinx.ext.autodoc',
'sphinx.ext.coverage'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Both the class’ and the __init__ method’s docstring
# are concatenated and inserted.
autoclass_content = "both"
autodoc_typehints = "none"
compendium_v2
========================================
This project is a Flask-based webservice serving json-based data, and
a React web application that consumes and renders the json data.
.. toctree::
:maxdepth: 2
:caption: Contents:
api
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment