{ "info": { "author": "Brien R. Givens", "author_email": "ic3b3rg@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "# Gnar Gear: Gnarly Python Apps\n\n[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)\n[![codecov](https://codecov.io/gl/gnaar/gear/branch/master/graph/badge.svg?token=MRowdXaujg)](https://codecov.io/gl/gnaar/gear)\n[![pipeline status](https://gitlab.com/gnaar/gear/badges/master/pipeline.svg)](https://gitlab.com/gnaar/gear/commits/master)\n[![Python versions](https://img.shields.io/pypi/pyversions/gnar-gear.svg)](https://pypi.python.org/pypi/gnar-gear)\n[![PyPI version](https://badge.fury.io/py/gnar-gear.svg)](https://badge.fury.io/py/gnar-gear)\n\n**Part of Project Gnar:**  [base](https://hub.docker.com/r/gnar/base)  \u2022  [gear](https://pypi.org/project/gnar-gear)  \u2022  [piste](https://gitlab.com/gnaar/piste)  \u2022  [off-piste](https://gitlab.com/gnaar/off-piste)  \u2022  [edge](https://www.npmjs.com/package/gnar-edge)  \u2022  [powder](https://gitlab.com/gnaar/powder)  \u2022  [genesis](https://gitlab.com/gnaar/genesis)  \u2022  [patrol](https://gitlab.com/gnaar/patrol)\n\n**Get started with Project Gnar on**  [![Project Gnar on Medium](https://s3-us-west-2.amazonaws.com/project-gnar/medium-68x20.png)](https://medium.com/@ic3b3rg/project-gnar-d274165793b6)\n\n**Join Project Gnar on**  [![Project Gnar on Slack](https://s3-us-west-2.amazonaws.com/project-gnar/slack-69x20.png)](https://join.slack.com/t/project-gnar/shared_invite/enQtNDM1NzExNjY0NjkzLWQ3ZTQyYjgwMjkzNWYxNDJiNTQzODY0ODRiMmZiZjVkYzYyZWRkOWQzNjA0OTk3NWViNWM5YTZkMGJlOGIzOWE)\n\n**Support Project Gnar on**  [![Project Gnar on Patreon](https://s3-us-west-2.amazonaws.com/project-gnar/patreon-85x12.png)](https://patreon.com/project_gnar)\n\nGnar Gear sets up a powerful Flask-based Python service with two lines of code:\n\n```python\nfrom gnar_gear import GnarApp\n\n...\n\nGnarApp('my_gnarly_app', production=True, port=80).run()\n```\n\n## Installation\n\n```bash\npip3 install gnar-gear\n```\n\n## Feature List\n\n- Flask app with auto blueprint registration\n- [Flask WSGI Development Server](http://flask.pocoo.org/docs/1.0/server)\n- [Bjoern WSGI Production Server](https://github.com/jonashaag/bjoern)\n - Why Bjoern? Check out [these benchmarks!](https://blog.appdynamics.com/engineering/a-performance-analysis-of-python-wsgi-servers-part-2/)\n - And also [these benchmarks](https://github.com/kubeup/python-wsgi-benchmark)\n- Postgres database connection via [Postgres.py](https://postgres-py.readthedocs.io/en/latest/#tutorial)\n- SES client connection via [Boto 3](https://boto3.readthedocs.io/en/latest/)\n- JWT configuration via [Flask-JWT-Extended](http://flask-jwt-extended.readthedocs.io/en/latest/)\n- Peer requests - HTTP requests to other microservices in the app\n- External requests - Convenience wrapper around [requests](http://docs.python-requests.org)\n- SQS Message Polling and Sending\n- [argon2_cffi.PasswordHasher](https://argon2-cffi.readthedocs.io) instance\n - `Argon2` was the winner of the [2015 Password Hashing Competition](https://password-hashing.net)\n- Logger configuration\n- Error handler with traceback\n- Overridable and extendable class-based design\n\n#### Development Mode\n\n- Flask WSGI server in fault-tolerant debug (watch & reload) mode\n - The module loader logs the stack trace of any modules that fail to load\n - If a module fails to load, the app continues to watch for file changes\n- CORS \"disabled\" (`Access-Control` response headers set), so you don't need to use other means to circumvent CORS\n\n#### Production Mode\n\n- Bjoern WSGI server\n- CORS \"enabled\" (`Access-Control` response headers are not set)\n\n## Requirements\n\n### Bjoern\n\n`Bjoern` requires `libev` (high performance event loop)\n- Install `libev` with `brew install libev` on Mac, or find your platform-specific installation command [here](https://github.com/jonashaag/bjoern/wiki/Installation#libev)\n\n### Application Structure\n\n`GnarApp` expects to be instantiated in `main.py` at `/app`, i.e. the minimum app folder structure is\n\n```\n+ \n + app\n main.py\n __init__.py\n```\nIt is recommended (not required) to place your apis in segregated folders under `app` and the tests in a `test` folder under the ``, e.g.\n\n```\n+ \n + app\n + admin\n apis.py\n constants.py\n services.py\n + user\n apis.py\n constants.py\n services.py\n __init__.py\n main.py\n + test\n constants.py\n test_app.py\n __init__.py\n```\n\n### Blueprints\n\nEach [Flask `Blueprint`](http://flask.pocoo.org/docs/1.0/blueprints/) must be assigned to a global-level `blueprint` variable in its module, e.g.\n\n```python\nfrom flask import Blueprint, jsonify\n\n\napi_name = 'user'\nurl_prefix = '/{}'.format(api_name)\n\nblueprint = Blueprint(api_name, __name__, url_prefix=url_prefix)\n^^^^^^^^^\n\n@blueprint.route('/get', methods=['GET'])\ndef user_get():\n return jsonify({'status': 'ok'})\n```\n\nBy default, the `GnarApp` picks up every `blueprint` in an auto-scan of the application code.\n\n## Overview\n\nThe `GnarApp` class provides a highly configurable, feature-rich, production-ready Flask-based app.\n\n### Parameters\n\n#### Args (required)\n\n- **name**: The name of the application's top-level module\n- **production**: Boolean flag indicating whether or not the build is in production mode\n- **port**: The port to bind to the WSGI server\n\n#### Kwargs (optional)\n\n- **env_prefix**: Environment variable prefix (defaults to `GNAR`)\n- **log_level**: Log level override\n - see [configure_logger](#configure_logger) for log level overview\n- **blueprint_modules**: List of modules to find Flask blueprints (default is auto-scan)\n - see [configure_blueprints](#configure_blueprints) for blueprints overview\n- **sqs**: List (or single dict) of SQS queues to poll\n - see [configure_sqs_polls](#configure_sqs_polls) for SQS polling overview\n- **no_db**: Boolean flag - specify `True` if the app does not need a Postgres connection\n- **no_jwt**: Boolean flag - specify `True` if the app does not use JWT headers (i.e. non-api services)\n\n### Overridable Behavior\n\n`GnarApp.run` simply calls a set of steps in the class. Here is an example of how to override any of the steps:\n\n```python\ndef postconfig():\n log.info('My Postconfig Step!')\n\nga = GnarApp('my_gnarly_app', production=True, port=80)\nga.postconfig = postconfig\nga.run()\n```\n\n### Run Steps\nThe run steps rely on a set of [environment variables](#environment-variables) which use a configurable prefix\n(i.e. the `env_prefix` parameter). The default `env_prefix` is `GNAR`. An example of a Gnar environment variable\nusing a custom prefix is `GnarApp( ..., env_prefix='MY_APP')` and then instead of reading `GNAR_LOG_LEVEL`, the\n`configure_logger` step will read `MY_APP_LOG_LEVEL`.\n\n#### preconfig\n\n- No default behavior - provided as an optional initial step in the app configuration.\n\n#### configure_flask\n\n- Attaches a `Flask` instance to the Gnar app.\n\n#### configure_logger\n\n- Attaches the root logger to `sys.stdout`.\n- Sets the logging level to the first defined:\n - `log_level` parameter\n - `GNAR__LOG_LEVEL` environment variable, e.g. `GNAR_MY_GNARLY_APP_LOG_LEVEL`\n - `GNAR_LOG_LEVEL` environment variable\n - `INFO`\n - Reminder: [Valid settings](https://docs.python.org/2/library/logging.html#logging-levels) (in increasing order of severity) are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`\n- Sets the log format to the first defined:\n - `GNAR_LOG_FORMAT`\n - `'%(asctime)s %(levelname)-8s %(name)s:%(lineno)d %(message)s'`, e.g.:\n
\n  2018-07-09 15:41:46.420 INFO     gear.gnar_app:75   Logging at INFO\n  
\n- Sets the log format `default_msec_format` to the first defined:\n - `GNAR_LOG_FORMAT_MSEC`\n - `'%s.%03d'` (e.g. `.001`)\n\n#### configure_argon2\n\n- Attaches an `argon2_cffi.PasswordHasher` instance to the Gnar app.\n- Reads the following environment variables `[TYPE: DEFAULT]` to [pass into the Argon2 instance](https://argon2-cffi.readthedocs.io/en/stable/api.html):\n - `GNAR_ARGON2_TIME_COST`: `[INT: 2]` Number of iterations to perform\n - `GNAR_ARGON2_MEMORY_COST`: `[INT: 512]` Amount of memory (in KB) to use\n - `GNAR_ARGON2_PARALLELISM`: `[INT: 2]` Number of parallel threads (changes the resulting hash value)\n - `GNAR_ARGON2_HASH_LEN`: `[INT: 16]` Length of the hash in bytes\n - `GNAR_ARGON2_SALT_LEN`: `[INT: 16]` Length of random salt to be generated for each password in bytes\n - `GNAR_ARGON2_ENCODING`: `[STR: 'utf-8']` Encoding to use when a string is passed into `hash` or `verify`\n- Note [from the docs](https://argon2-cffi.readthedocs.io/en/stable/parameters.html):\n > Only tweak these if you\u2019ve determined using CLI that these defaults are too slow or too fast for your use case.\n- To hash a password using `Argon2`:\n\n ```python\n from .main import app\n hash = app.generate_password_hash()\n # OR\n hash = app.argon2.hash()\n ```\n\n Note that this creates a [randomly salted, memory-hard hash using the Argon2i algorithm](https://argon2-cffi.readthedocs.io/en/stable/parameters.html).\n- To validate a password with `Argon2`:\n\n ```python\n from .main import app\n is_valid = app.check_password_hash(, )\n # OR\n is_valid = app.argon2.verify(, )\n ```\n\n 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.\n\n#### configure_database\n\n- Creates a Postgres database connection and attaches it to the Gnar app\n- Reads the following environment variables to set the `host`, `dbname`, `user`, `password` connection string parameters, respectively:\n - `GNAR_PG_ENDPOINT`\n - `GNAR_PG_DATABASE`\n - `GNAR_PG_USERNAME`\n - `GNAR_PG_PASSWORD`\n- Note: The [Postgres API](https://postgres-py.readthedocs.io/en/latest/#tutorial) primarily consists of `run`, `one`, and `all`\n\n#### attach_instance\n\n- Attaches the `GnarApp` instance to the `app.main` module. This enables easy access to the Gnar app from anywhere in the application using\n\n ```python\n from .main import app\n ```\n- The `GnarApp`'s runtime assets are `db`, `argon2`, `flask`, `check_password_hash`, `generate_password_hash`, and `get_ses_client`\n- For example, to fetch one result (or `None`) from the database:\n\n ```python\n app.db.one(\"SELECT * FROM foo WHERE bar='buz'\")\n ```\n\n#### configure_blueprints\n\n- By default, `GnarApp` [auto-scans every Python module](#blueprints) under the `app` folder for blueprints.\n- Each [Flask `Blueprint`](http://flask.pocoo.org/docs/1.0/blueprints/) must be assigned to a global-level `blueprint` variable in its module.\n- If you prefer to skip the auto-scan, you can provide a list (or single string) of blueprint modules.\n - Each item in the list of module names may use one of two formats:\n - Without a `.` in the module name: `GnarApp` will look for the module in `.app..apis`\n - With a `.` in the module: `GnarApp` will look for the module in `.app.`\n\n#### configure_errorhandler\n\n- Defines a generic (Exception-level) Flask error handler which:\n - Logs the error message and its `traceback` (format_exec)\n - Returns a 200-level json response containing `{\"error\": , \"traceback\": }`\n\n#### configure_jwt\n\n- Sets the Flask `JWT_SECRET_KEY` variable to the value of the `GNAR_JWT_SECRET_KEY` environment variable.\n- Sets the Flask `JWT_ACCESS_TOKEN_EXPIRES` variable to the value of the `GNAR_JWT_ACCESS_TOKEN_EXPIRES_MINUTES` environment variable (default 15 mins).\n- Attaches a [JWTManager](http://flask-jwt-extended.readthedocs.io/en/latest/_modules/flask_jwt_extended/jwt_manager.html#JWTManager) instance to the `GnarApp`.\n- Defines functions for `expired_token_loader`, `invalid_token_loader`, and `unauthorized_loader` which return meaningful error messages as 200-level json responses containing `{\"error\": }`.\n\n#### configure_sqs_polls\n\n- Configures a set of polls to run at specified intervals to receive messages from SQS queues.\n- Specified via the `sqs` kwarg as a list of dicts (or a single dict) with a set of required (\\*) and optional properties. See the [AWS ReceiveMessage docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html) for more details on the optional properties:\n - \\* **queue_name**: The name of the SQS queue - the queue URL is retrieved from AWS with this name\n - \\* **callback**: The function to call with each received message. The message will be deleted from the queue unless the callback function returns `False`.\n - **interval_seconds**: The interval (in seconds) between receive message requests - default 60\n - **attribute_names**: A list of attributes to return with each message\n - **max_number_of_messages**: The maximum number of messages to return with each request - 1 to 10, default 1\n - **message_attribute_names**: Metadata to include with the message - [more details](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-attributes.html)\n - **receive_request_attempt_id**: The token used for deduplication of ReceiveMessage calls (FIFO queues only)\n - **visibility_timeout**: The duration (in seconds) that the received messages are hidden from subsequent retrieve requests\n - **wait_time_seconds**: The duration (in seconds) for which the call waits for a message to arrive in the queue before returning\n- Each message received by the callback includes the following properties. See the [AWS Message docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_Message.html) for more details on these properties:\n - **Body**: Message contents\n - **Attributes**: Map of requested attributes\n - **MD5OfBody**: MD5 Digest of the message body\n - **MD5OfMessageAttributes**: MD5 digest of the message attribute string\n - **MessageAttributes**: Map of custom message metadata\n- Notes:\n - Messages are processed sequentially in the order they are received\n - The `callback` blocks message processing until it's complete\n - After all received messages are processed, the queue is immediately checked for new messages\n - Once the `receive_message` call returns no messages, a [`Timer`](https://docs.python.org/3.7/library/threading.html#timer-objects) with the specified `interval_seconds` starts the next loop\n- 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:\n - `GNAR_SQS_REGION_NAME`\n - `GNAR_SQS_ACCESS_KEY_ID`\n - `GNAR_SQS_SECRET_ACCESS_KEY`\n- Example:\n\n ```python\n def receive_sqs_message(message):\n redis_connection.set(message['MessageId'], message['Body'], 3600)\n\n sqs = {'queue_name': 'gnar-queue', 'callback': receive_sqs_message}\n GnarApp('piste', production, port, sqs=sqs).run()\n ```\n\n#### configure_after_request\n\n- Adds a JWT Authorization header (Bearer token) to responses which received a valid JWT token in the request.\n- In development mode, adds CORS headers to the response (so that you don't need to bother circumventing CORS).\n\n#### postconfig\n\n- No default behavior - provided as an optional initial step in the app configuration.\n\n### Runtime Functionality\n\n#### peer\n\n- Handles requests to other microservices in a Kubernetes app.\n- Usage:\n\n ```python\n from .main import app\n peer = app.peer(<< service name >>)\n ```\n\n- Requires environment variables in the form:\n - production: `<< SERVICE >>_SERVICE_PORT`, e.g. `PISTE_SERVICE_PORT`\n - development: `GNAR_<< SERVICE >>_SERVICE_PORT`, e.g. `GNAR_PISTE_SERVICE_PORT`\n- In production, the environment variable is automatically set by Kubernetes\n- In development, the environment variable should point to another Gnar Gear app on localhost on a separate port\n- The environment variable value must be in the form `<< ip address >>:<< port >>`, e.g.\n\n ```bash\n export GNAR_PISTE_SERVICE_PORT='127.0.0.1:9401'\n ```\n\n##### peer Methods\n\n- Each peer method accepts all the `kwargs` of the underlying `requests` method in addition to an `auto_auth` property.\n - If `auto_auth` is `True` and the current route is protected, an Authorization header is added to the request.\n - Both microservices must use the same `GNAR_JWT_SECRET_KEY` for the authorization to work.\n- Uses [requests](http://docs.python-requests.org/en/master/api/#main-interface) under the hood:\n - [**request**](http://docs.python-requests.org/en/master/api/#requests.request)(method, path, \\*\\*kwargs): Constructs and sends a request\n - [**head**](http://docs.python-requests.org/en/master/api/#requests.head)(path, \\*\\*kwargs): Sends a HEAD request\n - [**get**](http://docs.python-requests.org/en/master/api/#requests.get)(path, params=None, \\*\\*kwargs): Sends a GET request\n - [**post**](http://docs.python-requests.org/en/master/api/#requests.post)(path, data=None, json=None, \\*\\*kwargs): Sends a POST request\n - [**put**](http://docs.python-requests.org/en/master/api/#requests.put)(path, data=None, \\*\\*kwargs): Sends a PUT request\n - [**patch**](http://docs.python-requests.org/en/master/api/#requests.patch)(path, data=None, \\*\\*kwargs): Sends a PATCH request\n - [**delete**](http://docs.python-requests.org/en/master/api/#requests.delete)(path, \\*\\*kwargs): Sends a DELETE request\n- Example:\n\n ```python\n from .main import app\n response = app.peer('piste').post('login', {'email': 'ic3b3rg@gmail.com'})\n ```\n\n#### external\n\n- Convenience wrapper around [requests](http://docs.python-requests.org)\n- Example:\n\n ```python\n from .main import app\n response = app.external.get('https://api.pwnedpasswords.com/range/5ce7d')\n ```\n\n#### generate_password_hash / check_password_hash\n\n- Convenience wrappers for `app.argon2.hash` and `app.argon2.verify`\n- Usage:\n\n ```python\n from .main import app\n hash = app.generate_password_hash()\n is_valid = app.check_password_hash(, )\n ```\n\n#### get_ses_client\n\n- Exposed as runtime functionality (as opposed to creating the client at initialization) because AWS will close the client after a short period of time\n- Returns an SES connection (using boto3)\n- 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:\n - `GNAR_SES_REGION_NAME`\n - `GNAR_SES_ACCESS_KEY_ID`\n - `GNAR_SES_SECRET_ACCESS_KEY`\n- Usage:\n\n ```python\n from .main import app\n app.get_ses_client().send_email( ... )\n ```\n- 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.\n\n#### send_sqs_message\n\n- Sends a message to an SQS queue.\n- Args (required):\n - **queue_name**: The name of the SQS queue - the queue URL is retrieved from AWS with this name\n - **message_body**: Message contents\n- Kwargs (optional) - See the [AWS SendMessage docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html) for more details on these properties:\n - **delay_seconds**: The length of time, in seconds, for which to delay a specific message\n - **message_attributes**: Metadata to include with the message\n - **message_deduplication_id**: The token used for deduplication of sent messages (FIFO queues only)\n - **message_group_id**: The tag that specifies that a message belongs to a specific message group (FIFO queues only)\n- Returns a dict with the following attributes:\n - **MessageId**: Unique message identifier\n - **MD5OfMessageAttributes**: MD5 digest of the message attribute string\n - **MD5OfMessageBody**: MD5 digest of the message body\n - **SequenceNumber**: Unique, non-consecutive, 128 bit string (FIFO queues only)\n- 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:\n - `GNAR_SQS_REGION_NAME`\n - `GNAR_SQS_ACCESS_KEY_ID`\n - `GNAR_SQS_SECRET_ACCESS_KEY`\n- Example:\n\n ```python\n from .main import app\n message = app.send_sqs_message('gnar-queue', 'The gnarliest gear in the world \ud83e\udd19')\n ```\n\n### Environment Variables\n\n- The environment variables (with configurable prefix) used by `GnarApp` are:\n - `GNAR_ARGON2_ENCODING`\n - `GNAR_ARGON2_HASH_LEN`\n - `GNAR_ARGON2_MEMORY_COST`\n - `GNAR_ARGON2_PARALLELISM`\n - `GNAR_ARGON2_SALT_LEN`\n - `GNAR_ARGON2_TIME_COST`\n - `GNAR_JWT_SECRET_KEY`\n - `GNAR_LOG_LEVEL`\n - `GNAR_PG_DATABASE`\n - `GNAR_PG_ENDPOINT`\n - `GNAR_PG_PASSWORD`\n - `GNAR_PG_USERNAME`\n - `GNAR_SES_ACCESS_KEY_ID`\n - `GNAR_SES_REGION_NAME`\n - `GNAR_SES_SECRET_ACCESS_KEY`\n - `GNAR_SQS_REGION_NAME`\n - `GNAR_SQS_ACCESS_KEY_ID`\n - `GNAR_SQS_SECRET_ACCESS_KEY`\n- See the relevant sections above for details\n\n---\n\n

Made with Love by Brien Givens

\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://gitlab.com/gnaar/gear", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "gnar-gear", "package_url": "https://pypi.org/project/gnar-gear/", "platform": "", "project_url": "https://pypi.org/project/gnar-gear/", "project_urls": { "Homepage": "https://gitlab.com/gnaar/gear" }, "release_url": "https://pypi.org/project/gnar-gear/1.1.5/", "requires_dist": [ "argon2-cffi (>=18.3.0)", "boto3 (>=1.9.75)", "bjoern (>=2.2.3)", "flask (>=1.0.2)", "flask-jwt-extended (>=3.15.0)", "postgres (>=2.2.2)", "requests (>=2.21.0)" ], "requires_python": "", "summary": "The gnarliest gear in the world \ud83e\udd19", "version": "1.1.5" }, "last_serial": 4674835, "releases": { "1.0.1": [ { "comment_text": "", "digests": { "md5": "eb2bbfeacc75094e63c222843fe8ad12", "sha256": "76fa95d90e266dde5540fe86a42ee9987699eb1b0fc4ce281ddb09e9a2ee6741" }, "downloads": -1, "filename": "gnar_gear-1.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "eb2bbfeacc75094e63c222843fe8ad12", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8081, "upload_time": "2018-07-11T10:38:16", "url": "https://files.pythonhosted.org/packages/1c/05/ff9b918f8a7277249090bb3d87fe28ad0fb9c765e15b785f041651416008/gnar_gear-1.0.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d1e2b2c33b9d1e08761735d0a4870999", "sha256": "97636f6958f5fb0aa6ce82bfd77b0cb12f0e95c85f77b8808f0be623cd68a6df" }, "downloads": -1, "filename": "gnar-gear-1.0.1.tar.gz", "has_sig": false, "md5_digest": "d1e2b2c33b9d1e08761735d0a4870999", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9005, "upload_time": "2018-07-11T10:38:18", "url": "https://files.pythonhosted.org/packages/9a/c4/65f77a8cca40880751ea9a2745deca196a13ebab54fb31a3e0b2da8a4e8d/gnar-gear-1.0.1.tar.gz" } ], "1.0.2": [ { "comment_text": "", "digests": { "md5": "eb1377710aea3a8b1d78d32525f11435", "sha256": "4fd833de19789dcb6f3db884b79f07cb77a5b3148c932f0befafdc64f6835269" }, "downloads": -1, "filename": "gnar_gear-1.0.2-py3-none-any.whl", "has_sig": false, "md5_digest": "eb1377710aea3a8b1d78d32525f11435", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8132, "upload_time": "2018-07-12T02:23:36", "url": "https://files.pythonhosted.org/packages/f0/f0/9341db151d72b634a2aea37afca7cf1989e9afed35be53319e7437908be0/gnar_gear-1.0.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d44321a276e7254140336f16da389cc0", "sha256": "97cea16d0ee5d6c661ad933112d2a268c08b61358f7c35e1ae1ed043b73d9fa9" }, "downloads": -1, "filename": "gnar-gear-1.0.2.tar.gz", "has_sig": false, "md5_digest": "d44321a276e7254140336f16da389cc0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9050, "upload_time": "2018-07-12T02:23:38", "url": "https://files.pythonhosted.org/packages/38/9c/59c533ab8e3358827c45bb3f80bcbc823af525516a7f09635193d34b1c0d/gnar-gear-1.0.2.tar.gz" } ], "1.0.3": [ { "comment_text": "", "digests": { "md5": "34f02479c64592472613707659641ea5", "sha256": "c467f22841157658e3e3914c6918e634023d23abcc2ec792b4fedc9efbba18e0" }, "downloads": -1, "filename": "gnar_gear-1.0.3-py3-none-any.whl", "has_sig": false, "md5_digest": "34f02479c64592472613707659641ea5", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9112, "upload_time": "2018-07-18T07:23:35", "url": "https://files.pythonhosted.org/packages/06/59/d8acbff3b1b73a743f4bd8be15c3525a6a35e1635dae78d9edde7c10f56e/gnar_gear-1.0.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "cd8be85fee6cca2811a20957d0ff367b", "sha256": "325d83fcbb11fea525ed73d37772d42702f464bbd79f700fae12aaa9f743136e" }, "downloads": -1, "filename": "gnar-gear-1.0.3.tar.gz", "has_sig": false, "md5_digest": "cd8be85fee6cca2811a20957d0ff367b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10224, "upload_time": "2018-07-18T07:11:09", "url": "https://files.pythonhosted.org/packages/58/74/dce3659e5e19bb1dad66a77fa3fa0310cad12a46032380d9c4e7c71ee415/gnar-gear-1.0.3.tar.gz" } ], "1.0.4": [ { "comment_text": "", "digests": { "md5": "2d3788b910f7c864bb4e4da67127bfb2", "sha256": "c0b05dbecca6af39606ac6655c981489b95c7e362d82fd6fbd21d5bc7de8221b" }, "downloads": -1, "filename": "gnar_gear-1.0.4-py3-none-any.whl", "has_sig": false, "md5_digest": "2d3788b910f7c864bb4e4da67127bfb2", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9111, "upload_time": "2018-07-18T07:50:17", "url": "https://files.pythonhosted.org/packages/19/c6/025505c79255af21df76b78f48dbc3be1d7e63193ed73e53a2b9460fe402/gnar_gear-1.0.4-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9ae411f57d82395ead2cc20e7906f571", "sha256": "c043ab4fd7bff8669a667adb03e6d2c86273be82ab9f8c1c5ddbdbf6a2b93b88" }, "downloads": -1, "filename": "gnar-gear-1.0.4.tar.gz", "has_sig": false, "md5_digest": "9ae411f57d82395ead2cc20e7906f571", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10228, "upload_time": "2018-07-18T07:50:19", "url": "https://files.pythonhosted.org/packages/28/bd/ffcba4be49e76c72d5be9025bc7e5ead98fb57dbc52a243023d37642271a/gnar-gear-1.0.4.tar.gz" } ], "1.0.5": [ { "comment_text": "", "digests": { "md5": "8abcfa61799e7529d4b3cec653989d7b", "sha256": "b6be640c03a4e23ed7fbf3d948627c48894ae7093cd96d03b8ee768964169b01" }, "downloads": -1, "filename": "gnar_gear-1.0.5-py3-none-any.whl", "has_sig": false, "md5_digest": "8abcfa61799e7529d4b3cec653989d7b", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9248, "upload_time": "2018-07-26T23:08:51", "url": "https://files.pythonhosted.org/packages/42/d7/04ca1eda1922c7f69ff078d090603c95f37446e03fec823ec886530fb99e/gnar_gear-1.0.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "4c303affe0732b85c95a0734873728d5", "sha256": "24610ed0a7fcf48d7dd63cc5d8ee78ec12140236ff28dd956c92bef5ead426c4" }, "downloads": -1, "filename": "gnar-gear-1.0.5.tar.gz", "has_sig": false, "md5_digest": "4c303affe0732b85c95a0734873728d5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10367, "upload_time": "2018-07-26T23:08:52", "url": "https://files.pythonhosted.org/packages/2c/1f/94baf4adf1c21fd57afd197c8415ea45f3c94ad1cecfdd7aad67a9c0dd47/gnar-gear-1.0.5.tar.gz" } ], "1.0.6": [ { "comment_text": "", "digests": { "md5": "c7f0ce5635eebdbd63196b827011ea1d", "sha256": "49a5a0a709b3de165d5bc855f232afb93c14f29b579ad92ef3e5c7fe9f608a91" }, "downloads": -1, "filename": "gnar_gear-1.0.6-py3-none-any.whl", "has_sig": false, "md5_digest": "c7f0ce5635eebdbd63196b827011ea1d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9247, "upload_time": "2018-07-31T09:54:49", "url": "https://files.pythonhosted.org/packages/d5/30/7afdcedefc406b771cf96cc5fe7e2101e5c6508ab60db64df9aae53a650c/gnar_gear-1.0.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "e140ea179133507fcd9920b32796a526", "sha256": "abb3f82661d318fc2d8c086bbae8176d707047aff61d8fe155b0b05742edd9f0" }, "downloads": -1, "filename": "gnar-gear-1.0.6.tar.gz", "has_sig": false, "md5_digest": "e140ea179133507fcd9920b32796a526", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10376, "upload_time": "2018-07-31T09:54:51", "url": "https://files.pythonhosted.org/packages/8e/88/1f5e978886afa036a2ee5fc0a02a067f55498415f06f4ca24b89561af573/gnar-gear-1.0.6.tar.gz" } ], "1.0.7": [ { "comment_text": "", "digests": { "md5": "c9c11410183d1538fc67ab3c2ef6cca9", "sha256": "3702d8e5040e5bab8132311d1e104cf3ae63de8b0d726095f227e1bd5c2a73b0" }, "downloads": -1, "filename": "gnar_gear-1.0.7-py3-none-any.whl", "has_sig": false, "md5_digest": "c9c11410183d1538fc67ab3c2ef6cca9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9827, "upload_time": "2018-08-04T23:21:09", "url": "https://files.pythonhosted.org/packages/bb/e1/691efc548a59f35cbaafa759ff626e8515432e4a2ed66ac3a56400297605/gnar_gear-1.0.7-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "8e75a964c03024011f45852b0e21b6a5", "sha256": "ee83ace4e7a2256e332fd2bd9a276d48e445a4a62b258e6ca1d021261a94d924" }, "downloads": -1, "filename": "gnar-gear-1.0.7.tar.gz", "has_sig": false, "md5_digest": "8e75a964c03024011f45852b0e21b6a5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11048, "upload_time": "2018-08-04T23:21:11", "url": "https://files.pythonhosted.org/packages/1d/3e/a8a6aaf8f443b7806f6fbbaf61e657880956ebcb6ebf156f0b997220e42f/gnar-gear-1.0.7.tar.gz" } ], "1.0.8": [ { "comment_text": "", "digests": { "md5": "0ae852d0896242ae5e106b1a3096894b", "sha256": "41f25bc39cf4e6377432aefb43716942affb7ca4baf0ac8452c09183bae6fe39" }, "downloads": -1, "filename": "gnar_gear-1.0.8-py3-none-any.whl", "has_sig": false, "md5_digest": "0ae852d0896242ae5e106b1a3096894b", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 10946, "upload_time": "2018-08-26T08:43:50", "url": "https://files.pythonhosted.org/packages/c0/c5/107117c6ea48add1ede2a4d5390261967f87abb0cfeb95e14fbc7f5d96f3/gnar_gear-1.0.8-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "43275e223be61492542202fcda2400e3", "sha256": "dd39a6d2cffdbe2b59b2ab5a77c26601a4ac0edadf29ee7b86cf7ca327627917" }, "downloads": -1, "filename": "gnar-gear-1.0.8.tar.gz", "has_sig": false, "md5_digest": "43275e223be61492542202fcda2400e3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 16209, "upload_time": "2018-08-26T08:43:51", "url": "https://files.pythonhosted.org/packages/1c/ae/743a96db90abed60cc787d89881f6248fa8ef187acc3f0b430222dfc1415/gnar-gear-1.0.8.tar.gz" } ], "1.0.9": [ { "comment_text": "", "digests": { "md5": "e1274e0933917a569e9e6671cebd26f2", "sha256": "84c874f1d78ab6d8d7152b10c89714c11a80eb1a7c88844a0686e5ec038a2e02" }, "downloads": -1, "filename": "gnar_gear-1.0.9-py3-none-any.whl", "has_sig": false, "md5_digest": "e1274e0933917a569e9e6671cebd26f2", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 10981, "upload_time": "2018-08-27T06:12:25", "url": "https://files.pythonhosted.org/packages/5a/ec/f3886e263729336d0111369bc39ce92b58325e9e5ac0968db48ee921e23b/gnar_gear-1.0.9-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "72c193d67ef66190b18727cb9d85c544", "sha256": "29018e75b7f93c9c4e80403415cad8e735fff852e33a5c4d44e6ccafc950a712" }, "downloads": -1, "filename": "gnar-gear-1.0.9.tar.gz", "has_sig": false, "md5_digest": "72c193d67ef66190b18727cb9d85c544", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 16283, "upload_time": "2018-08-27T06:12:27", "url": "https://files.pythonhosted.org/packages/82/bd/1b17439591a5261e6a6333ea9be668093dbab35444ec7731d484861e05fa/gnar-gear-1.0.9.tar.gz" } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "cbb3f60ed0f95f236dca05f108422b6e", "sha256": "62632b9dd59f0cbbc70b4e6720272127b80ddef85e27421e8c23590870900c36" }, "downloads": -1, "filename": "gnar_gear-1.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "cbb3f60ed0f95f236dca05f108422b6e", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13095, "upload_time": "2018-09-13T00:43:27", "url": "https://files.pythonhosted.org/packages/d7/55/bf7b71660bb8805a51b9c08b2d79eb801402476914dfc8244f7e6cdfc5c4/gnar_gear-1.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "7b356b0f7078f17fdfdefb97b1844e96", "sha256": "8b38107ce482c55ae8b2627c6e98988cd46ba45f568aeaacbab32cf57568c0d7" }, "downloads": -1, "filename": "gnar-gear-1.1.0.tar.gz", "has_sig": false, "md5_digest": "7b356b0f7078f17fdfdefb97b1844e96", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19853, "upload_time": "2018-09-13T00:43:28", "url": "https://files.pythonhosted.org/packages/42/02/fc4ef940909452167d93d90cac5c6cef2fd485f8c09e24a456f55be8c151/gnar-gear-1.1.0.tar.gz" } ], "1.1.1": [ { "comment_text": "", "digests": { "md5": "5fca1abd7359060e2232bef804df4bbd", "sha256": "634a03d5eb0c7f25ee4c2cf235147da8db409ec504a5250607db31b4cda4d572" }, "downloads": -1, "filename": "gnar_gear-1.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "5fca1abd7359060e2232bef804df4bbd", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13120, "upload_time": "2018-09-13T01:05:03", "url": "https://files.pythonhosted.org/packages/0f/d2/60481c3666accccb75a32eff7ed42d0cd56f74ea0aad2ae583b0ddc5aa73/gnar_gear-1.1.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "05db32a901823a75334329c75271b9d6", "sha256": "5244d166fd1ab4e8c435cc5dd4905d4a23efc2d6a2731c7f0155c6208beb9176" }, "downloads": -1, "filename": "gnar-gear-1.1.1.tar.gz", "has_sig": false, "md5_digest": "05db32a901823a75334329c75271b9d6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19920, "upload_time": "2018-09-13T01:05:04", "url": "https://files.pythonhosted.org/packages/2d/0c/10b1172b257dfb8bb5e70765b842b86e8628ccc8e5d0a360bf973c8ed71e/gnar-gear-1.1.1.tar.gz" } ], "1.1.2": [ { "comment_text": "", "digests": { "md5": "2b63c86d6cc082fa67a3078fcfbe3dcd", "sha256": "38ef10a9cb18dcc6759792f69404aac8df1bf5e928b20e6c6e6b93920a10f5f5" }, "downloads": -1, "filename": "gnar_gear-1.1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "2b63c86d6cc082fa67a3078fcfbe3dcd", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13290, "upload_time": "2018-09-13T05:02:31", "url": "https://files.pythonhosted.org/packages/36/c9/1ff3aee1247cc25fbe26da34553e9c70cda165f0c3e9998c5fc917c4db16/gnar_gear-1.1.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "385f8229ac3e098b514c2309bbc3ea4d", "sha256": "0e50ed60f2088dae7f6baf550d20fc5c80c9e5460b7ff5a525a4b070270170b2" }, "downloads": -1, "filename": "gnar-gear-1.1.2.tar.gz", "has_sig": false, "md5_digest": "385f8229ac3e098b514c2309bbc3ea4d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20351, "upload_time": "2018-09-13T05:02:32", "url": "https://files.pythonhosted.org/packages/3b/ad/359719ec00c2c81716f69a3cf9a911fad1871d05703f9b0da17cc77349d7/gnar-gear-1.1.2.tar.gz" } ], "1.1.3": [ { "comment_text": "", "digests": { "md5": "97421f2816824b02bbc5ef546ef5aa18", "sha256": "32b0cab997b8bac2f2f41352b5894354c2ee4ed73c24886c5a49a7e680773546" }, "downloads": -1, "filename": "gnar_gear-1.1.3-py3-none-any.whl", "has_sig": false, "md5_digest": "97421f2816824b02bbc5ef546ef5aa18", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13293, "upload_time": "2018-09-14T00:00:35", "url": "https://files.pythonhosted.org/packages/d0/6b/f15f1baefe44a38663369fa87ac42782af7fe7dd40964af86ba40aa35f1c/gnar_gear-1.1.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5bae02b503be67fc5717e6f1447bdc17", "sha256": "2effdc65662ef6584618d106bed13612eb74bb5f0b11979a889f36815670b594" }, "downloads": -1, "filename": "gnar-gear-1.1.3.tar.gz", "has_sig": false, "md5_digest": "5bae02b503be67fc5717e6f1447bdc17", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20361, "upload_time": "2018-09-14T00:00:37", "url": "https://files.pythonhosted.org/packages/fd/20/f019ff413290ebc001708624d04b8a7aaf7f995ab3bceae8e96d1ed5dc93/gnar-gear-1.1.3.tar.gz" } ], "1.1.4": [ { "comment_text": "", "digests": { "md5": "5e7c9336981eb9f6fa13175a56833abe", "sha256": "b2fe9b1eb54bf5600f4c05d743138a560475f4d360c1706ae8272c42a9cad018" }, "downloads": -1, "filename": "gnar_gear-1.1.4-py3-none-any.whl", "has_sig": false, "md5_digest": "5e7c9336981eb9f6fa13175a56833abe", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13566, "upload_time": "2018-09-24T16:27:58", "url": "https://files.pythonhosted.org/packages/98/e9/de161fca949deaf4d49aca80767dfd48805fefdd11c88d5075575d9dec1b/gnar_gear-1.1.4-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f3744b135f0a3f1087aaea14f6809de9", "sha256": "7577f9340b6aa8cb27096a4b1eadcca03b75b906e3ed70bf4a2d4d62b913c781" }, "downloads": -1, "filename": "gnar-gear-1.1.4.tar.gz", "has_sig": false, "md5_digest": "f3744b135f0a3f1087aaea14f6809de9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20909, "upload_time": "2018-09-24T16:28:00", "url": "https://files.pythonhosted.org/packages/f8/be/a35e4a68893a7a0f7ea57e14c25d8b151fa98481ee756ce0c3b048950f74/gnar-gear-1.1.4.tar.gz" } ], "1.1.5": [ { "comment_text": "", "digests": { "md5": "206d918fa31fc10872901d2d6aa28142", "sha256": "19ba3ffd753c84372f8aad18428c49dba0c6507a86783af1b2978ea42565c2a6" }, "downloads": -1, "filename": "gnar_gear-1.1.5-py3-none-any.whl", "has_sig": false, "md5_digest": "206d918fa31fc10872901d2d6aa28142", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13564, "upload_time": "2019-01-09T00:38:34", "url": "https://files.pythonhosted.org/packages/94/60/16f54a1a7d314e05f9fdbce7426622a8601c6b67a5e8a34027ff09dcea64/gnar_gear-1.1.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5be59b8834e0429ef6fa7cefc3864ce1", "sha256": "4be755217fcabbbae7a752bb57f2dc8020acdd83d6904f6d3b2b283c4c603787" }, "downloads": -1, "filename": "gnar-gear-1.1.5.tar.gz", "has_sig": false, "md5_digest": "5be59b8834e0429ef6fa7cefc3864ce1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20929, "upload_time": "2019-01-09T00:38:36", "url": "https://files.pythonhosted.org/packages/ac/6f/f58ffb3809397ec8cbb01f7f61a3fcd5d931dda4e0844c2a9904e1aecc08/gnar-gear-1.1.5.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "206d918fa31fc10872901d2d6aa28142", "sha256": "19ba3ffd753c84372f8aad18428c49dba0c6507a86783af1b2978ea42565c2a6" }, "downloads": -1, "filename": "gnar_gear-1.1.5-py3-none-any.whl", "has_sig": false, "md5_digest": "206d918fa31fc10872901d2d6aa28142", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13564, "upload_time": "2019-01-09T00:38:34", "url": "https://files.pythonhosted.org/packages/94/60/16f54a1a7d314e05f9fdbce7426622a8601c6b67a5e8a34027ff09dcea64/gnar_gear-1.1.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5be59b8834e0429ef6fa7cefc3864ce1", "sha256": "4be755217fcabbbae7a752bb57f2dc8020acdd83d6904f6d3b2b283c4c603787" }, "downloads": -1, "filename": "gnar-gear-1.1.5.tar.gz", "has_sig": false, "md5_digest": "5be59b8834e0429ef6fa7cefc3864ce1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20929, "upload_time": "2019-01-09T00:38:36", "url": "https://files.pythonhosted.org/packages/ac/6f/f58ffb3809397ec8cbb01f7f61a3fcd5d931dda4e0844c2a9904e1aecc08/gnar-gear-1.1.5.tar.gz" } ] }