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>
[![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
Release history Release notifications | RSS feed
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)
Built Distribution
Close
Hashes for gnar_gear-1.0.3-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | c467f22841157658e3e3914c6918e634023d23abcc2ec792b4fedc9efbba18e0 |
|
MD5 | 34f02479c64592472613707659641ea5 |
|
BLAKE2b-256 | 0659d8acbff3b1b73a743f4bd8be15c3525a6a35e1635dae78d9edde7c10f56e |