Skip to main content

The gnarliest gear in the world 🤙

Project description

# Gnar Gear: Gnarly Python Apps

[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
[![codecov](https://codecov.io/gl/gnaar/gear/branch/master/graph/badge.svg?token=MRowdXaujg)](https://codecov.io/gl/gnaar/gear)
[![pipeline status](https://gitlab.com/gnaar/gear/badges/master/pipeline.svg)](https://gitlab.com/gnaar/gear/commits/master)
[![Python versions](https://img.shields.io/pypi/pyversions/gnar-gear.svg)](https://pypi.python.org/pypi/gnar-gear)
[![PyPI version](https://badge.fury.io/py/gnar-gear.svg)](https://badge.fury.io/py/gnar-gear)

Sets up a powerful Flask-based Python service with two lines of code:

``` python
from gnar_gear import GnarApp

...

GnarApp('my_gnarly_app', production=True, port=80).run()
```

## Installation

``` bash
pip3 install gnar-gear
```

## Feature List

- Flask app with auto blueprint registration
- [Bjoern WSGI Server](https://github.com/jonashaag/bjoern)
- Why Bjoern? Check out [these benchmarks!](https://blog.appdynamics.com/engineering/a-performance-analysis-of-python-wsgi-servers-part-2/)
- And also [these benchmarks](https://github.com/kubeup/python-wsgi-benchmark)
- Postgres database connection via [Postgres.py](https://postgres-py.readthedocs.io/en/latest/#tutorial)
- SES client connection via [Boto 3](https://boto3.readthedocs.io/en/latest/)
- JWT configuration via [Flask-JWT-Extended](http://flask-jwt-extended.readthedocs.io/en/latest/)
- [argon2_cffi.PasswordHasher](https://argon2-cffi.readthedocs.io) instance
- `Argon2` was the winner of the [2015 Password Hashing Competition](https://password-hashing.net)
- Logger configuration
- Error handler with traceback
- Overridable and extendable class-based design

## Requirements

### Bjoern

`Bjoern` requires `libev` (high performance event loop)
- Install `libev` with `brew install libev` on Mac, or find your platform-specific installation command [here](https://github.com/jonashaag/bjoern/wiki/Installation#libev)

### Application Structure

`GnarApp` expects to be instantiated in `main.py` at `<top-level-module>/app`, i.e. the minimum app folder structure is
```
+ <top-level-module>
+ app
main.py
__init__.py
```
It is recommended (not required) to place your apis in segregated folders under `app` and the tests in a `test` folder under the `<top-level-module>`, e.g.
```
+ <top-level-module>
+ app
+ admin
apis.py
constants.py
services.py
+ user
apis.py
constants.py
services.py
__init__.py
main.py
+ test
constants.py
test_app.py
__init__.py
```

### Blueprints

Each [Flask `Blueprint`](http://flask.pocoo.org/docs/1.0/blueprints/) must be assigned to a global-level `blueprint` variable in its module, e.g.

``` python
from flask import Blueprint, jsonify


api_name = 'user'
url_prefix = '/{}'.format(api_name)

blueprint = Blueprint(api_name, __name__, url_prefix=url_prefix)
^^^^^^^^^

@blueprint.route('/get', methods=['GET'])
def user_get():
return jsonify({'status': 'ok'})
```

By default, the `GnarApp` picks up every `blueprint` in an auto-scan of the application code.

## Overview

The `GnarApp` class provides a highly configurable, feature-rich, production-ready Flask-based app.

### Parameters

#### Args (required)

- *name*: The name of the application's top-level module
- *production*: Boolean flag indicating whether or not the build is in production mode
- *port*: The port to bind to the WSGI server

#### Kwargs (optional)

- *env_prefix*: Environment variable prefix (defaults to `GNAR`)
- *log_level*: Log level override - see [configure_logger](#configure_logger) for log level overview
- *blueprint_modules*: List of modules to find Flask blueprints (default is auto-scan)
- *no_db*: Boolean flag - specify `True` if the app does not need a Postgres connection
- *no_jwt*: Boolean flag - specify `True` if the app does not use JWT headers (i.e. non-api services)

### Overridable Behavior

`GnarApp.run` simply calls a set of steps in the class. Here is an example of how to override any of the steps:

``` python
def postconfig():
log.info('My Postconfig Step!')

ga = GnarApp('my_gnarly_app', production=True, port=80)
ga.postconfig = postconfig
ga.run()
```

### Run Steps
The run steps rely on a set of [environment variables](#environment-variables) which use a configurable prefix
(i.e. the `env_prefix` parameter). The default `env_prefix` is `GNAR`. An example of a Gnar environment variable
using a custom prefix is `GnarApp( ..., env_prefix='MY_APP')` and then instead of reading `GNAR_LOG_LEVEL`, the
`configure_logger` step will read `MY_APP_LOG_LEVEL`.

#### preconfig

- No default behavior - provided as an optional initial step in the app configuration.

#### configure_flask

- Attaches a `Flask` instance to the Gnar app.

#### configure_logger

- Attaches the root logger to `sys.stdout`.
- Sets the logging level to the first defined:
- `log_level` parameter
- `GNAR_<app name>_LOG_LEVEL` environment variable, e.g. `GNAR_MY_GNARLY_APP_LOG_LEVEL`
- `GNAR_LOG_LEVEL` environment variable
- `INFO`
- Reminder: [Valid settings](https://docs.python.org/2/library/logging.html#logging-levels) (in increasing order of severity) are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
- Sets the log format to the first defined:
- `GNAR_LOG_FORMAT`
- `'%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s'`, e.g.:
<pre>
2018-07-09 15:41:46.420 INFO gear.gnar_app:75 Logging at INFO
</pre>
- Sets the log format `default_msec_format` to the first defined:
- `GNAR_LOG_FORMAT_MSEC`
- `'%s.%03d'` (e.g. `.001`)

#### configure_argon2

- Attaches an `argon2_cffi.PasswordHasher` instance to the Gnar app.
- Reads the following environment variables `[TYPE: DEFAULT]` to [pass into the Argon2 instance](https://argon2-cffi.readthedocs.io/en/stable/api.html):
- `GNAR_ARGON2_TIME_COST`: `[INT: 2]` Number of iterations to perform
- `GNAR_ARGON2_MEMORY_COST`: `[INT: 512]` Amount of memory (in KB) to use
- `GNAR_ARGON2_PARALLELISM`: `[INT: 2]` Number of parallel threads (changes the resulting hash value)
- `GNAR_ARGON2_HASH_LEN`: `[INT: 16]` Length of the hash in bytes
- `GNAR_ARGON2_SALT_LEN`: `[INT: 16]` Length of random salt to be generated for each password in bytes
- `GNAR_ARGON2_ENCODING`: `[STR: 'utf-8']` Encoding to use when a string is passed into `hash` or `verify`
- Note [from the docs](https://argon2-cffi.readthedocs.io/en/stable/parameters.html):
> Only tweak these if you’ve determined using CLI that these defaults are too slow or too fast for your use case.
- To hash a password using `Argon2`:
``` python
from <top-level-module>.main import app
hash = app.generate_password_hash(<plain text password>)
# OR
hash = app.argon2.hash(<plain text password>)
```
Note that this creates a [randomly salted, memory-hard hash using the Argon2i algorithm](https://argon2-cffi.readthedocs.io/en/stable/parameters.html).
- To validate a password with `Argon2`:
``` python
from <top-level-module>.main import app
is_valid = app.check_password_hash(<password hash from database>, <plain text password>)
# OR
is_valid = app.argon2.verify(<password hash from database>, <plain text password>)
```
Note that `app.argon2.verify` [raises an exception](https://argon2-cffi.readthedocs.io/en/stable/faq.html) if the password is invalid whereas `app.check_password_hash` does not.

#### configure_database

- Creates a Postgres database connection and attaches it to the Gnar app
- Reads the following environment variables to set the `host`, `dbname`, `user`, `password` connection string parameters, respectively:
- `GNAR_PG_ENDPOINT`
- `GNAR_PG_DATABASE`
- `GNAR_PG_USERNAME`
- `GNAR_PG_PASSWORD`
- Note: The [Postgres API](https://postgres-py.readthedocs.io/en/latest/#tutorial) primarily consists of `run`, `one`, and `all`

#### attach_instance

- Attaches the `GnarApp` instance to the `app.main` module. This enables easy access to the Gnar app from anywhere in the application using
``` python
from <top-level-module>.main import app
```
- The `GnarApp`'s runtime assets are `db`, `argon2`, `flask`, `check_password_hash`, `generate_password_hash`, and `get_ses_client`
- For example, to fetch one result (or `None`) from the database:
``` python
app.db.one("SELECT * FROM foo WHERE bar='buz'")
```

#### configure_blueprints

- By default, `GnarApp` [auto-scans every Python module](#blueprints) under the `app` folder for blueprints.
- Each [Flask `Blueprint`](http://flask.pocoo.org/docs/1.0/blueprints/) must be assigned to a global-level `blueprint` variable in its module.
- If you prefer to skip the auto-scan, you can provide a list (or single string) of blueprint modules.
- Each item in the list of module names may use one of two formats:
- Without a `.` in the module name: `GnarApp` will look for the module in `<top-level-module>.app.<module name>.apis`
- With a `.` in the module: `GnarApp` will look for the module in `<top-level-module>.app.<module name>`

#### configure_errorhandler

- Defines a generic (Exception-level) Flask error handler which:
- Logs the error message and its `traceback` (format_exec)
- Returns a 200-level json response containing `{"error": <error message>, "traceback": <traceback>}`

#### configure_jwt

- Sets the Flask `JWT_SECRET_KEY` variable to the value of the `GNAR_JWT_SECRET_KEY` environment variable.
- Sets the Flask `JWT_ACCESS_TOKEN_EXPIRES` variable to the value of the `GNAR_JWT_ACCESS_TOKEN_EXPIRES_MINUTES` environment variable (default 15 mins).
- Attaches a [JWTManager](http://flask-jwt-extended.readthedocs.io/en/latest/_modules/flask_jwt_extended/jwt_manager.html#JWTManager) instance to the `GnarApp`.
- Defines functions for `expired_token_loader`, `invalid_token_loader`, and `unauthorized_loader` which return meaningful error messages as 200-level json responses containing `{"error": <error message>}`.

#### configure_after_request

- Adds a JWT Authorization header (Bearer token) to responses which received a valid JWT token in the request.
- In non-production mode, adds CORS headers to the response (so that you don't need to bother with circumventing CORS in development).

#### postconfig

- No default behavior - provided as an optional initial step in the app configuration.

### Runtime Functionality

#### generate_password_hash / check_password_hash

- Convenience wrappers for `app.argon2.hash` and `app.argon2.verify`
- Usage:
``` python
from <top-level-module>.main import app
hash = app.generate_password_hash(<plain text password>)
is_valid = app.check_password_hash(<password hash from database>, <plain text password>)
```

#### get_ses_client

- Exposed as runtime functionality (as opposed to creating the client at initialization) because AWS will close the client after a short period of time
- Returns an SES connection (using boto3)
- Reads the following environment variables to set the `region_name`, `aws_access_key_id`, and `aws_secret_access_key` parameters of the `boto3.client` call, respectively:
- `GNAR_SES_REGION_NAME`
- `GNAR_SES_ACCESS_KEY_ID`
- `GNAR_SES_SECRET_ACCESS_KEY`
- Usage:
``` python
from <top-level-module>.main import app
app.get_ses_client().send_email( ... )
```
- See the [`Boto 3 Docs`](https://boto3.readthedocs.io/en/latest/reference/services/ses.html#SES.Client.send_email) for the `send_email` request syntax.

### Environment Variables

- The environment variables (with configurable prefix) used by `GnarApp` are:
- `GNAR_ARGON2_ENCODING`
- `GNAR_ARGON2_HASH_LEN`
- `GNAR_ARGON2_MEMORY_COST`
- `GNAR_ARGON2_PARALLELISM`
- `GNAR_ARGON2_SALT_LEN`
- `GNAR_ARGON2_TIME_COST`
- `GNAR_JWT_SECRET_KEY`
- `GNAR_LOG_LEVEL`
- `GNAR_PG_DATABASE`
- `GNAR_PG_ENDPOINT`
- `GNAR_PG_PASSWORD`
- `GNAR_PG_USERNAME`
- `GNAR_SES_ACCESS_KEY_ID`
- `GNAR_SES_REGION_NAME`
- `GNAR_SES_SECRET_ACCESS_KEY`
- See the relevant sections above for details

---
<div align="center">Made with ❤ by a Canadian living in Redwood City, California | Keep it Rad, friends 🤙</div>

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gnar-gear-1.0.3.tar.gz (10.2 kB view hashes)

Uploaded Source

Built Distribution

gnar_gear-1.0.3-py3-none-any.whl (9.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page