diff --git a/.env b/.env new file mode 100644 index 0000000..202bb0a --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +ACTIVE_ENV='DEVELOPMENT' +MICROSERVICE_NAME='service_name' +PORT=5000 diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 0000000..0d31877 --- /dev/null +++ b/.flaskenv @@ -0,0 +1,4 @@ +FLASK_APP="src.app:app" +FLASK_ENV=DEVELOPMENT + +host=0.0.0.0 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04da507 --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +# .env +.venv +# env/ +venv/ +ENV/ +# env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### JetBrains template + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml + +# Sensitive or high-churn files: +.idea/dataSources/ +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/* +.idea + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4dd15ec --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "sdk"] + path = sdk + url = git@github.com:PyBackendBoilerplate/sdk.git diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..9ecf4c3 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,71 @@ +# Code style formatting settings +# https://github.com/google/yapf + +[style] + +# Which predefined style this style is based on. +BASED_ON_STYLE = google + +# The number of columns to use for indentation. +INDENT_WIDTH = 4 + +# Max line length +COLUMN_LIMIT = 120 + + +# Put closing brackets on a separate line, dedented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is dedented and on a separate line +DEDENT_CLOSING_BRACKETS = True + +# Do not split consecutive brackets. Only relevant when +# dedent_closing_brackets is set. For example: +# +# call_func_that_takes_a_dict( +# { +# 'key1': 'value1', +# 'key2': 'value2', +# } +# ) +# +# would reformat to: +# +# call_func_that_takes_a_dict({ +# 'key1': 'value1', +# 'key2': 'value2', +# }) +COALESCE_BRACKETS = True + +# Let spacing indicate operator precedence. +# For example: +# +# a = 1 * 2 + 3 / 4 +# b = 1 / 2 - 3 * 4 +# c = (1 + 2) * (3 - 4) +# d = (1 - 2) / (3 + 4) +# e = 1 * 2 - 3 +# f = 1 + 2 + 3 + 4 +# will be formatted as follows to indicate precedence: +# +# a = 1*2 + 3/4 +# b = 1/2 - 3*4 +# c = (1+2) * (3-4) +# d = (1-2) / (3+4) +# e = 1*2 - 3 +# f = 1 + 2 + 3 + 4 +ARITHMETIC_PRECEDENCE_INDICATION = True + +# If a comma separated list (dict, list, tuple, or function def) +# is on a line that is too long, split such that all elements are on a single line. +#SPLIT_ALL_COMMA_SEPARATED_VALUES = TRUE + + +# More settings +# https://github.com/google/yapf#knobs +# https://gist.github.com/krnd/3b8c5834c5c5c5097638ec10729787f7 +# https://github.com/google/yapf/blob/51ffe2d07930a509ecb2ef454a7b251eeeac0a59/yapf/yapflib/style.py \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d119488 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,62 @@ +{ + // 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 + "configurations": [ + { + "name": "Python: Gunicorn", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/.venv/bin/gunicorn", + "env": { + // Keeps Python from generating .pyc files (__pycache__) + "PYTHONDONTWRITEBYTECODE": "1" + }, + "args": [ + "--config", + "src/config/gunicorn_conf.py", + "src.app:app" + ] + // "postDebugTask": "Kill flask process if debugger was detached before the process was closed" + }, + { + "name": "Python: Docker (Remote Attach)", + "type": "python", + "request": "attach", + "port": 10001, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app" + } + ] + }, + { + "name": "Python: Flask", + "type": "python", + "request": "launch", + "module": "flask", + "args": [ + "run" + ], + "jinja": true, + "env": { + // Keeps Python from generating .pyc files (__pycache__) + "PYTHONDONTWRITEBYTECODE": "1" + } + }, + { + "name": "Python: Script", + "type": "python", + "request": "launch", + "program": "src/main.py", + "console": "integratedTerminal", + "env": { + // Keeps Python from generating .pyc files (__pycache__) + "PYTHONDONTWRITEBYTECODE": "1" + } + // "cwd": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24979fa --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "python.pythonPath": "${workspaceFolder}/.venv/bin/python3", + "python.languageSever": "Pylance", + "python.analysis.autoSearchPaths": true, + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "editor.formatOnSave": true, + "python.formatting.provider": "yapf", + "terminal.integrated.env.osx": { + "PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}/src" + }, + "terminal.integrated.env.linux": { + "PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}/src" + }, + "terminal.integrated.env.windows": { + "PYTHONPATH": "${env:PYTHONPATH};${workspaceFolder}/src" + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3ad58e5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Kill flask process if debugger was detached before the process was closed", + "type": "shell", + "command": "lsof -wni tcp:5000 | awk 'NR>1{kill -9 $2}'" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..394eb8c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# For more information, please refer to https://aka.ms/vscode-docker-python +FROM python:3.8 + +EXPOSE 5000 + +# Keeps Python from generating .pyc files in the container +ENV PYTHONDONTWRITEBYTECODE 1 + +# Turns off buffering for easier container logging +ENV PYTHONUNBUFFERED 1 + +WORKDIR /app + +ADD sdk sdk +ADD src src + +ADD .env . +ADD .flaskenv . + +# Install pip requirements +ADD requirements requirements +ADD install_requirements.sh . +RUN ./install_requirements.sh + +# Switching to a non-root user, please refer to https://aka.ms/vscode-docker-python-user-rights +RUN useradd appuser && chown -R appuser /app +USER appuser + +# gunicorn --check-config APP_MODULE + +# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug +# docker-compose up will run this +CMD ["gunicorn", "--config", "src/config/gunicorn_conf.py", "src.app:app"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d9da8de --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Taken originally from: https://github.com/adriencaccia/vscode-flask-debug + +.PHONY: help +.DEFAULT_GOAL := help +help: + @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' + + +## Build tools +install: ## setup the flask Docker container, install the requirements... + docker-compose build + +clean: ## Clean __pycache__ files + py3clean . + + +## Start the Dockerized flask application in local environment +gunicorn: ## gunicorn and hot-reload + docker-compose run --rm -e VSCODE_DEBUG_MODE=False --service-ports service_name gunicorn --config "src/config/gunicorn_conf.py" "src.app:app" + +gunicorndebug: ## gunicorn, hot-reload and VS Code debugger + docker-compose run --rm -e VSCODE_DEBUG_MODE=True --service-ports service_name gunicorn --config "src/config/gunicorn_conf.py" "src.app:app" + +flask: ## flask and hot-reload + docker-compose run --rm -e VSCODE_DEBUG_MODE=False --service-ports service_name flask run + +flaskdebug: ## flask, hot-reload and VS Code debugger + docker-compose run --rm -e VSCODE_DEBUG_MODE=True --service-ports service_name flask run diff --git a/README.md b/README.md new file mode 100644 index 0000000..fba7feb --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ + +# PyBackendBoilerplate Micro-Service Boilerplate. + +## Development status: WIP + +This is a [template github repository](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) for a microservice that is based on [Flask](https://flask.palletsprojects.com/) and [OpenAPI 3](https://swagger.io/specification/) using [Connexion](https://github.com/zalando/connexion). + +The infrastructure is based on the [SDK package](https://github.com/PyBackendBoilerplate/sdk) for all common features and is based on [this](https://github.com/PyBackendBoilerplate/sdk#tech-stack) stack. + +## How do I use this + +Basically what you need to do is create your own copy of this repository (can use the [GitHub Template Repository](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) feature) and you can start working. + +### Project source structure + +``` +src -> Root folder for the source code +│ app.py -> Module containing the WSGI (app) +│ main.py -> Main file for running as local python script +| sdk -> Soft link to ../../sdk/src/sdk +│ +└───api -> All routes RESTful APIs implementation +│ │ root.py -> Implements the root path's RESTful API +│ │ ... more routes's implementations +│ │ +│ └───sdk -> Soft link to ../../sdk/src/sdk/api +│ +└───config -> The micro-service specific's configurations +│ │ gunicorn_conf.py -> Gunicorn's config +│ +└───openapi -> Root folder for the OpenAPI specification YAML files + │ openapi.yaml -> Main (root) OpenAPI specification YAML file +``` + +This gives you a basic file structure with a working mechanism for running an app in a microservice. +From here on you need to rename some of the places with your own microservice name and add your own APIs and that's it, everything else is already set. + +Feel free to remove the example [APIs](./src/openapi/openapi.yaml#L10) and change them with your own. + +## How do I run this + +The package uses a [Makefile](./Makefile) for easy CLI but there are also the common methods. + +1) So first clone this repository. +2) Run `git submodule update --init --recursive` inside the repository (to get the sdk's code). +3) Run `make install` in the root repository directory. +4) Try it out using one of the available running options:s + + * Run `run_locally.sh` to run locally as a script (with auto-create a .venv and use that) - does not requires `make install` prior to running this way. + * `make gunicorn` to run with Gunicorn (in the docker container). + * `make gunicorndebug` to run with Gunicorn with container debugging enabled (VS Code only). + * `make flask` to run with the standard Flask CLI (in the docker container). + * `make flaskdebug` to run with the standard Flask CLI with container debugging enabled (VS Code only). + * `docker-compose up` to run with docker-compose (see [Dockerfile](./Dockerfile#L33) for entry point definition) + * `docker run micro-service_service_name` to run with docker (see [Dockerfile](./Dockerfile#L33) for entry point definition) + +Afterwards, you can see it running in your browser: +- http://0.0.0.0:5000/v1.0/ -> Will be handled by the [root()](./src/api/root.py#L15) function. +- http://localhost:5000/v1.0/ui/#/ -> Automatically generated Swagger UI. + +For example, running and using the http://0.0.0.0:5000/v1.0/ping REST API from the Swagger UI (using Gunicorn): + +![](./example_swagger.png) + +### Running in docker container with Flask + +``` +myuser@ [~/dev/pyBackendBoilerplate] [Git Branch: master] : +$ git clone https://github.com/PyBackendBoilerplate/micro-service.git +Cloning into 'micro-service'... +remote: Enumerating objects: 61, done. +remote: Counting objects: 100% (61/61), done. +remote: Compressing objects: 100% (40/40), done. +remote: Total 61 (delta 6), reused 61 (delta 6), pack-reused 0 +Receiving objects: 100% (61/61), 123.81 KiB | 220.00 KiB/s, done. +Resolving deltas: 100% (6/6), done. + +myuser@ [~/dev/pyBackendBoilerplate] [Git Branch: master] : +$ cd micro-service/ + +myuser@ [~/dev/pyBackendBoilerplate/micro-service] [Git Branch: master] : +$ git submodule update --init --recursive +Submodule 'sdk' (git@github.com:PyBackendBoilerplate/sdk.git) registered for path 'sdk' +Cloning into '/home/nusnus/dev/PyBackendBoilerplate/ttt/micro-service/sdk'... +Submodule path 'sdk': checked out '9aa954b8a8ff47bd1ecd3f6b6506cfbf3f95f348' + +myuser@ [~/dev/pyBackendBoilerplate/micro-service] [Git Branch: master] : +$ make install +docker-compose build +Building service_name +Step 1/15 : FROM python:3.8 + ---> 7f5b6ccd03e9 +... +Step 15/15 : CMD ["gunicorn", "--config", "src/config/gunicorn_conf.py", "src.app:app"] + ---> Running in f6840528b530 +Removing intermediate container f6840528b530 + ---> 504a6fe9312a + +Successfully built 504a6fe9312a +Successfully tagged micro-service_service_name:latest + +myuser@ [~/dev/pyBackendBoilerplate/micro-service] [Git Branch: master] : +$ make flask +docker-compose run --rm -e VSCODE_DEBUG_MODE=False --service-ports service_name flask run + * Serving Flask app "src.app:app" + * Environment: DEVELOPMENT + * Debug mode: off + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +``` + +Entering into: http://0.0.0.0:5000/v1.0/ +``` +127.0.0.1 - - [21/Aug/2020 21:22:58] "GET /v1.0/ HTTP/1.1" 200 - +``` + +Entering into: http://localhost:5000/v1.0/ui/#/ +``` +127.0.0.1 - - [21/Aug/2020 21:23:21] "GET /v1.0/ui/ HTTP/1.1" 200 - +127.0.0.1 - - [21/Aug/2020 21:23:21] "GET /v1.0/openapi.json HTTP/1.1" 200 - +``` + +Running the http://0.0.0.0:5000/v1.0/ping API using the Swagger UI: +``` +127.0.0.1 - - [21/Aug/2020 21:23:31] "GET /v1.0/ping/ HTTP/1.1" 200 - +``` + +### Running in docker container with Gunicorn + +``` +$ make gunicorn +docker-compose run --rm -e VSCODE_DEBUG_MODE=False --service-ports service_name gunicorn --config "src/config/gunicorn_conf.py" "src.app:app" +[2020-08-21 21:30:37 +0000] [1] [INFO] Starting gunicorn 20.0.4 +[2020-08-21 21:30:37 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1) +[2020-08-21 21:30:37 +0000] [1] [INFO] Using worker: sync +[2020-08-21 21:30:37 +0000] [1] [INFO] Server is ready. Spawning workers +[2020-08-21 21:30:37 +0000] [7] [INFO] Booting worker with pid: 7 +[2020-08-21 21:30:37 +0000] [7] [INFO] Worker spawned (pid: 7) +``` + +Both URLs are working the same: +- http://0.0.0.0:5000/v1.0/ +- http://localhost:5000/v1.0/ui/#/ + +And after hitting Ctrl+C in the terminal: + +``` +^C[2020-08-21 21:32:17 +0000] [7] [INFO] worker received INT or QUIT signal +[2020-08-21 21:32:17 +0000] [1] [INFO] Handling signal: int +[2020-08-21 21:32:17 +0000] [7] [INFO] worker received INT or QUIT signal +[2020-08-21 21:32:17 +0000] [7] [INFO] Worker exiting (pid: 7) +[2020-08-21 21:32:17 +0000] [1] [INFO] Shutting down: Master +``` + +### Running locally as a python script + +``` +myuser@ [~/dev/pyBackendBoilerplate/micro-service] [Git Branch: master] : +$ ./run_locally.sh +Installing virtual environment in .venv +Active Environment: DEVELOPMENT +Installing pip requirements files in the virtual environment: .venv + +Collecting pip + Using cached pip-20.2.2-py2.py3-none-any.whl (1.5 MB) +Installing collected packages: pip + Attempting uninstall: pip + Found existing installation: pip 20.0.2 + Uninstalling pip-20.0.2: + Successfully uninstalled pip-20.0.2 +Successfully installed pip-20.2.2 +Collecting Flask + Using cached Flask-1.1.2-py2.py3-none-any.whl (94 kB) +Collecting gunicorn + Using cached gunicorn-20.0.4-py2.py3-none-any.whl (77 kB) +... + +Finished installing pip requirements files in the virtual environment: .venv + * Serving Flask app "service_name" (lazy loading) + * Environment: DEVELOPMENT + * Debug mode: off + * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) +``` + +Both URLs are working the same: +- http://0.0.0.0:5000/v1.0/ +- http://localhost:5000/v1.0/ui/#/ + +``` +127.0.0.1 - - [22/Aug/2020 00:58:30] "GET /v1.0/ HTTP/1.1" 200 - +127.0.0.1 - - [22/Aug/2020 00:58:45] "GET /v1.0/ui/ HTTP/1.1" 200 - +127.0.0.1 - - [22/Aug/2020 00:58:45] "GET /v1.0/ui/swagger-ui.css HTTP/1.1" 200 - +127.0.0.1 - - [22/Aug/2020 00:58:45] "GET /v1.0/openapi.json HTTP/1.1" 200 - +127.0.0.1 - - [22/Aug/2020 00:58:52] "GET /v1.0/ping/ HTTP/1.1" 200 - +``` + +### Running in using docker-container up (per defined in the Dockerfile) + +``` +myuser@ [~/dev/pyBackendBoilerplate/micro-service] [Git Branch: master] : +$ docker-compose up +Recreating micro-service_service_name_1 ... done +Attaching to micro-service_service_name_1 +service_name_1 | [2020-08-21 22:09:07 +0000] [1] [INFO] Starting gunicorn 20.0.4 +service_name_1 | [2020-08-21 22:09:07 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1) +service_name_1 | [2020-08-21 22:09:07 +0000] [1] [INFO] Using worker: sync +service_name_1 | [2020-08-21 22:09:07 +0000] [1] [INFO] Server is ready. Spawning workers +service_name_1 | [2020-08-21 22:09:07 +0000] [7] [INFO] Booting worker with pid: 7 +service_name_1 | [2020-08-21 22:09:07 +0000] [7] [INFO] Worker spawned (pid: 7) +``` + +Both URLs are working the same: +- http://0.0.0.0:5000/v1.0/ +- http://localhost:5000/v1.0/ui/#/ + +And after hitting Ctrl+C in the terminal: + +``` +^CGracefully stopping... (press Ctrl+C again to force) +Stopping micro-service_service_name_1 ... done +``` + +## Debugging options (VS Code only) + +Run locally via with the `Python: Gunicorn` VS Code launch configuration. + +To run as a docker container and debug remotely, use the `Python: Docker (Remote Attach)` VS Code launch configuration. To be used when running with `make gunicorndebug` or `make flaskdebug`. + +For example (Running from the base dir of the Makefile): +- Step 0) Set the `Python: Docker (Remote Attach)` as your selected launch config. +- Step 1) Run `make gunicorndebug` in the VS Code terminal. +- Step 2) Wait for the debugger attaching instructions in stdout. +- Step 3) Set some breakpoints and that's it, you're debugging the container running the Flask app. + +For addition help on this subject, see [Flask Debugging in VS Code with Hot-Reload 🔥](https://blog.theodo.com/2020/05/debug-flask-vscode/). + +## Development + +For additional development info, please see the [SDK's instructions and information](https://github.com/PyBackendBoilerplate/sdk#development). \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f3f91f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.4" + +# TODO: invest some time in this file... + +services: + service_name: + build: + context: . + dockerfile: Dockerfile + ports: + - 5000:5000 + - 10001:10001 + # environment: + # - DEBUGGER=True + volumes: + - .:/app:cached + # set to host in the dev env only! + network_mode: host diff --git a/envs/development.env b/envs/development.env new file mode 100644 index 0000000..4fe9fe8 --- /dev/null +++ b/envs/development.env @@ -0,0 +1,2 @@ +ACTIVE_ENV='DEVELOPMENT' +PORT=5000 \ No newline at end of file diff --git a/envs/production.env b/envs/production.env new file mode 100644 index 0000000..00b6d6e --- /dev/null +++ b/envs/production.env @@ -0,0 +1,2 @@ +ACTIVE_ENV='PRODUCTION' +PORT=5000 \ No newline at end of file diff --git a/envs/staging.env b/envs/staging.env new file mode 100644 index 0000000..f9a6dd0 --- /dev/null +++ b/envs/staging.env @@ -0,0 +1,2 @@ +ACTIVE_ENV='STAGING' +PORT=5000 \ No newline at end of file diff --git a/example_swagger.png b/example_swagger.png new file mode 100644 index 0000000..d798d64 Binary files /dev/null and b/example_swagger.png differ diff --git a/install_requirements.sh b/install_requirements.sh new file mode 120000 index 0000000..068227d --- /dev/null +++ b/install_requirements.sh @@ -0,0 +1 @@ +sdk/scripts/install_requirements.sh \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..65eed93 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1 @@ +-r ../sdk/requirements/base.txt \ No newline at end of file diff --git a/requirements/development.txt b/requirements/development.txt new file mode 100644 index 0000000..f9096e3 --- /dev/null +++ b/requirements/development.txt @@ -0,0 +1,2 @@ +-r base.txt +-r ../sdk/requirements/development.txt \ No newline at end of file diff --git a/requirements/production.txt b/requirements/production.txt new file mode 100644 index 0000000..cf8d544 --- /dev/null +++ b/requirements/production.txt @@ -0,0 +1,2 @@ +-r base.txt +-r ../sdk/requirements/production.txt \ No newline at end of file diff --git a/requirements/staging.txt b/requirements/staging.txt new file mode 100644 index 0000000..9da7f1f --- /dev/null +++ b/requirements/staging.txt @@ -0,0 +1,2 @@ +-r base.txt +-r ../sdk/requirements/staging.txt \ No newline at end of file diff --git a/run_locally.sh b/run_locally.sh new file mode 100755 index 0000000..aee170a --- /dev/null +++ b/run_locally.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +VENV=".venv" +if [ ! -d "$VENV" ]; then + echo "Installing virtual environment in $VENV" + python3 -m venv $VENV + + ./install_requirements.sh +fi + +source .venv/bin/activate + +python src/main.py + +deactivate + +py3clean . \ No newline at end of file diff --git a/sdk b/sdk new file mode 160000 index 0000000..9aa954b --- /dev/null +++ b/sdk @@ -0,0 +1 @@ +Subproject commit 9aa954b8a8ff47bd1ecd3f6b6506cfbf3f95f348 diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/hello/__init__.py b/src/api/hello/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/hello/world/__init__.py b/src/api/hello/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/hello/world/greeting/__init__.py b/src/api/hello/world/greeting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/hello/world/greeting/greeting.py b/src/api/hello/world/greeting/greeting.py new file mode 100755 index 0000000..3aab06e --- /dev/null +++ b/src/api/hello/world/greeting/greeting.py @@ -0,0 +1,18 @@ +"""APIs implementation. + +Implementing the route's RESTFul API. + + To attach route handlers functions to their routes in the relevant openapi yaml file, use this: + x-openapi-router-controller: [module path after src].[python module name (without extension)] + operationId: Route handler function name + + Example: + x-openapi-router-controller: api.hello.world.greeting.greeting + operationId: greeting +""" + +from api.hello.world.root import root + + +def greeting() -> str: + return root().replace('Hello, World', 'Greetings') diff --git a/src/api/hello/world/greeting/personal.py b/src/api/hello/world/greeting/personal.py new file mode 100755 index 0000000..661a08d --- /dev/null +++ b/src/api/hello/world/greeting/personal.py @@ -0,0 +1,18 @@ +"""APIs implementation. + +Implementing the route's RESTFul API. + + To attach route handlers functions to their routes in the relevant openapi yaml file, use this: + x-openapi-router-controller: [module path after src].[python module name (without extension)] + operationId: Route handler function name + + Example: + x-openapi-router-controller: api.hello.world.greeting.personal + operationId: personal_greeting +""" + +from api.hello.world.root import root + + +def personal_greeting(name: str) -> str: + return root().replace('World', name) diff --git a/src/api/hello/world/root.py b/src/api/hello/world/root.py new file mode 100644 index 0000000..2b839bb --- /dev/null +++ b/src/api/hello/world/root.py @@ -0,0 +1,22 @@ +"""APIs implementation. + +Implementing the route's RESTFul API. + + To attach route handlers functions to their routes in the relevant openapi yaml file, use this: + x-openapi-router-controller: [module path after src].[python module name (without extension)] + operationId: Route handler function name + + Example: + x-openapi-router-controller: api.hello.world.root + operationId: root +""" + +from datetime import datetime + + +def root() -> str: + now = datetime.now() + formatted_now = now.strftime('%A, %d %B, %Y at %X') + + content = f"Hello, World! It's {formatted_now}" + return content \ No newline at end of file diff --git a/src/api/root.py b/src/api/root.py new file mode 100644 index 0000000..2615349 --- /dev/null +++ b/src/api/root.py @@ -0,0 +1,16 @@ +"""APIs implementation. + +Implementing the route's RESTFul API. + + To attach route handlers functions to their routes in the relevant openapi yaml file, use this: + x-openapi-router-controller: [module path after src].[python module name (without extension)] + operationId: Route handler function name + + Example: + x-openapi-router-controller: api.root + operationId: root +""" + + +def root() -> str: + return 'Swagger UI: http://localhost:5000/v1.0/ui/#/' \ No newline at end of file diff --git a/src/api/sdk b/src/api/sdk new file mode 120000 index 0000000..8fa3729 --- /dev/null +++ b/src/api/sdk @@ -0,0 +1 @@ +../../sdk/src/sdk/api \ No newline at end of file diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..edec604 --- /dev/null +++ b/src/app.py @@ -0,0 +1,22 @@ +import os +import sys +from pathlib import Path + +sys.path.append(os.path.join(sys.path[0], 'src')) + +from sdk.infra.app.wsgi import create_connexion_microservice + +# Create the microservice and receive its settings + +microservice, settings = create_connexion_microservice() + +# Modify the settings as required + +settings.openapi_spec = Path('src/openapi/openapi.yaml') + +# Use the microservice to create the WSGI app instance (with the modified settings) + +connexion_app = microservice.create_app() + +# For automatic app:app lookup +app = connexion_app.app diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/gunicorn_conf.py b/src/config/gunicorn_conf.py new file mode 100644 index 0000000..4d7371d --- /dev/null +++ b/src/config/gunicorn_conf.py @@ -0,0 +1,10 @@ +import sys +import os + +sys.path.append(os.path.join(sys.path[0], 'src')) + +from sdk.config.gunicorn_conf import * + +# override default sdk values here + +bind = '0.0.0.0:5000' diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..6cd6b86 --- /dev/null +++ b/src/main.py @@ -0,0 +1,9 @@ +import sys +import os + +sys.path.append(os.path.join(sys.path[0], 'src')) + +from app import connexion_app, settings + +if __name__ == '__main__': + connexion_app.run(host=settings.host, port=settings.port) diff --git a/src/openapi/openapi.yaml b/src/openapi/openapi.yaml new file mode 100644 index 0000000..108d40a --- /dev/null +++ b/src/openapi/openapi.yaml @@ -0,0 +1,35 @@ +openapi: "3.0.0" + +info: + title: PyBackendBoilerplate Microservice Boilerplate + version: "1.0" + description: Multi-file boilerplate for OpenAPI Specification. +servers: + - url: http://localhost:5000/v1.0 + +paths: + /: + $ref: "./paths/root.yaml" + /ping/: + $ref: "./paths/ping.yaml" + /hello/world/: + $ref: "./paths/hello/world/root.yaml" + /hello/world/greeting/: + $ref: "./paths/hello/world/greeting.yaml" + /hello/world/greeting/{name}: + $ref: "./paths/hello/world/personal.yaml" + +components: + parameters: + name: + $ref: "./parameters/path/name.yaml" + + schemas: + Error: + $ref: "./schemas/Error.yaml" + + responses: + UnexpectedError: + $ref: "./responses/UnexpectedError.yaml" + NullResponse: + $ref: "./responses/NullResponse.yaml" diff --git a/src/openapi/parameters/path/name.yaml b/src/openapi/parameters/path/name.yaml new file mode 100644 index 0000000..f4fd26e --- /dev/null +++ b/src/openapi/parameters/path/name.yaml @@ -0,0 +1,7 @@ +name: name +in: path +description: Name of the person to greet. +required: true +schema: + type: string + example: "dave" diff --git a/src/openapi/paths/hello/world/greeting.yaml b/src/openapi/paths/hello/world/greeting.yaml new file mode 100644 index 0000000..7bbf520 --- /dev/null +++ b/src/openapi/paths/hello/world/greeting.yaml @@ -0,0 +1,17 @@ +get: + summary: TBD + description: TBD. + x-openapi-router-controller: api.hello.world.greeting.greeting + operationId: greeting + responses: + "200": + description: Greeting. + content: + text/plain: + schema: + type: string + example: "Greetings! It's Sunday, 12 July, 2020 at 00:22:06" + "201": + $ref: "../../../openapi.yaml#/components/responses/NullResponse" + "default": + $ref: "../../../openapi.yaml#/components/responses/UnexpectedError" diff --git a/src/openapi/paths/hello/world/personal.yaml b/src/openapi/paths/hello/world/personal.yaml new file mode 100644 index 0000000..7c0f918 --- /dev/null +++ b/src/openapi/paths/hello/world/personal.yaml @@ -0,0 +1,19 @@ +post: + summary: TBD + description: TBD. + x-openapi-router-controller: api.hello.world.greeting.personal + operationId: personal_greeting + responses: + "200": + description: Personal greeting.. + content: + text/plain: + schema: + type: string + example: "Hello, dave! It's Sunday, 12 July, 2020 at 00:22:06" + "201": + $ref: "../../../openapi.yaml#/components/responses/NullResponse" + "default": + $ref: "../../../openapi.yaml#/components/responses/UnexpectedError" + parameters: + - $ref: "../../../openapi.yaml#/components/parameters/name" diff --git a/src/openapi/paths/hello/world/root.yaml b/src/openapi/paths/hello/world/root.yaml new file mode 100644 index 0000000..5095f0b --- /dev/null +++ b/src/openapi/paths/hello/world/root.yaml @@ -0,0 +1,17 @@ +get: + summary: TBD + description: TBD. + x-openapi-router-controller: api.hello.world.root + operationId: root + responses: + "200": + description: Hello, World! + content: + text/plain: + schema: + type: string + example: "Hello, World! It's Sunday, 12 July, 2020 at 00:22:06" + "201": + $ref: "../../../openapi.yaml#/components/responses/NullResponse" + "default": + $ref: "../../../openapi.yaml#/components/responses/UnexpectedError" diff --git a/src/openapi/paths/ping.yaml b/src/openapi/paths/ping.yaml new file mode 100644 index 0000000..d205fc5 --- /dev/null +++ b/src/openapi/paths/ping.yaml @@ -0,0 +1,17 @@ +get: + summary: TBD + description: TBD. + x-openapi-router-controller: api.sdk.common.ping + operationId: ping + responses: + "200": + description: Ping test + content: + text/plain: + schema: + type: string + example: "pong" + "201": + $ref: "../openapi.yaml#/components/responses/NullResponse" + "default": + $ref: "../openapi.yaml#/components/responses/UnexpectedError" diff --git a/src/openapi/paths/root.yaml b/src/openapi/paths/root.yaml new file mode 100644 index 0000000..8559125 --- /dev/null +++ b/src/openapi/paths/root.yaml @@ -0,0 +1,17 @@ +get: + summary: TBD + description: TBD. + x-openapi-router-controller: api.root + operationId: root + responses: + "200": + description: Hello world message + content: + text/plain: + schema: + type: string + example: "Hello, World! It's Sunday, 12 July, 2020 at 00:22:06" + "201": + $ref: "../openapi.yaml#/components/responses/NullResponse" + "default": + $ref: "../openapi.yaml#/components/responses/UnexpectedError" diff --git a/src/openapi/responses/NullResponse.yaml b/src/openapi/responses/NullResponse.yaml new file mode 100644 index 0000000..5840944 --- /dev/null +++ b/src/openapi/responses/NullResponse.yaml @@ -0,0 +1 @@ +description: Null response diff --git a/src/openapi/responses/UnexpectedError.yaml b/src/openapi/responses/UnexpectedError.yaml new file mode 100644 index 0000000..6b45b4a --- /dev/null +++ b/src/openapi/responses/UnexpectedError.yaml @@ -0,0 +1,5 @@ +description: Unexpected error has happened +content: + text\plain: + schema: + type: string diff --git a/src/openapi/schemas/Error.yaml b/src/openapi/schemas/Error.yaml new file mode 100644 index 0000000..2d87b74 --- /dev/null +++ b/src/openapi/schemas/Error.yaml @@ -0,0 +1,10 @@ +type: object +required: + - code + - message +properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/src/sdk b/src/sdk new file mode 120000 index 0000000..a97c12e --- /dev/null +++ b/src/sdk @@ -0,0 +1 @@ +../sdk/src/sdk/ \ No newline at end of file