From 244386cc4e7a7ad007de340c5fdc55b50a6cd08b Mon Sep 17 00:00:00 2001 From: Mark Beacom Date: Fri, 28 Jun 2019 14:08:38 -0400 Subject: [PATCH 1/4] Update docstrings, docs, and typing --- .DS_Store | Bin 8196 -> 0 bytes cloudendure/api.py | 13 ++--- cloudendure/cloudendure.py | 2 + lambda/exceptions.py | 20 +++++++ lambda/handler.py | 112 +++++++++++++++++++++++++++---------- pydocmd.yml | 11 ++++ 6 files changed, 122 insertions(+), 36 deletions(-) delete mode 100644 .DS_Store create mode 100644 lambda/exceptions.py diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index c5c864be1cb2175cc490eb5e689a56898f1adf02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMO>fgc5Pb`wahggXghY=%av>7pg2crw2~s3lA;KNVR~4GpQQJu2<_rIUzra7^ z&JTb$yGymZc2G|U#H_VDjy-Q?=gqFIaR8XZ_4Em_4^;WDW8n{@B| z*6O3cY6lbgE;)xOdiaPHP7>Q+={-br1H=sH#I13GS&T9d6V!ud6n)Nc!s9VHIL91i zV&`*Wr$v7YvT&8?eNXi6Q?I1YQ`*0v^$CBSF}l(+*uy9OTE;enaq}(UW*7ay#@uEp zjg#{;n*ejS2}A!YdVGp!u}8yMM`(#oW}W6Y%|RQlFvJTCaEuWKu~mq6xNLj5lF4`_Lam-H~gTV{#biM<%fOo-Ygqo=>-PecR5f{Eg7<25a>w#-X%8hY4^ zwdq4vE^{2u%;qr*a?-WzJ7&)~!^XJ@YbHc>@P^SJFrP=1Jw_LPVdJioHJ%{24;YKA zIN2GK+OClJLmzwAhp;TZj%`MpJjproUm->IO{rmsT4XUOxpbY$T4477b)Ia33 zeQjL}Kx*xr{XM%h(W{)a5DvB+r?llb<@FziJkO~rn8M4-NnT;o4*`}29h8AT%D_)d C&sRbK diff --git a/cloudendure/api.py b/cloudendure/api.py index 9220142ae..b38c48068 100644 --- a/cloudendure/api.py +++ b/cloudendure/api.py @@ -90,16 +90,12 @@ def login(self, username="", password=""): _xsrf_token (str): The XSRF token to be used for subsequent API requests. """ - endpoint: str = "login" _username: str = self.config.active_config["username"] or username _password: str = self.config.active_config["password"] or password _auth: Dict[str, str] = {"username": _username, "password": _password} # Attempt to login to the CloudEndure API via a POST request. - response: requests.Response = self.api_call( - "login", "post", data=json.dumps(_auth) - ) - # response: requests.Response = self.session.post(f'{self.api_endpoint}/{endpoint}', json=_auth) + response: requests.Response = self.api_call("login", "post", data=json.dumps(_auth)) # Check whether or not the request was successful. if response.status_code not in [200, 307]: @@ -117,8 +113,11 @@ def login(self, username="", password=""): ) raise CloudEndureUnauthorized() - # print('response: ', response, response.cookies) - _xsrf_token: str = str(response.cookies["XSRF-TOKEN"]) + # Grab the XSRF token received from the response, as stored in cookies. + # _xsrf_token: str = str(response.cookies["XSRF-TOKEN"]) + _xsrf_token: str = str(response.cookies.get("XSRF-TOKEN", "")) + if not _xsrf_token: + raise CloudEndureException("Failed to fetch a token from CloudEndure!") # Strip the XSRF token of wrapping double-quotes from the cookie. if _xsrf_token.startswith('"') and _xsrf_token.endswith('"'): diff --git a/cloudendure/cloudendure.py b/cloudendure/cloudendure.py index 8dba87112..f83cd2cc9 100644 --- a/cloudendure/cloudendure.py +++ b/cloudendure/cloudendure.py @@ -30,6 +30,7 @@ MIGRATION_WAVE: str = os.environ.get("CLOUDENDURE_MIGRATION_WAVE", "0") CLONE_STATUS: str = os.environ.get("CLOUDENDURE_CLONE_STATUS", "NOT_STARTED") MAX_LAG_TTL: int = int(os.environ.get("CLOUDENDURE_MAX_LAG_TTL", "90")) +SHARE_IMAGE: str = os.environ.get("", "") class CloudEndure: @@ -201,6 +202,7 @@ def update_blueprint( except Exception as e: print(f"Updating blueprint task failed! {e}") return False + return True def launch(self, project_id=global_project_id, launch_type="test", dry_run=False): """Launch the test target instances.""" diff --git a/lambda/exceptions.py b/lambda/exceptions.py new file mode 100644 index 000000000..1c6e3375a --- /dev/null +++ b/lambda/exceptions.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Define the Lambda specific exceptions.""" + + +class LambdaException(Exception): + """Define the generic AWS Lambda exception.""" + + pass + + +class InvalidPayload(LambdaException): + """Define the exception to be raised when an invalid payload is encountered.""" + + pass + + +class ImproperlyConfigured(LambdaException): + """Define the exception to be raised if the environment is improperly configured or missing.""" + + pass diff --git a/lambda/handler.py b/lambda/handler.py index 3e58808ae..a1b916c8b 100644 --- a/lambda/handler.py +++ b/lambda/handler.py @@ -4,17 +4,20 @@ Attributes: LOG_LEVEL (str): REGION_OVERRIDE (str): If provided, this value will override the default AWS region. - logger (logging.Logger): + logger (logging.Logger): The logger to be used throughout execution of the AWS Lambda. """ import datetime import json import logging import os +from typing import Any, Dict, List import boto3 from botocore.exceptions import ClientError +from .exceptions import ImproperlyConfigured, InvalidPayload + LOG_LEVEL = getattr(logging, os.environ.get("CLOUDENDURE_LOGLEVEL", "INFO")) REGION_OVERRIDE = os.environ.get("CLOUDENDURE_REGION_OVERRIDE", "") @@ -23,23 +26,51 @@ logger.setLevel(LOG_LEVEL) -def send_sqs_message(imageinfo): - """Sends a SQS message with the AMI information - that was created from the migrated instance - that passed testing post migration in CloudEndure +def send_sqs_message(image_info: Dict) -> bool: + """Send a SQS message. + + The message includes the AMI information that was created from the migrated + instance that passed testing post migration in CloudEndure. + + Raises: + ClientError: The exception is raised in the event of a boto3 client error. + ImproperlyConfigured: The exception is raised in the event of missing or invalid + environment configuration settings. + + Returns: + bool: Whether or not the message has been sent successfully. + """ + queue_url = os.environ.get("QueueURL", "") + if not queue_url: + raise ImproperlyConfigured("Missing environment variable: QueueURL") + try: - message = json.dumps(imageinfo) + message = json.dumps(image_info) sqsclient = boto3.client("sqs") - sqsclient.send_message(QueueUrl=os.environ["QueueURL"], MessageBody=message) - - except ClientError as err: - logger.error(err.response) + sqsclient.send_message(QueueUrl=queue_url, MessageBody=message) + except ClientError as e: + logger.error(e.response) + return False + except ImproperlyConfigured as e: + logger.error(e) + return False + return True -def create_ami(project_id, instance_id): +def create_ami(project_id: str, instance_id: str) -> bool: """Create an AMI from the specified instance. + Args: + project_id (str): The ID associated with the Project. + instance_id (str): The ID associated with the AWS instance. + + Raises: + ClientError: The exception is raised in the event of a boto3 client error. + + Returns: + bool: Whether or not the AMI has been created successfully. + """ try: _ec2_client = boto3.client("ec2") @@ -53,7 +84,10 @@ def create_ami(project_id, instance_id): NoReboot=True, ) logger.info("AMI Id: %s", ec2_image) - _filters = [{"Name": "resource-id", "Values": [instance_id]}] + _filters: List[Dict] = [{ + "Name": "resource-id", + "Values": [instance_id], + }] # Tag the newly created AMI by getting the tags of the migrated instance to copy to the AMI. ec2_tags = _ec2_client.describe_tags(Filters=_filters) @@ -69,26 +103,46 @@ def create_ami(project_id, instance_id): except ClientError as err: logger.error(err.response) + return False + return True -def lambda_handler(event, context): - """Lambda entry point""" - logger.info(event) +def lambda_handler(event: Dict[str, Any], context: Dict[str, Any]) -> bool: + """Define the AWS Lambda entry point and handler. - try: - json_sns_message = json.loads(event["Records"][0]["Sns"]["Message"]) + Args: + event (str): The event performed against Lambda. + context (dict): The context of the request performed against Lambda. - if json_sns_message["Pass"] != "True": - logger.error( - "%s did not pass post migration testing! Not creating an AMI." - % (json_sns_message["instanceId"]) - ) + Raises: + ClientError: The exception is raised in the event of a boto3 client error. + InvalidPayload: The exception is raised in the event of an invalid payload. + + Returns: + bool: Whether or not the lambda function has executed successfully. + + """ + logger.debug(event) + + event_records = event.get("Records", []) + if not event_records: + return False + try: + event_message = event_records[0].get("Sns", {}).get("Message", "") + json_sns_message = json.loads(event_message) + instance_id: str = json_sns_message.get("instanceId", "") + project_id: str = json_sns_message.get("projectId", "") + + if json_sns_message.get("Pass", "NA") != "True": + raise InvalidPayload(f"{instance_id} did not pass post migration testing! Not creating an AMI.") else: - logger.info( - "%s passed post migration testing. Creating an AMI." - % (json_sns_message["instanceId"]) - ) - create_ami("", json_sns_message["instanceId"]) - except ClientError as err: - logger.error(err.response) + logger.info("%s passed post migration testing. Creating an AMI." % (instance_id)) + create_ami(project_id, instance_id) + except ClientError as e: + logger.error(e.response) + return False + except InvalidPayload as e: + logger.error(e) + return False + return True diff --git a/pydocmd.yml b/pydocmd.yml index b674fd25b..61d14738a 100644 --- a/pydocmd.yml +++ b/pydocmd.yml @@ -15,6 +15,12 @@ generate: - cloudendure.models++ - code/cloudendure/utils.md: - cloudendure.utils++ + - code/lambda/copy_ami.md: + - lambda.copy_ami++ + - code/lambda/exceptions.md: + - lambda.exceptions++ + - code/lambda/handler.md: + - lambda.handler++ pages: - General: @@ -28,6 +34,10 @@ pages: - Exceptions: code/cloudendure/exceptions.md - Models: code/cloudendure/models.md - Utilities: code/cloudendure/utils.md + - AWS Lambda: + - Handler: code/lambda/handler.md + - Exceptions: code/lambda/exceptions.md + - Copy AMI Utils: code/lambda/copy_ami.md - API Documentation: - MAIN: api/API_README.md << docs/API_README.md - AccountApi: api/AccountApi.md << docs/AccountApi.md @@ -133,3 +143,4 @@ theme: additional_search_paths: - cloudendure/ + - lambda/ From 475057b576e84b5c1d7fe6810db431b17ee8cd16 Mon Sep 17 00:00:00 2001 From: Mark Beacom Date: Fri, 28 Jun 2019 18:57:54 -0400 Subject: [PATCH 2/4] Add changelog, update deps, and adjust travis - increment version 0.0.4 --- .travis.yml | 1 + CHANGELOG.md | 24 ++++++++++++++++++ Pipfile.lock | 69 ++++++++++++++++++++++++++-------------------------- README.md | 8 ++++-- pydocmd.yml | 1 + setup.py | 4 +-- 6 files changed, 68 insertions(+), 39 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.travis.yml b/.travis.yml index e1fb1fe24..91fe17173 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ jobs: password: $PYPI_PASSWORD distributions: "sdist bdist_wheel" skip_existing: true + skip_cleanup: true on: tags: true - provider: releases diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..57cf83aaf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Change Log + +## [v0.0.3](https://github.com/mbeacom/cloudendure-python/tree/v0.0.3) (2019-06-20) +[Full Changelog](https://github.com/mbeacom/cloudendure-python/compare/v0.0.2...v0.0.3) + +**Implemented enhancements:** + +- Update pip dependencies [\#3](https://github.com/mbeacom/cloudendure-python/pull/3) ([mbeacom](https://github.com/mbeacom)) + +**Merged pull requests:** + +- Update README.md [\#4](https://github.com/mbeacom/cloudendure-python/pull/4) ([twarnock2w](https://github.com/twarnock2w)) + +## [v0.0.2](https://github.com/mbeacom/cloudendure-python/tree/v0.0.2) (2019-06-16) +[Full Changelog](https://github.com/mbeacom/cloudendure-python/compare/v0.0.1...v0.0.2) + +**Implemented enhancements:** + +- Pointed API usecase and CLI additions [\#2](https://github.com/mbeacom/cloudendure-python/pull/2) ([mbeacom](https://github.com/mbeacom)) + +## [v0.0.1](https://github.com/mbeacom/cloudendure-python/tree/v0.0.1) (2019-05-30) + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index 22f04783a..69fec9e05 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,18 +18,18 @@ "default": { "boto3": { "hashes": [ - "sha256:64acb422442042a6f908d03909d2e73a3f3ca2eed4252c8b6e3a460b43d316d1", - "sha256:e482f835cd547589bf9f11c5c1cbad5f3303105e1e909af620d3617c6dee08eb" + "sha256:7fa7431115be7bdbebf207f34fd010553d0e3ccf30ad977cf38ea80ea47e68bf", + "sha256:99e5d071332f176c32ced41902349fe760add1f02cc86de1d5ae4cf05ff7a6e7" ], "index": "pypi", - "version": "==1.9.172" + "version": "==1.9.180" }, "botocore": { "hashes": [ - "sha256:13e75f594c77988efd13f0862f3c7397d587e74b623fe0825d48b0ec0dc96d6b", - "sha256:1fa1ad4be9e9fea77eed7d5021be4283e9bcfecc60d2de83f96552025c4725d1" + "sha256:a2ceaa00724228a961ef6f97da60ab09f3161a76e2f3ae82a49be396ca1083fc", + "sha256:f049dbfe83423f5cf350a861861e7f904967dea5e142ec1a17c70c07f9fdb117" ], - "version": "==1.12.172" + "version": "==1.12.180" }, "certifi": { "hashes": [ @@ -299,11 +299,11 @@ }, "isort": { "hashes": [ - "sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a", - "sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d" + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], "index": "pypi", - "version": "==4.3.20" + "version": "==4.3.21" }, "jinja2": { "hashes": [ @@ -428,28 +428,28 @@ }, "more-itertools": { "hashes": [ - "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", - "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" + "sha256:3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d", + "sha256:8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a" ], "markers": "python_version > '2.7'", - "version": "==7.0.0" + "version": "==7.1.0" }, "mypy": { "hashes": [ - "sha256:0c00f90cb20d3b5a7fa9b190e92e8a94cd4bbb26dd58bbed13bfc76677a39700", - "sha256:104086c924b1ac605bed1539886b86202c02b08f78649b45881b4a624ff66a46", - "sha256:207d37116a4a06143f50a2d83d537cf43e7cd648e6969b6176da9e2881ec1c22", - "sha256:5922d6fdebec30131e4077f3b45d8634410a464f665976bba880ee815cee2a64", - "sha256:5ad4cc4c54f82cd0b01905720dc340f16d1e4d6c1535bbfa6dfa045f957be4a2", - "sha256:5f210761b3e94b30ed3ff559ab8bee35a187df070d28d577bd123c8a76e2cd2d", - "sha256:66e679562e486aa440abd3f9f366a3a116576ef2b0ba24584dded9ae8d1719ac", - "sha256:67a632ea4596b417ed9af60fde828f6c6bb7e83e7f7f0d7057a3a1dece940199", - "sha256:953e5e10203df8691fcd8ce40a5e6e2ec37144b9fb5adf6bcbafc6b74bda0593", - "sha256:9e80c47b3d621cf8f98e58cc202a03a92b430a4d736129197a2a83cbe35a3243", - "sha256:d0e3c21620637b1548c1e0f0c2b56617dd0a9dc1fda1afa00210db5922487cd3" + "sha256:12d18bd7fc642c5d54b1bb62dde813a7e2ab79b32ee11ff206ac387c68fc2ad4", + "sha256:23e24bc1683a36f39dee67d8ac74ea414654642eee26d420bada95b8ee8c9095", + "sha256:2b38e64c52a8968df4ebcae0ddba4a54eb94d184695dd4e54e14509a9389b78c", + "sha256:3d4f551466a76e278187ec3a5b26cfb50f72f6760b749aa00ac69a6f9c99898d", + "sha256:53d5dacb8d844e50be698830509aa592b093547e7ab90aee63eb23db61109007", + "sha256:56f981d246010ba21cac6b2455eaecfaf68fc8a5663d865b26c8e579c36f751d", + "sha256:8c57f6f59f1e8479d9fc6e1bf034353e54626ed64e32394c613afc493a441dc1", + "sha256:bbed4a593d87476b592d52867ef86da2155ccd0becf0c4c02e6567d842e43368", + "sha256:d6ff850e2ba18b2db7704897c8f2f1384478e3b75ad292ec06196bf7794f3a40", + "sha256:e13b1bb8785d7f785e0b88873f1c21cda58ceba9ce1153b58cbfa24b09a111d5", + "sha256:e2b9ee6f648ce72d6741925a47c88c2391168ef973b6f74f17969450c5b1ffdd" ], "index": "pypi", - "version": "==0.710" + "version": "==0.711" }, "mypy-extensions": { "hashes": [ @@ -676,10 +676,9 @@ }, "snowballstemmer": { "hashes": [ - "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", - "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + "sha256:9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9" ], - "version": "==1.2.1" + "version": "==1.9.0" }, "stevedore": { "hashes": [ @@ -703,15 +702,15 @@ }, "tornado": { "hashes": [ - "sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b", - "sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec", - "sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2", - "sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8", - "sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d", - "sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0", - "sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa" + "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", + "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", + "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", + "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", + "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", + "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", + "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" ], - "version": "==6.0.2" + "version": "==6.0.3" }, "tqdm": { "hashes": [ diff --git a/README.md b/README.md index 4c6d810bf..25ecd0d57 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # cloudendure-python -Python wrapper and CLI for CloudEndure +Python wrapper and CLI for [CloudEndure](https://www.cloudendure.com/) [![Build Status](https://travis-ci.com/mbeacom/cloudendure-python.svg?branch=master)](https://travis-ci.com/mbeacom/cloudendure-python) [Documentation](https://mbeacom.github.io/cloudendure-python/) -Package version: `0.0.3` +Package version: `0.0.4` ## Requirements @@ -95,3 +95,7 @@ Logging in for the first time will generate the `~/.cloudendure.yml` file. ## Coming Soon This project is currently a work in progress and will actively change. This client has not yet been finalized and is entirely subject to change. + +## Changelog + +Check out the [CHANGELOG](CHANGELOG.md) diff --git a/pydocmd.yml b/pydocmd.yml index 61d14738a..1d011da5c 100644 --- a/pydocmd.yml +++ b/pydocmd.yml @@ -26,6 +26,7 @@ pages: - General: - Home: index.md << README.md - References: references.md << REFERENCE.md + - Changelog: changes.md << CHANGELOG.md - Code: - CloudEndure: - API: code/cloudendure/api.md diff --git a/setup.py b/setup.py index 992135b97..3aee5389e 100644 --- a/setup.py +++ b/setup.py @@ -19,11 +19,11 @@ # Package meta-data. NAME: str = "cloudendure" DESCRIPTION: str = "CloudEndure Python Client and CLI" -URL: str = "https://github.com/mbeacom/cloudendure-py" +URL: str = "https://github.com/mbeacom/cloudendure-python" EMAIL: str = "markvbeacom@gmail.com" AUTHOR: str = "Mark Beacom" REQUIRES_PYTHON: str = ">=3.6.0" -VERSION: str = "0.0.3" +VERSION: str = "0.0.4" REQUIRED: List[str] = ["requests", "boto3"] EXTRAS: Dict[str, List[str]] = { From c0fb38ab6a1f8dccab3cc9b23b252d1f99a4a245 Mon Sep 17 00:00:00 2001 From: Mark Beacom Date: Fri, 28 Jun 2019 19:06:44 -0400 Subject: [PATCH 3/4] Add security policy to docs --- pydocmd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pydocmd.yml b/pydocmd.yml index 1d011da5c..d7a431afd 100644 --- a/pydocmd.yml +++ b/pydocmd.yml @@ -135,6 +135,8 @@ pages: - RecoveryPlansApi: api/RecoveryPlansApi.md << docs/RecoveryPlansApi.md - ReplicationApi: api/ReplicationApi.md << docs/ReplicationApi.md - UserApi: api/UserApi.md << docs/UserApi.md + - Miscellaneous: + - Security Policy: securitypolicy.md << SECURITY.md theme: name: material logo: 'http://chittagongit.com/images/cloud-png-icon/cloud-png-icon-3.jpg' From c358087c662b8b79580df56c97a2b9a1be37fca8 Mon Sep 17 00:00:00 2001 From: Mark Beacom Date: Fri, 28 Jun 2019 19:13:07 -0400 Subject: [PATCH 4/4] Add stickler config --- .stickler.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .stickler.yml diff --git a/.stickler.yml b/.stickler.yml new file mode 100644 index 000000000..fe611e7ee --- /dev/null +++ b/.stickler.yml @@ -0,0 +1,9 @@ +--- +linters: + black: + fixer: false + flake8: + python: 3 + max-line-length: 120 + max-complexity: 20 + fixer: false