Skip to content
Snippets Groups Projects
Commit b6b4256f authored by Erik Reid's avatar Erik Reid
Browse files

initial template

parents
Branches main
No related tags found
No related merge requests found
Showing
with 605 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 gap_service_editor/static *
# 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=gap_service_editor.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 ...
{
"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:: gap_service_editor.routes.default
.. automodule:: gap_service_editor.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__),
'..', '..', 'gap_service_editor')))
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 = 'gap-service-editor'
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"
gap_service_editor
========================================
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
"""
automatically invoked app factory
"""
import logging
import os
from flask import Flask
from flask_cors import CORS # for debugging
from gap_service_editor import environment
from gap_service_editor 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 gap_service_editor.routes import default
app.register_blueprint(default.routes, url_prefix='/')
from gap_service_editor.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 gap_service_editor
from gap_service_editor import environment
environment.setup_logging()
app = gap_service_editor.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': {
'gap_service_editor': {
'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:: gap_service_editor.routes.api.things
"""
import binascii
import hashlib
import random
import time
from flask import Blueprint, jsonify
from gap_service_editor.routes import common
routes = Blueprint("gap-service-editor-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::
gap_service_editor.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:: gap_service_editor.routes.default.version
"""
import pkg_resources
from flask import Blueprint, jsonify
from gap_service_editor.routes import common
routes = Blueprint("gap-service-editor-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::
gap_service_editor.routes.default.VERSION_SCHEMA
:return:
"""
version_params = {
'api': API_VERSION,
'module':
pkg_resources.get_distribution('gap-service-editor').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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment