├── .flake8 ├── images ├── app-architecture.png └── app-architecture.pptx ├── app ├── lambdainit.py ├── checkpoint.py ├── poller.py └── twitter_proxy.py ├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── test └── unit │ ├── test_constants.py │ ├── test_checkpoint.py │ ├── conftest.py │ └── test_poller.py ├── Pipfile ├── LICENSE ├── .vscode └── settings.json ├── Makefile ├── .gitignore ├── template.yml ├── CONTRIBUTING.md ├── README.md └── Pipfile.lock /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = E126 -------------------------------------------------------------------------------- /images/app-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-serverless-twitter-event-source/HEAD/images/app-architecture.png -------------------------------------------------------------------------------- /images/app-architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-serverless-twitter-event-source/HEAD/images/app-architecture.pptx -------------------------------------------------------------------------------- /app/lambdainit.py: -------------------------------------------------------------------------------- 1 | """Special initializations for Lambda.""" 2 | 3 | import sys 4 | 5 | # add packaged dependencies to search path 6 | sys.path.append('lib') 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /test/unit/test_constants.py: -------------------------------------------------------------------------------- 1 | REGION = 'us-east-1' 2 | SEARCH_CHECKPOINT_TABLE_NAME = 'Checkpoint' 3 | SEARCH_TEXT = '#searchtext' 4 | TWEET_PROCESSOR_FUNCTION_NAME = 'TweetProcessor' 5 | BATCH_SIZE = '2' 6 | STREAM_MODE_ENABLED = 'false' 7 | SSM_PARAMETER_PREFIX = 'ssm_prefix' 8 | 9 | CONSUMER_KEY = 'key' 10 | CONSUMER_SECRET = 'secret' 11 | ACCESS_TOKEN = 'token' 12 | ACCESS_TOKEN_SECRET = 'token_secret' 13 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | "boto3" = "*" 8 | python-twitter = "*" 9 | requests = "==2.25.1" 10 | 11 | [dev-packages] 12 | flake8 = "*" 13 | autopep8 = "*" 14 | pydocstyle = "*" 15 | cfn-lint = "*" 16 | aws-sam-cli = "*" 17 | awscli = "*" 18 | pytest = "*" 19 | pytest-mock = "*" 20 | pytest-cov = "*" 21 | 22 | [requires] 23 | python_version = "3.8" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /app/checkpoint.py: -------------------------------------------------------------------------------- 1 | """Manages search checkpoint (used when stream mode enabled).""" 2 | 3 | import os 4 | 5 | import boto3 6 | from boto3.dynamodb.conditions import Attr, Or 7 | from botocore.exceptions import ClientError 8 | 9 | DDB = boto3.resource('dynamodb') 10 | TABLE = DDB.Table(os.getenv('SEARCH_CHECKPOINT_TABLE_NAME')) 11 | RECORD_KEY = 'checkpoint' 12 | 13 | 14 | def last_id(): 15 | """Return last checkpoint tweet id.""" 16 | result = TABLE.get_item( 17 | Key={'id': RECORD_KEY} 18 | ) 19 | if 'Item' in result: 20 | return result['Item']['since_id'] 21 | return None 22 | 23 | 24 | def update(since_id): 25 | """Update checkpoint to given tweet id.""" 26 | try: 27 | TABLE.put_item( 28 | Item={ 29 | 'id': RECORD_KEY, 30 | 'since_id': since_id 31 | }, 32 | ConditionExpression=Or( 33 | Attr('id').not_exists(), 34 | Attr('since_id').lt(since_id) 35 | ) 36 | ) 37 | except ClientError as e: 38 | if e.response['Error']['Code'] != 'ConditionalCheckFailedException': 39 | raise 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.unitTest.unittestEnabled": false, 3 | "python.unitTest.nosetestsEnabled": false, 4 | "python.unitTest.pyTestEnabled": true, 5 | "python.unitTest.pyTestPath": "pipenv", 6 | "python.unitTest.pyTestArgs": [ 7 | "run", 8 | "py.test", 9 | "--cov", 10 | "app", 11 | "--cov-fail-under", 12 | "85", 13 | "-vv", 14 | "test/unit" 15 | ], 16 | "python.linting.pylintEnabled": false, 17 | "python.linting.ignorePatterns": [ 18 | ".vscode/*.py", 19 | "**/site-packages/**/*.py", 20 | "dist/**/*.py" 21 | ], 22 | "python.linting.flake8Enabled": true, 23 | "python.linting.flake8Path": "pipenv", 24 | "python.linting.flake8Args": [ 25 | "run", 26 | "flake8", 27 | "app" 28 | ], 29 | "python.linting.pydocstyleEnabled": true, 30 | "python.linting.pydocstylePath": "pipenv", 31 | "python.linting.pydocstyleArgs": [ 32 | "run", 33 | "pydocstyle", 34 | "app" 35 | ], 36 | "python.formatting.autopep8Path": "pipenv", 37 | "python.formatting.autopep8Args": [ 38 | "run", 39 | "autopep8", 40 | "--max-line-length=120", 41 | "--ignore", 42 | "E126,E226,E24,W503" 43 | ] 44 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | PY_VERSION := 3.8 3 | 4 | export PYTHONUNBUFFERED := 1 5 | 6 | BUILD_DIR := dist 7 | 8 | # Required environment variables (user must override) 9 | 10 | # S3 bucket used for packaging SAM templates 11 | PACKAGE_BUCKET ?= mynicebucketwithadot.dot 12 | 13 | # user can optionally override the following by setting environment variables with the same names before running make 14 | 15 | # Path to system pip 16 | PIP ?= pip 17 | # Default AWS CLI region 18 | AWS_DEFAULT_REGION ?= us-east-1 19 | 20 | PYTHON := $(shell /usr/bin/which python$(PY_VERSION)) 21 | 22 | .DEFAULT_GOAL := build 23 | 24 | clean: 25 | rm -rf $(BUILD_DIR) 26 | 27 | init: 28 | $(PYTHON) -m $(PIP) install pipenv --user 29 | pipenv sync --dev 30 | 31 | compile: 32 | mkdir -p $(BUILD_DIR) 33 | pipenv run flake8 app 34 | pipenv run pydocstyle app 35 | pipenv run cfn-lint template.yml 36 | pipenv run py.test --cov=app --cov-fail-under=85 -vv test/unit 37 | 38 | build: compile 39 | 40 | package: compile 41 | cp -r template.yml app $(BUILD_DIR) 42 | 43 | # package dependencies in lib dir 44 | pipenv requirements > $(BUILD_DIR)/requirements.txt 45 | pipenv run pip install -t $(BUILD_DIR)/app/lib -r $(BUILD_DIR)/requirements.txt 46 | 47 | # replace code local references with S3 48 | pipenv run sam package --template-file $(BUILD_DIR)/template.yml --s3-bucket $(PACKAGE_BUCKET) --output-template-file $(BUILD_DIR)/packaged-app.yml 49 | -------------------------------------------------------------------------------- /app/poller.py: -------------------------------------------------------------------------------- 1 | """Lambda handler for polling twitter API with configured search.""" 2 | 3 | import lambdainit # noqa: F401 4 | 5 | import json 6 | import os 7 | 8 | import boto3 9 | 10 | import twitter_proxy 11 | import checkpoint 12 | 13 | LAMBDA = boto3.client('lambda') 14 | 15 | SEARCH_TEXT = os.getenv('SEARCH_TEXT') 16 | TWEET_PROCESSOR_FUNCTION_NAME = os.getenv('TWEET_PROCESSOR_FUNCTION_NAME') 17 | BATCH_SIZE = int(os.getenv('BATCH_SIZE')) 18 | STREAM_MODE_ENABLED = os.getenv('STREAM_MODE_ENABLED') == 'true' 19 | 20 | 21 | def handler(event, context): 22 | """Forward SQS messages to Kinesis Firehose Delivery Stream.""" 23 | for batch in _search_batches(): 24 | LAMBDA.invoke( 25 | FunctionName=TWEET_PROCESSOR_FUNCTION_NAME, 26 | InvocationType='Event', 27 | Payload=json.dumps(batch) 28 | ) 29 | 30 | 31 | def _search_batches(): 32 | since_id = None 33 | if STREAM_MODE_ENABLED: 34 | since_id = checkpoint.last_id() 35 | 36 | tweets = [] 37 | while True: 38 | result = twitter_proxy.search(SEARCH_TEXT, since_id) 39 | if not result['statuses']: 40 | # no more results 41 | break 42 | 43 | tweets = result['statuses'] 44 | size = len(tweets) 45 | for i in range(0, size, BATCH_SIZE): 46 | yield tweets[i:min(i + BATCH_SIZE, size)] 47 | since_id = result['search_metadata']['max_id'] 48 | 49 | if STREAM_MODE_ENABLED: 50 | checkpoint.update(since_id) 51 | -------------------------------------------------------------------------------- /app/twitter_proxy.py: -------------------------------------------------------------------------------- 1 | """Twitter API Helper.""" 2 | 3 | import os 4 | 5 | import boto3 6 | import twitter 7 | 8 | SSM_PARAMETER_PREFIX = os.getenv("SSM_PARAMETER_PREFIX") 9 | CONSUMER_KEY_PARAM_NAME = '/{}/consumer_key'.format(SSM_PARAMETER_PREFIX) 10 | CONSUMER_SECRET_PARAM_NAME = '/{}/consumer_secret'.format(SSM_PARAMETER_PREFIX) 11 | ACCESS_TOKEN_PARAM_NAME = '/{}/access_token'.format(SSM_PARAMETER_PREFIX) 12 | ACCESS_TOKEN_SECRET_PARAM_NAME = '/{}/access_token_secret'.format(SSM_PARAMETER_PREFIX) 13 | 14 | SSM = boto3.client('ssm') 15 | 16 | 17 | def search(search_text, since_id=None): 18 | """Search for tweets matching the given search text.""" 19 | return TWITTER.GetSearch(term=search_text, count=100, return_json=True, since_id=since_id) 20 | 21 | 22 | def _create_twitter_api(): 23 | parameter_names = [ 24 | CONSUMER_KEY_PARAM_NAME, 25 | CONSUMER_SECRET_PARAM_NAME, 26 | ACCESS_TOKEN_PARAM_NAME, 27 | ACCESS_TOKEN_SECRET_PARAM_NAME 28 | ] 29 | result = SSM.get_parameters( 30 | Names=parameter_names, 31 | WithDecryption=True 32 | ) 33 | 34 | if result['InvalidParameters']: 35 | raise RuntimeError( 36 | 'Could not find expected SSM parameters containing Twitter API keys: {}'.format(parameter_names)) 37 | 38 | param_lookup = {param['Name']: param['Value'] for param in result['Parameters']} 39 | return twitter.Api( 40 | consumer_key=param_lookup[CONSUMER_KEY_PARAM_NAME], 41 | consumer_secret=param_lookup[CONSUMER_SECRET_PARAM_NAME], 42 | access_token_key=param_lookup[ACCESS_TOKEN_PARAM_NAME], 43 | access_token_secret=param_lookup[ACCESS_TOKEN_SECRET_PARAM_NAME], 44 | tweet_mode='extended' 45 | ) 46 | 47 | 48 | TWITTER = _create_twitter_api() 49 | -------------------------------------------------------------------------------- /test/unit/test_checkpoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from boto3.dynamodb.conditions import Attr, Or 4 | from botocore.exceptions import ClientError 5 | 6 | import checkpoint 7 | 8 | 9 | def test_last_id_no_record(mocker): 10 | mocker.patch.object(checkpoint, 'TABLE') 11 | checkpoint.TABLE.get_item.return_value = {} 12 | assert checkpoint.last_id() is None 13 | checkpoint.TABLE.get_item.assert_called_with( 14 | Key={'id': checkpoint.RECORD_KEY} 15 | ) 16 | 17 | 18 | def test_last_id_record_exists(mocker): 19 | mocker.patch.object(checkpoint, 'TABLE') 20 | checkpoint.TABLE.get_item.return_value = {'Item': {'since_id': 5}} 21 | assert checkpoint.last_id() == 5 22 | 23 | 24 | def test_update(mocker): 25 | mocker.patch.object(checkpoint, 'TABLE') 26 | checkpoint.update(5) 27 | checkpoint.TABLE.put_item.assert_called_with( 28 | Item={ 29 | 'id': checkpoint.RECORD_KEY, 30 | 'since_id': 5 31 | }, 32 | ConditionExpression=Or( 33 | Attr('id').not_exists(), 34 | Attr('since_id').lt(5) 35 | ) 36 | ) 37 | 38 | 39 | def test_update_condition_fails(mocker): 40 | mocker.patch.object(checkpoint, 'TABLE') 41 | checkpoint.TABLE.put_item.side_effect = ClientError( 42 | { 43 | 'Error': {'Code': 'ConditionalCheckFailedException'} 44 | }, 45 | 'PutItem' 46 | ) 47 | checkpoint.update(5) 48 | 49 | 50 | def test_update_other_error_code(mocker): 51 | mocker.patch.object(checkpoint, 'TABLE') 52 | checkpoint.TABLE.put_item.side_effect = ClientError( 53 | { 54 | 'Error': {'Code': 'SomethingElse'} 55 | }, 56 | 'PutItem' 57 | ) 58 | with pytest.raises(ClientError): 59 | checkpoint.update(5) 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *.sw[po] 107 | .DS_Store 108 | ~$*.ppt* 109 | -------------------------------------------------------------------------------- /test/unit/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import MagicMock 3 | 4 | # make sure we can find the app code 5 | import sys 6 | import os 7 | 8 | import test_constants 9 | 10 | my_path = os.path.dirname(os.path.abspath(__file__)) 11 | sys.path.insert(0, my_path + '/../../app/') 12 | 13 | # set expected config environment variables to test constants 14 | os.environ['AWS_DEFAULT_REGION'] = test_constants.REGION 15 | os.environ['SEARCH_CHECKPOINT_TABLE_NAME'] = test_constants.SEARCH_CHECKPOINT_TABLE_NAME 16 | os.environ['SEARCH_TEXT'] = test_constants.SEARCH_TEXT 17 | os.environ['TWEET_PROCESSOR_FUNCTION_NAME'] = test_constants.TWEET_PROCESSOR_FUNCTION_NAME 18 | os.environ['BATCH_SIZE'] = test_constants.BATCH_SIZE 19 | os.environ['STREAM_MODE_ENABLED'] = test_constants.STREAM_MODE_ENABLED 20 | os.environ['SSM_PARAMETER_PREFIX'] = test_constants.SSM_PARAMETER_PREFIX 21 | 22 | 23 | @pytest.fixture 24 | def mock_twitter_proxy(mocker): 25 | mock_client = MagicMock() 26 | mock_client.get_parameters.return_value = { 27 | 'Parameters': [ 28 | { 29 | 'Name': '/{}/consumer_key'.format(test_constants.SSM_PARAMETER_PREFIX), 30 | 'Value': test_constants.CONSUMER_KEY 31 | }, 32 | { 33 | 'Name': '/{}/consumer_secret'.format(test_constants.SSM_PARAMETER_PREFIX), 34 | 'Value': test_constants.CONSUMER_SECRET 35 | }, 36 | { 37 | 'Name': '/{}/access_token'.format(test_constants.SSM_PARAMETER_PREFIX), 38 | 'Value': test_constants.ACCESS_TOKEN 39 | }, 40 | { 41 | 'Name': '/{}/access_token_secret'.format(test_constants.SSM_PARAMETER_PREFIX), 42 | 'Value': test_constants.ACCESS_TOKEN_SECRET 43 | } 44 | ], 45 | 'InvalidParameters': [] 46 | } 47 | import boto3 48 | mocker.patch.object(boto3, 'client') 49 | boto3.client.return_value = mock_client 50 | import twitter_proxy 51 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Parameters: 4 | SearchText: 5 | Type: String 6 | Description: Non-URL-encoded search text poller should use when querying Twitter Search API. 7 | TweetProcessorFunctionName: 8 | Type: String 9 | Description: Name of lambda function that should be invoked to process tweets. Note, this must be a function name and not a function ARN. 10 | SSMParameterPrefix: 11 | Type: String 12 | Default: 'twitter-event-source' 13 | Description: > 14 | This app assumes API keys needed to use the Twitter API are stored as SecureStrings in SSM Parameter Store under the prefix defined by 15 | this parameter. See the app README for details. 16 | PollingFrequencyInMinutes: 17 | Type: Number 18 | MinValue: 1 19 | Default: 1 20 | Description: Frequency in minutes to poll for more tweets. 21 | BatchSize: 22 | Type: Number 23 | MinValue: 1 24 | Default: 15 25 | Description: Max number of tweets to send to the TweetProcessor lambda function on each invocation. 26 | StreamModeEnabled: 27 | Type: String 28 | Default: false 29 | AllowedValues: 30 | - true 31 | - false 32 | Description: If true, the app will remember the last tweet found and only invoke the tweet processor function for newer tweets. If false, the app will be stateless and invoke the tweet processor function with all tweets found in each polling cycle. 33 | 34 | Conditions: 35 | IsPollingFrequencyInMinutesSingular: !Equals [!Ref PollingFrequencyInMinutes, 1] 36 | 37 | Resources: 38 | TwitterSearchPoller: 39 | Type: AWS::Serverless::Function 40 | Properties: 41 | CodeUri: app/ 42 | Runtime: python3.8 43 | Handler: poller.handler 44 | Tracing: Active 45 | MemorySize: 128 46 | Timeout: 60 47 | Policies: 48 | - LambdaInvokePolicy: 49 | FunctionName: !Ref TweetProcessorFunctionName 50 | - DynamoDBCrudPolicy: 51 | TableName: !Ref SearchCheckpoint 52 | - Statement: 53 | Effect: Allow 54 | Action: 55 | - ssm:GetParameters 56 | Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSMParameterPrefix}/* 57 | Environment: 58 | Variables: 59 | SSM_PARAMETER_PREFIX: !Ref SSMParameterPrefix 60 | SEARCH_TEXT: !Ref SearchText 61 | SEARCH_CHECKPOINT_TABLE_NAME: !Ref SearchCheckpoint 62 | TWEET_PROCESSOR_FUNCTION_NAME: !Ref TweetProcessorFunctionName 63 | BATCH_SIZE: !Ref BatchSize 64 | STREAM_MODE_ENABLED: !Ref StreamModeEnabled 65 | Events: 66 | Timer: 67 | Type: Schedule 68 | Properties: 69 | Schedule: !If [IsPollingFrequencyInMinutesSingular, !Sub 'rate(${PollingFrequencyInMinutes} minute)', !Sub 'rate(${PollingFrequencyInMinutes} minutes)'] 70 | 71 | SearchCheckpoint: 72 | Type: AWS::Serverless::SimpleTable 73 | 74 | Outputs: 75 | TwitterSearchPollerFunctionName: 76 | Value: !Ref TwitterSearchPoller 77 | TwitterSearchPollerFunctionArn: 78 | Value: !GetAtt TwitterSearchPoller.Arn 79 | SearchCheckpointTableName: 80 | Value: !Ref SearchCheckpoint 81 | SearchCheckpointTableArn: 82 | Value: !GetAtt SearchCheckpoint.Arn 83 | -------------------------------------------------------------------------------- /test/unit/test_poller.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import json 4 | import os 5 | 6 | import test_constants 7 | 8 | 9 | @pytest.fixture 10 | def poller(mocker, mock_twitter_proxy): 11 | import poller 12 | mocker.patch.object(poller, 'LAMBDA') 13 | mocker.patch.object(poller, 'checkpoint') 14 | mocker.patch.object(poller, 'twitter_proxy') 15 | return poller 16 | 17 | 18 | def test_handler_single_batch(poller): 19 | tweets = [ 20 | {'id': 1}, 21 | {'id': 2} 22 | ] 23 | poller.twitter_proxy.search.side_effect = [ 24 | { 25 | 'statuses': tweets, 26 | 'search_metadata': { 27 | 'max_id': 2 28 | } 29 | }, 30 | { 31 | 'statuses': [] 32 | } 33 | ] 34 | 35 | poller.handler(None, None) 36 | 37 | assert poller.twitter_proxy.search.call_count == 2 38 | poller.twitter_proxy.search.assert_any_call(test_constants.SEARCH_TEXT, None) 39 | 40 | poller.LAMBDA.invoke.assert_called_once_with( 41 | FunctionName=test_constants.TWEET_PROCESSOR_FUNCTION_NAME, 42 | InvocationType='Event', 43 | Payload=json.dumps(tweets) 44 | ) 45 | poller.checkpoint.last_id.assert_not_called() 46 | poller.checkpoint.update.assert_not_called() 47 | 48 | 49 | def test_handler_multiple_batches(poller): 50 | tweets = [ 51 | {'id': 1}, 52 | {'id': 2}, 53 | {'id': 3}, 54 | {'id': 4}, 55 | {'id': 5} 56 | ] 57 | poller.twitter_proxy.search.side_effect = [ 58 | { 59 | 'statuses': tweets, 60 | 'search_metadata': { 61 | 'max_id': 5 62 | } 63 | }, 64 | { 65 | 'statuses': [] 66 | } 67 | ] 68 | poller.handler(None, None) 69 | 70 | assert poller.twitter_proxy.search.call_count == 2 71 | poller.twitter_proxy.search.assert_any_call(test_constants.SEARCH_TEXT, None) 72 | 73 | assert poller.LAMBDA.invoke.call_count == 3 74 | poller.LAMBDA.invoke.assert_any_call( 75 | FunctionName=test_constants.TWEET_PROCESSOR_FUNCTION_NAME, 76 | InvocationType='Event', 77 | Payload=json.dumps([{'id': 1}, {'id': 2}]) 78 | ) 79 | poller.LAMBDA.invoke.assert_any_call( 80 | FunctionName=test_constants.TWEET_PROCESSOR_FUNCTION_NAME, 81 | InvocationType='Event', 82 | Payload=json.dumps([{'id': 3}, {'id': 4}]) 83 | ) 84 | poller.LAMBDA.invoke.assert_any_call( 85 | FunctionName=test_constants.TWEET_PROCESSOR_FUNCTION_NAME, 86 | InvocationType='Event', 87 | Payload=json.dumps([{'id': 5}]) 88 | ) 89 | poller.checkpoint.last_id.assert_not_called() 90 | poller.checkpoint.update.assert_not_called() 91 | 92 | 93 | def test_handler_stream_mode_enabled(mocker, poller): 94 | mocker.patch.object(poller, 'STREAM_MODE_ENABLED') 95 | poller.STREAM_MODE_ENABLED = True 96 | 97 | poller.checkpoint.last_id.return_value = 3 98 | 99 | tweets = [ 100 | {'id': 4}, 101 | {'id': 5} 102 | ] 103 | poller.twitter_proxy.search.side_effect = [ 104 | { 105 | 'statuses': tweets, 106 | 'search_metadata': { 107 | 'max_id': 5 108 | } 109 | }, 110 | { 111 | 'statuses': [] 112 | } 113 | ] 114 | 115 | poller.handler(None, None) 116 | 117 | assert poller.twitter_proxy.search.call_count == 2 118 | poller.twitter_proxy.search.assert_any_call(test_constants.SEARCH_TEXT, 3) 119 | poller.twitter_proxy.search.assert_any_call(test_constants.SEARCH_TEXT, 5) 120 | 121 | poller.checkpoint.last_id.assert_called_once() 122 | poller.checkpoint.update.assert_called_once_with(5) 123 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-serverless-twitter-event-source/issues), or [recently closed](https://github.com/awslabs/aws-serverless-twitter-event-source/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-serverless-twitter-event-source/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-serverless-twitter-event-source/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Serverless Twitter Event Source 2 | 3 | ![Build Status](https://codebuild.us-east-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiTldCYjBXdGhocTJkRmtGK2k0REFHVVNsRllyK2hKTmZNWWR2T1creHhsZWVTMCszNC9hSGcyWklFMWE5ZWN6NDVOZnV4WTN4ekV3NDFlcys2L3ZjRmN3PSIsIml2UGFyYW1ldGVyU3BlYyI6Ikgyd084eEVRdGUzNDY4djMiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) 4 | 5 | This serverless app turns a twitter search query into an AWS Lambda event source by invoking a given lambda function to process tweets found by search. It works by periodically polling the freely available public [Twitter Standard Search API](https://developer.twitter.com/en/docs/tweets/search/overview/standard) and invoking a lambda function you provide to process tweets found. 6 | 7 | ## Architecture 8 | 9 | ![App Architecture](https://github.com/awslabs/aws-serverless-twitter-event-source/raw/master/images/app-architecture.png) 10 | 11 | 1. The TwitterSearchPoller lambda function is periodically triggered by a CloudWatch Events Rule. 12 | 1. In stream mode, a DynamoDB table is used to keep track of a checkpoint, which is the latest tweet timestamp found by past searches. 13 | 1. The poller function calls the Twitter Standard Search API and searches for tweets using the search text provided as an app parameter. 14 | 1. The TweetProcessor lambda function (provided by the app user) is invoked with any new tweets that were found after the last checkpoint. 15 | 1. If stream mode is not enabled, the TweetProcessor lambda function will be invoked with all search results found, regardless of whether they had been seen before. 16 | 1. Note, the TweetProcessor function is invoked asynchronously (Event invocation type). The app does not confirm that the lambda was able to successfully process the tweets. If you're concerned about tweets being lost due to failures in your lambda function, you should configure a DLQ on your lambda function so failed messages will end up on the DLQ automatically. See the [AWS Lambda DLQ documentation](https://docs.aws.amazon.com/lambda/latest/dg/dlq.html) for more information. 17 | 18 | ## Installation Steps 19 | 20 | This app is meant to be used as part of a larger application, so the recommended way to use it is to embed it as a nested app in your serverless application. To do this, paste the following into your SAM template: 21 | 22 | ```yaml 23 | TweetSource: 24 | Type: AWS::Serverless::Application 25 | Properties: 26 | Location: 27 | ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/aws-serverless-twitter-event-source 28 | SemanticVersion: 2.0.0 29 | Parameters: 30 | # Non-URL-encoded search text poller should use when querying Twitter Search API. 31 | SearchText: '#serverless -filter:nativeretweets' 32 | # Name of lambda function that should be invoked to process tweets. Note, this must be a function name and not a function ARN. 33 | TweetProcessorFunctionName: !Ref MyFunction 34 | # This app assumes API keys needed to use the Twitter API are stored as SecureStrings in SSM Parameter Store under the prefix 35 | # defined by this parameter. See the app README for details. 36 | #SSMParameterPrefix: twitter-event-source # Uncomment to override default value 37 | # Frequency in minutes to poll for more tweets. 38 | #PollingFrequencyInMinutes: 1 # Uncomment to override default value 39 | # Max number of tweets to send to the TweetProcessor lambda function on each invocation. 40 | #BatchSize: 15 # Uncomment to override default value 41 | # If true, the app will remember the last tweet found and only invoke the tweet processor function for newer tweets. 42 | # If false, the app will be stateless and invoke the tweet processor function with all tweets found in each polling cycle. 43 | #StreamModeEnabled: false # Uncomment to override default value 44 | ``` 45 | 46 | Alternatively, you can deploy the application into your account manually via the [aws-serverless-twitter-event-source SAR page](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:077246666028:applications~aws-serverless-twitter-event-source). 47 | 48 | 1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login 49 | 1. Go to the app's page on the [Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:077246666028:applications~aws-serverless-twitter-event-source) and click "Deploy" 50 | 1. Provide the required app parameters (see below for steps to create Twitter API parameters, e.g., Consumer Key) 51 | 52 | ### Twitter API Keys 53 | 54 | The app requires the following Twitter API Keys: Consumer Key (API Key), Consumer Secret (API Secret), Access Token, and Access Token Secret. The following steps walk you through registering the app with your Twitter account to create these values. 55 | 56 | 1. Create a [Twitter](https://twitter.com/) account if you do not already have one 57 | 1. Register a new application with your Twitter account: 58 | 1. Go to [http://twitter.com/oauth_clients/new](http://twitter.com/oauth_clients/new) 59 | 1. Click "Create New App" 60 | 1. Under Name, enter something descriptive (but unique), e.g., `aws-serverless-twitter-es` 61 | 1. Enter a description 62 | 1. Under Website, you can enter `https://github.com/awslabs/aws-serverless-twitter-event-source` 63 | 1. Leave Callback URL blank 64 | 1. Read and agree to the Twitter Developer Agreement 65 | 1. Click "Create your Twitter application" 66 | 1. (Optional, but recommended) Restrict the application permissions to read only 67 | 1. From the detail page of your Twitter application, click the "Permissions" tab 68 | 1. Under the "Access" section, make sure "Read only" is selected and click the "Update Settings" button 69 | 1. Generate an access token: 70 | 1. From the detail page of your Twitter application, click the "Keys and Access Tokens" tab 71 | 1. On this tab, you will already see the Consumer Key (API Key) and Consumer Secret (API Secret) values required by the app. 72 | 1. Scroll down to the Access Token section and click "Create my access token" 73 | 1. You will now have the Access Token and Access Token Secret values required by the app. 74 | 75 | ### Twitter API Key Setup 76 | 77 | The app expects to find the Twitter API keys as encrypted SecureString values in [SSM Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html). You can setup the required parameters via the AWS Console or using the following AWS CLI commands: 78 | 79 | ```text 80 | aws ssm put-parameter --name /twitter-event-source/consumer_key --value --type SecureString --overwrite 81 | aws ssm put-parameter --name /twitter-event-source/consumer_secret --value --type SecureString --overwrite 82 | aws ssm put-parameter --name /twitter-event-source/access_token --value --type SecureString --overwrite 83 | aws ssm put-parameter --name /twitter-event-source/access_token_secret --value --type SecureString --overwrite 84 | ``` 85 | 86 | ## App Parameters 87 | 88 | In addition to the Twitter API key parameters, the app also requires the following additional parameters: 89 | 90 | 1. `SearchText` (required) - This is the **non-URL-encoded** search text the app will use when polling the Twitter Standard Search API. See the [Search Tweets](https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators) help page to understand the available features. The [Twitter Search page](https://twitter.com/search) is a good place to manually test different searches, although note the standard search API generally returns different results, because it only indexes a sampling of tweets. 91 | 1. `TweetProcessorFunctionName` (required) - This is the name (not ARN) of the lambda function that will process tweets generated by the app. 92 | 1. `SSMParameterPrefix` (optional) - The prefix (without the leading `/`) of the SSM parameter names where the Twitter API keys can be found. Note, if you override this value, you need to make sure you have put the keys in SSM parameter store with names matching your prefix. For example, if you override the prefix to `myprefix`, then you will need to store the Twitter API keys in parameters with names `/myprefix/consumer_key`, `/myprefix/consumer_secret`, `/myprefix/access_token`, `/myprefix/access_token_secret`. Default: twitter-event-source. 93 | 1. `PollingFrequencyInMinutes` (optional) - The frequency at which the lambda will poll the Twitter Search API (in minutes). Default: 1. 94 | 1. `BatchSize` (optional) - The max number of tweets that will be sent to the TweetProcessor lambda function in a single invocation. Default: 15. 95 | 1. `StreamModeEnabled` (optional) - If true, the app will save the latest timestamp of the previous tweets found and only invoke the tweet processor function for newer tweets. If false, the app will be stateless and invoke the tweet processor function with all tweets found in each polling cycle. Default: false. 96 | 97 | ## App Outputs 98 | 99 | 1. `TwitterSearchPollerFunctionName` - Name of the search poller Lambda function. 100 | 1. `TwitterSearchPollerFunctionArn` - ARN of the search poller Lambda function. 101 | 1. `SearchCheckpointTableName` - Name of the search checkpoint table. 102 | 1. `SearchCheckpointTableArn` - ARN of the search checkpoint table. 103 | 104 | ## App Interface 105 | 106 | The aws-serverless-twitter-event-source app invokes the TweetProcessor function with a payload containing a JSON array of Tweet objects as they were returned from the Twitter search API. See the [Twitter API documentation](https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/intro-to-tweet-json) for the format of tweet objects. A single invocation of the TweetProcessor will never receive more tweets than the configured batch size, but it may receive less. 107 | 108 | The TweetProcessor is invoked asynchronously by the search poller, so there is no need to return a value from the TweetProcessor function. 109 | 110 | ## Upgrading from version 1.x 111 | 112 | aws-serverless-twitter-event-source v2 contains breaking changes from v1 so apps wishing to upgrade from v1 to v2 need to make changes to their existing TweetProcessor Lambda function. The major changes are: 113 | 114 | 1. Added support for extended mode so longer tweets are no longer truncated. No special upgrade steps are necessary for this change. 115 | 1. The app was previously sending the tweets to the TweetProcessor as a JSON array of strings, where each string was the Tweet JSON. This meant, the TweetProcessor function had to deserialize the JSON instead of letting Lambda's native deserialization handle it. As part of upgrading, the TweetProcessor should be updated to expect that the payload will now be an array of JSON objects, rather than an array of strings containing the JSON object data. 116 | 1. Twitter API keys are now fetched from SSM Parameter Store instead of being passed in as app parameters. When upgrading, you must follow the installation steps above to install your Twitter API Keys as SSM Parameter Store SecureStrings **before** deploying the upgraded app. 117 | 1. When stream mode is enabled, the app now stores the last tweet id processed instead of a timestamp. This allows the app to take advantage of the Twitter API's native support for passing in the last tweet id to continue reading only tweets after that id. If you have stream mode enabled, you will have to perform the following steps to upgrade: 118 | 1. Disable the CloudWatch Events Rule to stop the search poller from being invoked. 119 | 1. Manually delete the "checkpoint" row in the SearchCheckpoint DynamoDB table. 120 | 1. Re-enable the CloudWatch Events Rule to resume the search poller. 121 | 122 | ## License Summary 123 | 124 | This sample code is made available under a modified MIT license. See the LICENSE file. 125 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ede52aef097037bb433d017091bda9d8cf10f99c4cb493d9c79765d5fc4efc76" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "boto3": { 20 | "hashes": [ 21 | "sha256:293593dd93c0a5fe6088dd9a393002376824977c55c8e5756d53a1c0283473dd", 22 | "sha256:def153fb4773c55d89193529e8b0f829eab5fed57cee1d28f3cdcaa18b8837c8" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.24.74" 26 | }, 27 | "botocore": { 28 | "hashes": [ 29 | "sha256:e41a81a18511f2f9181b2a9ab302a55c0effecccbef846c55aad0c47bfdbefb9", 30 | "sha256:fc0a13ef6042e890e361cf408759230f8574409bb51f81740d2e5d8ad5d1fbea" 31 | ], 32 | "markers": "python_version >= '3.7'", 33 | "version": "==1.27.96" 34 | }, 35 | "certifi": { 36 | "hashes": [ 37 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 38 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 39 | ], 40 | "markers": "python_version >= '3.6'", 41 | "version": "==2022.12.7" 42 | }, 43 | "chardet": { 44 | "hashes": [ 45 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 46 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 47 | ], 48 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 49 | "version": "==4.0.0" 50 | }, 51 | "future": { 52 | "hashes": [ 53 | "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307" 54 | ], 55 | "index": "pypi", 56 | "version": "==0.18.3" 57 | }, 58 | "idna": { 59 | "hashes": [ 60 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 61 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 62 | ], 63 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 64 | "version": "==2.10" 65 | }, 66 | "jmespath": { 67 | "hashes": [ 68 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 69 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 70 | ], 71 | "markers": "python_version >= '3.7'", 72 | "version": "==1.0.1" 73 | }, 74 | "oauthlib": { 75 | "hashes": [ 76 | "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", 77 | "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" 78 | ], 79 | "markers": "python_version >= '3.6'", 80 | "version": "==3.2.2" 81 | }, 82 | "python-dateutil": { 83 | "hashes": [ 84 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 85 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 86 | ], 87 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 88 | "version": "==2.8.2" 89 | }, 90 | "python-twitter": { 91 | "hashes": [ 92 | "sha256:45855742f1095aa0c8c57b2983eee3b6b7f527462b50a2fa8437a8b398544d90", 93 | "sha256:4a420a6cb6ee9d0c8da457c8a8573f709c2ff2e1a7542e2d38807ebbfe8ebd1d" 94 | ], 95 | "index": "pypi", 96 | "version": "==3.5" 97 | }, 98 | "requests": { 99 | "hashes": [ 100 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 101 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 102 | ], 103 | "index": "pypi", 104 | "version": "==2.25.1" 105 | }, 106 | "requests-oauthlib": { 107 | "hashes": [ 108 | "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", 109 | "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" 110 | ], 111 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 112 | "version": "==1.3.1" 113 | }, 114 | "s3transfer": { 115 | "hashes": [ 116 | "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", 117 | "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" 118 | ], 119 | "markers": "python_version >= '3.7'", 120 | "version": "==0.6.0" 121 | }, 122 | "six": { 123 | "hashes": [ 124 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 125 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 126 | ], 127 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 128 | "version": "==1.16.0" 129 | }, 130 | "urllib3": { 131 | "hashes": [ 132 | "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", 133 | "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" 134 | ], 135 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 136 | "version": "==1.26.14" 137 | } 138 | }, 139 | "develop": { 140 | "arrow": { 141 | "hashes": [ 142 | "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1", 143 | "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2" 144 | ], 145 | "markers": "python_version >= '3.6'", 146 | "version": "==1.2.3" 147 | }, 148 | "attrs": { 149 | "hashes": [ 150 | "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", 151 | "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" 152 | ], 153 | "markers": "python_version >= '3.6'", 154 | "version": "==22.2.0" 155 | }, 156 | "autopep8": { 157 | "hashes": [ 158 | "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087", 159 | "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142" 160 | ], 161 | "index": "pypi", 162 | "version": "==1.7.0" 163 | }, 164 | "aws-lambda-builders": { 165 | "hashes": [ 166 | "sha256:32e26425ad626c7e2c41989c894c2c5f70cce7574ed7729e37cdd262a049fd8a", 167 | "sha256:38fcb9023df09f3c39504498cf45a213a29b176be5cec36126b13b77604731bd", 168 | "sha256:61e3f1e77b62ab72b97f822c09385ce16dc0e5478b52de7296a79570be41be73" 169 | ], 170 | "markers": "python_version >= '3.6'", 171 | "version": "==1.19.0" 172 | }, 173 | "aws-sam-cli": { 174 | "hashes": [ 175 | "sha256:5508e95476e44beb876bc9ca706c4d73fc1e2c0ada0cd635bd8b0a281de226b8", 176 | "sha256:a45f6afdcccabe0dec4f9ee57851af3b8a8acbe5aee88ae33a30ff3ee811023b" 177 | ], 178 | "index": "pypi", 179 | "version": "==1.56.1" 180 | }, 181 | "aws-sam-translator": { 182 | "hashes": [ 183 | "sha256:09668d12b5d330412421d30d4a8e826da6fe06f5a451f771c3b37f48f1b25889", 184 | "sha256:85bea2739e1b4a61b3e4add8a12f727d7a8e459e3da195dfd0cd2e756be054ec", 185 | "sha256:d375e9333c0262ed74b6d7ae90938060713ab17341f4e06c5cdbfd755902d9b4" 186 | ], 187 | "markers": "python_version >= '3.7' and python_version != '4.0' and python_version <= '4.0'", 188 | "version": "==1.50.0" 189 | }, 190 | "awscli": { 191 | "hashes": [ 192 | "sha256:0955a60f979fab9ccf9eb113c2f355e0b35cdef199f7335a2c75c690b5d1fdfd", 193 | "sha256:12e342a9da6111c3a9358966345ff6f46f098a6aa9fb8be19639fb8ffc03782a" 194 | ], 195 | "index": "pypi", 196 | "version": "==1.25.75" 197 | }, 198 | "backports.zoneinfo": { 199 | "hashes": [ 200 | "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf", 201 | "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", 202 | "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546", 203 | "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", 204 | "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570", 205 | "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", 206 | "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7", 207 | "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", 208 | "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722", 209 | "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582", 210 | "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc", 211 | "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b", 212 | "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", 213 | "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08", 214 | "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", 215 | "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" 216 | ], 217 | "markers": "python_version < '3.9'", 218 | "version": "==0.2.1" 219 | }, 220 | "binaryornot": { 221 | "hashes": [ 222 | "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", 223 | "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4" 224 | ], 225 | "version": "==0.4.4" 226 | }, 227 | "boto3": { 228 | "hashes": [ 229 | "sha256:293593dd93c0a5fe6088dd9a393002376824977c55c8e5756d53a1c0283473dd", 230 | "sha256:def153fb4773c55d89193529e8b0f829eab5fed57cee1d28f3cdcaa18b8837c8" 231 | ], 232 | "index": "pypi", 233 | "version": "==1.24.74" 234 | }, 235 | "botocore": { 236 | "hashes": [ 237 | "sha256:e41a81a18511f2f9181b2a9ab302a55c0effecccbef846c55aad0c47bfdbefb9", 238 | "sha256:fc0a13ef6042e890e361cf408759230f8574409bb51f81740d2e5d8ad5d1fbea" 239 | ], 240 | "markers": "python_version >= '3.7'", 241 | "version": "==1.27.96" 242 | }, 243 | "certifi": { 244 | "hashes": [ 245 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 246 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 247 | ], 248 | "markers": "python_version >= '3.6'", 249 | "version": "==2022.12.7" 250 | }, 251 | "cfn-lint": { 252 | "hashes": [ 253 | "sha256:a83c6844463d79dfaa2f7747dfc98282af901502a7db4c66cb7200262e3cc1d0", 254 | "sha256:b5992f52a86e6ef0a150fabbb4d131bbf626eddd4154ca708193c1d233a7efca" 255 | ], 256 | "index": "pypi", 257 | "version": "==0.65.0" 258 | }, 259 | "chardet": { 260 | "hashes": [ 261 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 262 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 263 | ], 264 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 265 | "version": "==4.0.0" 266 | }, 267 | "chevron": { 268 | "hashes": [ 269 | "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", 270 | "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443" 271 | ], 272 | "version": "==0.14.0" 273 | }, 274 | "click": { 275 | "hashes": [ 276 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 277 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 278 | ], 279 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 280 | "version": "==7.1.2" 281 | }, 282 | "colorama": { 283 | "hashes": [ 284 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 285 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 286 | ], 287 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 288 | "version": "==0.4.4" 289 | }, 290 | "cookiecutter": { 291 | "hashes": [ 292 | "sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022", 293 | "sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5" 294 | ], 295 | "markers": "python_version >= '3.7'", 296 | "version": "==2.1.1" 297 | }, 298 | "coverage": { 299 | "extras": [ 300 | "toml" 301 | ], 302 | "hashes": [ 303 | "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45", 304 | "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809", 305 | "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4", 306 | "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b", 307 | "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7", 308 | "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0", 309 | "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0", 310 | "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea", 311 | "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2", 312 | "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a", 313 | "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45", 314 | "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b", 315 | "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209", 316 | "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca", 317 | "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab", 318 | "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095", 319 | "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7", 320 | "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6", 321 | "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af", 322 | "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499", 323 | "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831", 324 | "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637", 325 | "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2", 326 | "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb", 327 | "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029", 328 | "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc", 329 | "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8", 330 | "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f", 331 | "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2", 332 | "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d", 333 | "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289", 334 | "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c", 335 | "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded", 336 | "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96", 337 | "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0", 338 | "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904", 339 | "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21", 340 | "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89", 341 | "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78", 342 | "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad", 343 | "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196", 344 | "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd", 345 | "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0", 346 | "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882", 347 | "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757", 348 | "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16", 349 | "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0", 350 | "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47", 351 | "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40", 352 | "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1", 353 | "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3" 354 | ], 355 | "markers": "python_version >= '3.7'", 356 | "version": "==7.0.5" 357 | }, 358 | "dateparser": { 359 | "hashes": [ 360 | "sha256:c47b6e4b8c4b2b2a21690111b6571b6991295ba327ec6503753abeebf5e80696", 361 | "sha256:e703db1815270c020552f4b3e3a981937b48b2cbcfcef5347071b74788dd9214" 362 | ], 363 | "markers": "python_version >= '3.7'", 364 | "version": "==1.1.6" 365 | }, 366 | "docker": { 367 | "hashes": [ 368 | "sha256:03a46400c4080cb6f7aa997f881ddd84fef855499ece219d75fbdb53289c17ab", 369 | "sha256:26eebadce7e298f55b76a88c4f8802476c5eaddbdbe38dbc6cce8781c47c9b54" 370 | ], 371 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 372 | "version": "==4.2.2" 373 | }, 374 | "docutils": { 375 | "hashes": [ 376 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 377 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 378 | ], 379 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 380 | "version": "==0.16" 381 | }, 382 | "flake8": { 383 | "hashes": [ 384 | "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", 385 | "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" 386 | ], 387 | "index": "pypi", 388 | "version": "==5.0.4" 389 | }, 390 | "flask": { 391 | "hashes": [ 392 | "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196", 393 | "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22" 394 | ], 395 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 396 | "version": "==1.1.4" 397 | }, 398 | "idna": { 399 | "hashes": [ 400 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 401 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 402 | ], 403 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 404 | "version": "==2.10" 405 | }, 406 | "iniconfig": { 407 | "hashes": [ 408 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 409 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 410 | ], 411 | "markers": "python_version >= '3.7'", 412 | "version": "==2.0.0" 413 | }, 414 | "itsdangerous": { 415 | "hashes": [ 416 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 417 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 418 | ], 419 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 420 | "version": "==1.1.0" 421 | }, 422 | "jinja2": { 423 | "hashes": [ 424 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 425 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 426 | ], 427 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 428 | "version": "==2.11.3" 429 | }, 430 | "jinja2-time": { 431 | "hashes": [ 432 | "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", 433 | "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa" 434 | ], 435 | "version": "==0.2.0" 436 | }, 437 | "jmespath": { 438 | "hashes": [ 439 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 440 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 441 | ], 442 | "markers": "python_version >= '3.7'", 443 | "version": "==1.0.1" 444 | }, 445 | "jschema-to-python": { 446 | "hashes": [ 447 | "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91", 448 | "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05" 449 | ], 450 | "markers": "python_version >= '2.7'", 451 | "version": "==1.2.3" 452 | }, 453 | "jsonpatch": { 454 | "hashes": [ 455 | "sha256:26ac385719ac9f54df8a2f0827bb8253aa3ea8ab7b3368457bcdb8c14595a397", 456 | "sha256:b6ddfe6c3db30d81a96aaeceb6baf916094ffa23d7dd5fa2c13e13f8b6e600c2" 457 | ], 458 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 459 | "version": "==1.32" 460 | }, 461 | "jsonpickle": { 462 | "hashes": [ 463 | "sha256:032538804795e73b94ead410800ac387fdb6de98f8882ac957fcd247e3a85200", 464 | "sha256:130d8b293ea0add3845de311aaba55e6d706d0bb17bc123bd2c8baf8a39ac77c" 465 | ], 466 | "markers": "python_version >= '3.7'", 467 | "version": "==3.0.1" 468 | }, 469 | "jsonpointer": { 470 | "hashes": [ 471 | "sha256:51801e558539b4e9cd268638c078c6c5746c9ac96bc38152d443400e4f3793e9", 472 | "sha256:97cba51526c829282218feb99dab1b1e6bdf8efd1c43dc9d57be093c0d69c99a" 473 | ], 474 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 475 | "version": "==2.3" 476 | }, 477 | "jsonschema": { 478 | "hashes": [ 479 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 480 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 481 | ], 482 | "version": "==3.2.0" 483 | }, 484 | "junit-xml": { 485 | "hashes": [ 486 | "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732" 487 | ], 488 | "version": "==1.9" 489 | }, 490 | "markupsafe": { 491 | "hashes": [ 492 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 493 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 494 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 495 | "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", 496 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 497 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 498 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 499 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 500 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 501 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 502 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 503 | "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", 504 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 505 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 506 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 507 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 508 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 509 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 510 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 511 | "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", 512 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 513 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 514 | "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", 515 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 516 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 517 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 518 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 519 | "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", 520 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 521 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 522 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 523 | "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", 524 | "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", 525 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 526 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 527 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 528 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 529 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 530 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 531 | "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", 532 | "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", 533 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 534 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 535 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 536 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 537 | "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", 538 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 539 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 540 | "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", 541 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 542 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 543 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 544 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 545 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 546 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 547 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 548 | "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", 549 | "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", 550 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 551 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 552 | "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", 553 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 554 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 555 | "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", 556 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 557 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 558 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 559 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 560 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 561 | ], 562 | "markers": "python_version >= '3.6'", 563 | "version": "==2.0.1" 564 | }, 565 | "mccabe": { 566 | "hashes": [ 567 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 568 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 569 | ], 570 | "markers": "python_version >= '3.6'", 571 | "version": "==0.7.0" 572 | }, 573 | "networkx": { 574 | "hashes": [ 575 | "sha256:230d388117af870fce5647a3c52401fcf753e94720e6ea6b4197a5355648885e", 576 | "sha256:e435dfa75b1d7195c7b8378c3859f0445cd88c6b0375c181ed66823a9ceb7524" 577 | ], 578 | "markers": "python_version >= '3.8'", 579 | "version": "==2.8.8" 580 | }, 581 | "packaging": { 582 | "hashes": [ 583 | "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", 584 | "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" 585 | ], 586 | "markers": "python_version >= '3.7'", 587 | "version": "==23.0" 588 | }, 589 | "pbr": { 590 | "hashes": [ 591 | "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b", 592 | "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3" 593 | ], 594 | "markers": "python_version >= '2.6'", 595 | "version": "==5.11.1" 596 | }, 597 | "pluggy": { 598 | "hashes": [ 599 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 600 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 601 | ], 602 | "markers": "python_version >= '3.6'", 603 | "version": "==1.0.0" 604 | }, 605 | "py": { 606 | "hashes": [ 607 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 608 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 609 | ], 610 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 611 | "version": "==1.11.0" 612 | }, 613 | "pyasn1": { 614 | "hashes": [ 615 | "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", 616 | "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", 617 | "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", 618 | "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", 619 | "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", 620 | "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", 621 | "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", 622 | "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", 623 | "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", 624 | "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", 625 | "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", 626 | "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", 627 | "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" 628 | ], 629 | "version": "==0.4.8" 630 | }, 631 | "pycodestyle": { 632 | "hashes": [ 633 | "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", 634 | "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" 635 | ], 636 | "markers": "python_version >= '3.6'", 637 | "version": "==2.9.1" 638 | }, 639 | "pydocstyle": { 640 | "hashes": [ 641 | "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc", 642 | "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4" 643 | ], 644 | "index": "pypi", 645 | "version": "==6.1.1" 646 | }, 647 | "pyflakes": { 648 | "hashes": [ 649 | "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", 650 | "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3" 651 | ], 652 | "markers": "python_version >= '3.6'", 653 | "version": "==2.5.0" 654 | }, 655 | "pyrsistent": { 656 | "hashes": [ 657 | "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", 658 | "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", 659 | "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", 660 | "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", 661 | "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", 662 | "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", 663 | "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", 664 | "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", 665 | "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", 666 | "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", 667 | "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", 668 | "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", 669 | "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", 670 | "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", 671 | "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", 672 | "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", 673 | "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", 674 | "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", 675 | "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", 676 | "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", 677 | "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", 678 | "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", 679 | "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", 680 | "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", 681 | "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", 682 | "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", 683 | "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" 684 | ], 685 | "markers": "python_version >= '3.7'", 686 | "version": "==0.19.3" 687 | }, 688 | "pytest": { 689 | "hashes": [ 690 | "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7", 691 | "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39" 692 | ], 693 | "index": "pypi", 694 | "version": "==7.1.3" 695 | }, 696 | "pytest-cov": { 697 | "hashes": [ 698 | "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", 699 | "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" 700 | ], 701 | "index": "pypi", 702 | "version": "==3.0.0" 703 | }, 704 | "pytest-mock": { 705 | "hashes": [ 706 | "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2", 707 | "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948" 708 | ], 709 | "index": "pypi", 710 | "version": "==3.8.2" 711 | }, 712 | "python-dateutil": { 713 | "hashes": [ 714 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 715 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 716 | ], 717 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 718 | "version": "==2.8.2" 719 | }, 720 | "python-slugify": { 721 | "hashes": [ 722 | "sha256:003aee64f9fd955d111549f96c4b58a3f40b9319383c70fad6277a4974bbf570", 723 | "sha256:7a0f21a39fa6c1c4bf2e5984c9b9ae944483fd10b54804cb0e23a3ccd4954f0b" 724 | ], 725 | "markers": "python_version >= '3.7'", 726 | "version": "==7.0.0" 727 | }, 728 | "pytz": { 729 | "hashes": [ 730 | "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", 731 | "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" 732 | ], 733 | "version": "==2022.7.1" 734 | }, 735 | "pyyaml": { 736 | "hashes": [ 737 | "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", 738 | "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", 739 | "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", 740 | "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", 741 | "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", 742 | "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", 743 | "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", 744 | "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", 745 | "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", 746 | "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", 747 | "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", 748 | "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", 749 | "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", 750 | "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", 751 | "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", 752 | "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", 753 | "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", 754 | "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", 755 | "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", 756 | "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", 757 | "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", 758 | "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", 759 | "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", 760 | "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", 761 | "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", 762 | "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", 763 | "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", 764 | "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", 765 | "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" 766 | ], 767 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 768 | "version": "==5.4.1" 769 | }, 770 | "regex": { 771 | "hashes": [ 772 | "sha256:0de8ad66b08c3e673b61981b9e3626f8784d5564f8c3928e2ad408c0eb5ac38c", 773 | "sha256:1f1125bc5172ab3a049bc6f4b9c0aae95a2a2001a77e6d6e4239fa3653e202b5", 774 | "sha256:255791523f80ea8e48e79af7120b4697ef3b74f6886995dcdb08c41f8e516be0", 775 | "sha256:28040e89a04b60d579c69095c509a4f6a1a5379cd865258e3a186b7105de72c6", 776 | "sha256:37868075eda024470bd0feab872c692ac4ee29db1e14baec103257bf6cc64346", 777 | "sha256:3b71213ec3bad9a5a02e049f2ec86b3d7c3e350129ae0f4e2f99c12b5da919ed", 778 | "sha256:3be40f720af170a6b20ddd2ad7904c58b13d2b56f6734ee5d09bbdeed2fa4816", 779 | "sha256:42952d325439ef223e4e9db7ee6d9087b5c68c5c15b1f9de68e990837682fc7b", 780 | "sha256:470f2c882f2672d8eeda8ab27992aec277c067d280b52541357e1acd7e606dae", 781 | "sha256:4907fb0f9b9309a5bded72343e675a252c2589a41871874feace9a05a540241e", 782 | "sha256:4d87459ad3ab40cd8493774f8a454b2e490d8e729e7e402a0625867a983e4e02", 783 | "sha256:4fa7ba9ab2eba7284e0d7d94f61df7af86015b0398e123331362270d71fab0b9", 784 | "sha256:5b34d2335d6aedec7dcadd3f8283b9682fadad8b9b008da8788d2fce76125ebe", 785 | "sha256:6348a7ab2a502cbdd0b7fd0496d614007489adb7361956b38044d1d588e66e04", 786 | "sha256:638e98d069b14113e8afba6a54d1ca123f712c0d105e67c1f9211b2a825ef926", 787 | "sha256:66696c8336a1b5d1182464f3af3427cc760118f26d0b09a2ddc16a976a4d2637", 788 | "sha256:78cf6a1e023caf5e9a982f5377414e1aeac55198831b852835732cfd0a0ca5ff", 789 | "sha256:81e125d9ba54c34579e4539a967e976a3c56150796674aec318b1b2f49251be7", 790 | "sha256:81fdc90f999b2147fc62e303440c424c47e5573a9b615ed5d43a5b832efcca9e", 791 | "sha256:87e9c489aa98f50f367fb26cc9c8908d668e9228d327644d7aa568d47e456f47", 792 | "sha256:8c1ad61fa024195136a6b7b89538030bd00df15f90ac177ca278df9b2386c96f", 793 | "sha256:9910869c472e5a6728680ca357b5846546cbbd2ab3ad5bef986ef0bc438d0aa6", 794 | "sha256:9925985be05d54b3d25fd6c1ea8e50ff1f7c2744c75bdc4d3b45c790afa2bcb3", 795 | "sha256:9a0b0db6b49da7fa37ca8eddf9f40a8dbc599bad43e64f452284f37b6c34d91c", 796 | "sha256:9c065d95a514a06b92a5026766d72ac91bfabf581adb5b29bc5c91d4b3ee9b83", 797 | "sha256:a6f08187136f11e430638c2c66e1db091105d7c2e9902489f0dbc69b44c222b4", 798 | "sha256:ad0517df22a97f1da20d8f1c8cb71a5d1997fa383326b81f9cf22c9dadfbdf34", 799 | "sha256:b345ecde37c86dd7084c62954468a4a655fd2d24fd9b237949dd07a4d0dd6f4c", 800 | "sha256:b55442650f541d195a535ccec33078c78a9521973fb960923da7515e9ed78fa6", 801 | "sha256:c2b180ed30856dfa70cfe927b0fd38e6b68198a03039abdbeb1f2029758d87e7", 802 | "sha256:c9e30838df7bfd20db6466fd309d9b580d32855f8e2c2e6d74cf9da27dcd9b63", 803 | "sha256:cae4099031d80703954c39680323dabd87a69b21262303160776aa0e55970ca0", 804 | "sha256:ce7b1cca6c23f19bee8dc40228d9c314d86d1e51996b86f924aca302fc8f8bf9", 805 | "sha256:d0861e7f6325e821d5c40514c551fd538b292f8cc3960086e73491b9c5d8291d", 806 | "sha256:d331f238a7accfbbe1c4cd1ba610d4c087b206353539331e32a8f05345c74aec", 807 | "sha256:e07049cece3462c626d650e8bf42ddbca3abf4aa08155002c28cb6d9a5a281e2", 808 | "sha256:e2cb7d4909ed16ed35729d38af585673f1f0833e73dfdf0c18e5be0061107b99", 809 | "sha256:e3770781353a4886b68ef10cec31c1f61e8e3a0be5f213c2bb15a86efd999bc4", 810 | "sha256:e502f8d4e5ef714bcc2c94d499684890c94239526d61fdf1096547db91ca6aa6", 811 | "sha256:e6f2d2f93001801296fe3ca86515eb04915472b5380d4d8752f09f25f0b9b0ed", 812 | "sha256:f588209d3e4797882cd238195c175290dbc501973b10a581086b5c6bcd095ffb" 813 | ], 814 | "version": "==2021.9.30" 815 | }, 816 | "requests": { 817 | "hashes": [ 818 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 819 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 820 | ], 821 | "index": "pypi", 822 | "version": "==2.25.1" 823 | }, 824 | "rsa": { 825 | "hashes": [ 826 | "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", 827 | "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" 828 | ], 829 | "markers": "python_version >= '3.5' and python_version < '4'", 830 | "version": "==4.7.2" 831 | }, 832 | "s3transfer": { 833 | "hashes": [ 834 | "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", 835 | "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" 836 | ], 837 | "markers": "python_version >= '3.7'", 838 | "version": "==0.6.0" 839 | }, 840 | "sarif-om": { 841 | "hashes": [ 842 | "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911", 843 | "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98" 844 | ], 845 | "markers": "python_version >= '2.7'", 846 | "version": "==1.0.4" 847 | }, 848 | "serverlessrepo": { 849 | "hashes": [ 850 | "sha256:671f48038123f121437b717ed51f253a55775590f00fbab6fbc6a01f8d05c017", 851 | "sha256:b99c69be8ce87ccc48103fbe371ba7b148c3374c57862e59118c402522e5ed52" 852 | ], 853 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 854 | "version": "==0.1.10" 855 | }, 856 | "setuptools": { 857 | "hashes": [ 858 | "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab", 859 | "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6" 860 | ], 861 | "markers": "python_version >= '3.7'", 862 | "version": "==66.0.0" 863 | }, 864 | "six": { 865 | "hashes": [ 866 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 867 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 868 | ], 869 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 870 | "version": "==1.16.0" 871 | }, 872 | "snowballstemmer": { 873 | "hashes": [ 874 | "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", 875 | "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" 876 | ], 877 | "version": "==2.2.0" 878 | }, 879 | "text-unidecode": { 880 | "hashes": [ 881 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", 882 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" 883 | ], 884 | "version": "==1.3" 885 | }, 886 | "toml": { 887 | "hashes": [ 888 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 889 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 890 | ], 891 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 892 | "version": "==0.10.2" 893 | }, 894 | "tomli": { 895 | "hashes": [ 896 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 897 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 898 | ], 899 | "markers": "python_version >= '3.7'", 900 | "version": "==2.0.1" 901 | }, 902 | "tomlkit": { 903 | "hashes": [ 904 | "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117", 905 | "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754" 906 | ], 907 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 908 | "version": "==0.7.2" 909 | }, 910 | "typing-extensions": { 911 | "hashes": [ 912 | "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", 913 | "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", 914 | "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" 915 | ], 916 | "version": "==3.10.0.0" 917 | }, 918 | "tzlocal": { 919 | "hashes": [ 920 | "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559", 921 | "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12" 922 | ], 923 | "markers": "python_version >= '3.6'", 924 | "version": "==3.0" 925 | }, 926 | "urllib3": { 927 | "hashes": [ 928 | "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", 929 | "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" 930 | ], 931 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 932 | "version": "==1.26.14" 933 | }, 934 | "watchdog": { 935 | "hashes": [ 936 | "sha256:0237db4d9024859bea27d0efb59fe75eef290833fd988b8ead7a879b0308c2db", 937 | "sha256:104266a778906ae0e971368d368a65c4cd032a490a9fca5ba0b78c6c7ae11720", 938 | "sha256:188145185c08c73c56f1478ccf1f0f0f85101191439679b35b6b100886ce0b39", 939 | "sha256:1a62a4671796dc93d1a7262286217d9e75823c63d4c42782912d39a506d30046", 940 | "sha256:255a32d44bbbe62e52874ff755e2eefe271b150e0ec240ad7718a62a7a7a73c4", 941 | "sha256:3d6405681471ebe0beb3aa083998c4870e48b57f8afdb45ea1b5957cc5cf1014", 942 | "sha256:4b219d46d89cfa49af1d73175487c14a318a74cb8c5442603fd13c6a5b418c86", 943 | "sha256:581e3548159fe7d2a9f377a1fbcb41bdcee46849cca8ab803c7ac2e5e04ec77c", 944 | "sha256:58ebb1095ee493008a7789d47dd62e4999505d82be89fc884d473086fccc6ebd", 945 | "sha256:598d772beeaf9c98d0df946fbabf0c8365dd95ea46a250c224c725fe0c4730bc", 946 | "sha256:668391e6c32742d76e5be5db6bf95c455fa4b3d11e76a77c13b39bccb3a47a72", 947 | "sha256:6ef9fe57162c4c361692620e1d9167574ba1975ee468b24051ca11c9bba6438e", 948 | "sha256:91387ee2421f30b75f7ff632c9d48f76648e56bf346a7c805c0a34187a93aab4", 949 | "sha256:a42e6d652f820b2b94cd03156c62559a2ea68d476476dfcd77d931e7f1012d4a", 950 | "sha256:a6471517315a8541a943c00b45f1d252e36898a3ae963d2d52509b89a50cb2b9", 951 | "sha256:d34ce2261f118ecd57eedeef95fc2a495fc4a40b3ed7b3bf0bd7a8ccc1ab4f8f", 952 | "sha256:edcd9ef3fd460bb8a98eb1fcf99941e9fd9f275f45f1a82cb1359ec92975d647" 953 | ], 954 | "markers": "python_version >= '3.6'", 955 | "version": "==2.1.2" 956 | }, 957 | "websocket-client": { 958 | "hashes": [ 959 | "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574", 960 | "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59" 961 | ], 962 | "markers": "python_version >= '3.7'", 963 | "version": "==1.4.2" 964 | }, 965 | "werkzeug": { 966 | "hashes": [ 967 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 968 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 969 | ], 970 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 971 | "version": "==1.0.1" 972 | }, 973 | "wheel": { 974 | "hashes": [ 975 | "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac", 976 | "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8" 977 | ], 978 | "markers": "python_version >= '3.7'", 979 | "version": "==0.38.4" 980 | } 981 | } 982 | } 983 | --------------------------------------------------------------------------------