├── tests ├── unit │ ├── __init__.py │ ├── test_app.py │ └── test_integration.py ├── deploy.sh └── template.yaml ├── updater ├── __init__.py ├── requirements.txt └── app.py ├── .github ├── workflows │ ├── reviewdog.yml │ ├── test.yml │ ├── update.yml │ └── codeql-analysis.yml └── dependabot.yml ├── Pipfile ├── Makefile ├── deploy-shogo82148.sh ├── download-certificate.sh ├── LICENSE ├── README.md ├── .gitignore ├── template.yaml └── Pipfile.lock /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /updater/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # deploy a cloudformation tack for test. 4 | 5 | set -ux 6 | 7 | ROOT=$(cd "$(dirname "$0/")" && pwd) 8 | 9 | aws cloudformation --region ap-northeast-1 deploy \ 10 | --stack-name acme-cert-updater-test \ 11 | --template-file "${ROOT}/template.yaml" \ 12 | --capabilities CAPABILITY_IAM 13 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | on: [pull_request] 3 | 4 | jobs: 5 | actions-cfn-lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v6 9 | - uses: shogo82148/actions-cfn-lint@v4 10 | with: 11 | reporter: github-pr-review 12 | level: warning 13 | cfn_lint_args: "**/*.yaml" 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | pytest = "*" 9 | mypy = "*" 10 | 11 | [packages] 12 | boto3 = "*" 13 | botocore = "*" 14 | 15 | configobj = "*" 16 | certbot = "*" 17 | certbot-dns-route53 = "*" 18 | pyyaml = ">=4.2b1" 19 | 20 | [requires] 21 | python_version = "3.14" 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: ## Show this text. 2 | # http://postd.cc/auto-documented-makefile/ 3 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 4 | 5 | .PHONY: help build test validate 6 | 7 | build: ## Build SAM application. 8 | sam build --use-container --debug 9 | cp README.md .aws-sam/build/ 10 | cp LICENSE .aws-sam/build/ 11 | 12 | test: ## run tests 13 | python -m pytest tests/ -v 14 | 15 | validate: ## validate SAM template 16 | sam validate 17 | 18 | release: build ## Release the application to AWS Serverless Application Repository 19 | sam package \ 20 | --template-file .aws-sam/build/template.yaml \ 21 | --output-template-file .aws-sam/build/packaged.yaml \ 22 | --s3-bucket shogo82148-sam 23 | sam publish \ 24 | --template .aws-sam/build/packaged.yaml \ 25 | --region us-east-1 26 | -------------------------------------------------------------------------------- /deploy-shogo82148.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # deploy the author's AWS Account for testing 4 | 5 | set -uex 6 | 7 | CURRENT=$(cd "$(dirname "$0")" && pwd) 8 | 9 | cd "$CURRENT" 10 | make build 11 | 12 | sam package \ 13 | --region ap-northeast-1 \ 14 | --template-file "$CURRENT/.aws-sam/build/template.yaml" \ 15 | --output-template-file "$CURRENT/.aws-sam/build/packaged-test.yaml" \ 16 | --s3-bucket shogo82148-test \ 17 | --s3-prefix acme-cert-updater/resource 18 | 19 | sam deploy \ 20 | --template "$CURRENT/.aws-sam/build/packaged-test.yaml" \ 21 | --capabilities CAPABILITY_AUTO_EXPAND CAPABILITY_IAM \ 22 | --stack-name acme-cert-updater-deployment-test \ 23 | --parameter-overrides Domains=shogo82148.com Email=shogo82148@gmail.com Environment=staging BucketName=shogo82148-test Prefix=acme-cert-updater/cert HostedZone=Z1TR8BQNS8S1I7 LogLevel=DEBUG 24 | -------------------------------------------------------------------------------- /download-certificate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ $# -lt 3 ]]; then 4 | echo "Usage: $(basename "$0") BUCKET_NAME OBJECT_KEY_NAME OUTPUT_DIRECTORY COMMAND" 5 | exit 2 6 | fi 7 | 8 | BUCKET=$1 9 | OBJECT=$2 10 | OUTPUT=$3 11 | 12 | set -eu 13 | JSON=$(aws s3 cp "s3://$BUCKET/$OBJECT" -) 14 | if [[ -f "$OUTPUT/timestamp.txt" ]] && [[ ! $(echo "$JSON" | jq -r .timestamp) > $(cat "$OUTPUT/timestamp.txt") ]]; then 15 | exit 0 16 | fi 17 | 18 | aws s3 cp --only-show-errors "s3://$BUCKET/$(echo "$JSON" | jq -r .cert.cert)" "$OUTPUT" 19 | aws s3 cp --only-show-errors "s3://$BUCKET/$(echo "$JSON" | jq -r .cert.chain)" "$OUTPUT" 20 | aws s3 cp --only-show-errors "s3://$BUCKET/$(echo "$JSON" | jq -r .cert.fullchain)" "$OUTPUT" 21 | aws s3 cp --only-show-errors "s3://$BUCKET/$(echo "$JSON" | jq -r .cert.privkey)" "$OUTPUT" 22 | echo "$JSON" | jq -r .timestamp > "$OUTPUT/timestamp.txt" 23 | 24 | shift 3 25 | if [ $# -eq 0 ]; then 26 | exit 0 27 | fi 28 | 29 | exec "$@" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ICHINOSE Shogo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /updater/requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | acme==5.2.1; python_version >= '3.10' 3 | boto3==1.42.4; python_version >= '3.9' 4 | botocore==1.42.4; python_version >= '3.9' 5 | certbot==5.2.1; python_version >= '3.10' 6 | certbot-dns-route53==5.2.1; python_version >= '3.10' 7 | certifi==2025.11.12; python_version >= '3.7' 8 | cffi==2.0.0; python_version >= '3.9' 9 | charset-normalizer==3.4.4; python_version >= '3.7' 10 | configargparse==1.7.1; python_version >= '3.6' 11 | configobj==5.0.9; python_version >= '3.7' 12 | cryptography==46.0.3; python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1' 13 | distro==1.9.0; python_version >= '3.6' 14 | idna==3.11; python_version >= '3.8' 15 | jmespath==1.0.1; python_version >= '3.7' 16 | josepy==2.2.0; python_full_version >= '3.9.2' 17 | parsedatetime==2.6 18 | pycparser==2.23; python_version >= '3.8' 19 | pyopenssl==25.3.0; python_version >= '3.7' 20 | pyrfc3339==2.1.0; python_version >= '3.9' 21 | python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' 22 | pyyaml==6.0.3; python_version >= '3.8' 23 | requests==2.32.5; python_version >= '3.9' 24 | s3transfer==0.16.0; python_version >= '3.9' 25 | six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' 26 | urllib3==2.6.0; python_version >= '3.9' 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | concurrency: 9 | group: integration-test 10 | cancel-in-progress: false 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | id-token: write 17 | contents: read 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | 22 | - uses: actions/setup-python@v6 23 | with: 24 | python-version: "3.14" 25 | - name: install dependencies 26 | run: | 27 | pip install --upgrade pip 28 | pip install pipenv 29 | pipenv update --dev 30 | 31 | - name: Configure AWS Credentials 32 | uses: fuller-inc/actions-aws-assume-role@v1 33 | with: 34 | aws-region: ap-northeast-1 35 | role-to-assume: arn:aws:iam::445285296882:role/acme-cert-updater-test-TestRole-SPRNY1U43M59 36 | role-session-tagging: true 37 | 38 | - name: Login to ECR Public 39 | run: | 40 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws 41 | 42 | - name: test 43 | run: | 44 | pipenv run make test 45 | pipenv run make validate 46 | pipenv run make build 47 | 48 | - name: Logout from ECR Public 49 | run: | 50 | docker login public.ecr.aws 51 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update 2 | on: 3 | schedule: 4 | - cron: "53 12 * * *" 5 | workflow_dispatch: 6 | 7 | jobs: 8 | requirements: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | contents: write 13 | pull-requests: write 14 | 15 | steps: 16 | - id: generate_token 17 | uses: shogo82148/actions-github-app-token@v1 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v6 21 | 22 | - uses: actions/setup-python@v6 23 | with: 24 | python-version: "3.14" 25 | 26 | - name: update 27 | id: update 28 | run: | 29 | pip install --upgrade pip 30 | pip install pipenv 31 | pipenv update --dev 32 | pipenv requirements > updater/requirements_new.txt 33 | 34 | # boto3 and botocore are updated very often, so ignore them 35 | diff <(cat updater/requirements.txt | grep -v -E 'boto(3|core)') \ 36 | <(cat updater/requirements_new.txt | grep -v -E 'boto(3|core)') && true 37 | echo "result=$?" >> "$GITHUB_OUTPUT" 38 | mv updater/requirements_new.txt updater/requirements.txt 39 | 40 | - name: commit 41 | if: steps.update.outputs.result == '1' 42 | uses: shogo82148/actions-commit-and-create-pr@v1 43 | with: 44 | github-token: ${{ steps.generate_token.outputs.token }} 45 | head-branch-prefix: "auto-update/" 46 | commit-message: "update dependencies" 47 | -------------------------------------------------------------------------------- /tests/unit/test_app.py: -------------------------------------------------------------------------------- 1 | """tests of acme-cert-updater""" 2 | 3 | import unittest 4 | 5 | from updater import app 6 | 7 | class TestConfig(unittest.TestCase): 8 | def test_domains(self): 9 | config = app.Config({'domains': ''}) 10 | self.assertEqual(config.domains, []) 11 | 12 | config = app.Config({'domains': 'example.com'}) 13 | self.assertEqual(config.domains, ['example.com']) 14 | 15 | config = app.Config({'domains': 'example.com, *.EXAMPLE.com ,'}) 16 | self.assertEqual(config.domains, ['example.com', '*.example.com']) 17 | 18 | config = app.Config({'domains': []}) 19 | self.assertEqual(config.domains, []) 20 | 21 | config = app.Config({'domains': ['example.com']}) 22 | self.assertEqual(config.domains, ['example.com']) 23 | 24 | config = app.Config({'domains': ['example.com', ' *.EXAMPLE.com ', ' ', 123]}) 25 | self.assertEqual(config.domains, ['example.com', '*.example.com']) 26 | 27 | def test_cert_name(self): 28 | config = app.Config({'domains': ''}) 29 | self.assertEqual(config.cert_name, '') 30 | 31 | config = app.Config({'domains': 'EXAMPLE.com'}) 32 | self.assertEqual(config.cert_name, 'example.com') 33 | 34 | config = app.Config({'domains': '*.example.com'}) 35 | self.assertEqual(config.cert_name, 'example.com') 36 | 37 | config = app.Config({'cert_name': 'EXAMPLE.com'}) 38 | self.assertEqual(config.cert_name, 'example.com') 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [main] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [main] 14 | schedule: 15 | - cron: '0 6 * * 2' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v6 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v4 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v4 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v4 63 | -------------------------------------------------------------------------------- /tests/unit/test_integration.py: -------------------------------------------------------------------------------- 1 | """tests of acme-cert-updater""" 2 | 3 | import unittest 4 | import secrets 5 | import traceback 6 | 7 | import boto3 8 | from typing import List 9 | from updater import app 10 | 11 | # pylint: disable=missing-docstring 12 | 13 | AUTHOR_ACCOUNT = '445285296882' 14 | 15 | class DummyConfig: 16 | def __init__(self): 17 | self._prefix = secrets.token_hex(16) 18 | 19 | @property 20 | def domains(self) -> List[str]: 21 | return ['shogo82148.com', '*.shogo82148.com', '*.acme.shogo82148.com'] 22 | 23 | @property 24 | def cert_name(self) -> str: 25 | return 'example.com' 26 | 27 | @property 28 | def email(self) -> str: 29 | return "shogo82148@gmail.com" 30 | 31 | @property 32 | def bucket_name(self) -> str: 33 | return "shogo82148-acme-cert-updater-test" 34 | 35 | @property 36 | def prefix(self) -> str: 37 | return self._prefix 38 | 39 | @property 40 | def environment(self) -> str: 41 | return 'staging' 42 | 43 | @property 44 | def acme_server(self) -> str: 45 | return 'https://acme-v02.api.letsencrypt.org/directory' 46 | 47 | @property 48 | def notification(self) -> str: 49 | # pylint: disable=line-too-long 50 | return 'arn:aws:sns:ap-northeast-1:445285296882:acme-cert-updater-test-UpdateTopic-141WK4DP5P40E' 51 | 52 | class TestIntegration(unittest.TestCase): 53 | def setUp(self): 54 | identity = {} 55 | try: 56 | sts = boto3.client('sts') 57 | identity = sts.get_caller_identity() 58 | except: 59 | self.skipTest("external resource not available") 60 | 61 | if identity.get('Account') != AUTHOR_ACCOUNT: 62 | self.skipTest("external resource not available") 63 | 64 | self.__config = DummyConfig() 65 | 66 | def tearDown(self): 67 | cfg = self.__config 68 | s3 = boto3.resource('s3') # pylint: disable=invalid-name 69 | s3.Bucket(cfg.bucket_name).objects.filter( 70 | Prefix=cfg.prefix+'/', 71 | ).delete() 72 | 73 | def test_certonly(self): 74 | cfg = self.__config 75 | assert app.needs_init(cfg) 76 | app.certonly(cfg) 77 | 78 | assert not app.needs_init(cfg) 79 | app.renew(cfg) 80 | 81 | def test_notify_failed(self): 82 | try: 83 | raise Exception("some error") 84 | except: 85 | app.notify_failed(self.__config, traceback.format_exc()) 86 | 87 | 88 | if __name__ == '__main__': 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /tests/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: test environment for acme-cert-updater 3 | 4 | Resources: 5 | 6 | UpdateTopic: 7 | Type: AWS::SNS::Topic 8 | Properties: 9 | DisplayName: acme-cert-updater-test 10 | 11 | TestBucket: 12 | Type: AWS::S3::Bucket 13 | Properties: 14 | BucketName: shogo82148-acme-cert-updater-test 15 | 16 | TestRole: 17 | Type: AWS::IAM::Role 18 | Properties: 19 | AssumeRolePolicyDocument: 20 | Version: "2012-10-17" 21 | Statement: 22 | - Effect: Allow 23 | Principal: 24 | AWS: arn:aws:iam::053160724612:root 25 | Action: 26 | - 'sts:AssumeRole' 27 | Condition: 28 | StringEquals: 29 | "sts:ExternalId": shogo82148/acme-cert-updater 30 | - Effect: Allow 31 | Principal: 32 | AWS: arn:aws:iam::053160724612:root 33 | Action: 34 | - 'sts:TagSession' 35 | ManagedPolicyArns: 36 | - arn:aws:iam::aws:policy/AmazonElasticContainerRegistryPublicReadOnly 37 | Policies: 38 | - PolicyName: sns 39 | PolicyDocument: 40 | Version: 2012-10-17 41 | Statement: 42 | - Effect: Allow 43 | Action: 44 | - "sns:Publish" 45 | Resource: 46 | - !Ref UpdateTopic 47 | - PolicyName: s3 48 | PolicyDocument: 49 | Version: 2012-10-17 50 | Statement: 51 | - Effect: Allow 52 | Action: 53 | - "s3:*" 54 | Resource: 55 | - !GetAtt TestBucket.Arn 56 | - !Sub "${TestBucket.Arn}/*" 57 | - PolicyName: route53 58 | PolicyDocument: 59 | Version: 2012-10-17 60 | Statement: 61 | - Effect: Allow 62 | Action: 63 | - "route53:ListHostedZones" 64 | - "route53:GetChange" 65 | Resource: 66 | - "*" 67 | - Effect: Allow 68 | Action: 69 | - route53:ChangeResourceRecordSets 70 | Resource: 71 | - arn:aws:route53:::hostedzone/Z1TR8BQNS8S1I7 72 | - PolicyName: sam-validation 73 | PolicyDocument: 74 | Version: 2012-10-17 75 | Statement: 76 | - Effect: Allow 77 | Action: 78 | - "cloudformation:ValidateTemplate" 79 | Resource: 80 | - "*" 81 | - Effect: Allow 82 | Action: 83 | - iam:ListPolicies 84 | Resource: 85 | - "*" 86 | 87 | Outputs: 88 | Role: 89 | Value: !Ref TestRole 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acme-cert-updater 2 | 3 | [![test](https://github.com/shogo82148/acme-cert-updater/actions/workflows/test.yml/badge.svg)](https://github.com/shogo82148/acme-cert-updater/actions/workflows/test.yml) 4 | 5 | The acme-cert-updater automatically updates the certificate using ACME (Automated Certificate Management Environment) and Amazon Route 53. 6 | It is a pre-build AWS Serverless Application of [Certbot](https://certbot.eff.org/) with [certbot-dns-route53](https://certbot-dns-route53.readthedocs.io/en/stable/) plugin. 7 | 8 | ## Usage 9 | 10 | ### Permission 11 | 12 | The acme-cert-updater requires some SAM policy templates (S3ReadPolicy, S3CrudPolicy, and SNSPublishMessagePolicy), 13 | and CAPABILITY_IAM Capabilities to use the Amazon Web Services Route 53 API. 14 | 15 | ### Deploy 16 | 17 | The acme-cert-updater is available on [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:445285296882:applications~acme-cert-updater). 18 | 19 | Or here is a resource template of AWS Serverless Application Model. 20 | 21 | ```yaml 22 | AWSTemplateFormatVersion: 2010-09-09 23 | Transform: AWS::Serverless-2016-10-31 24 | 25 | Resources: 26 | AcmeCertUpdater: 27 | Type: AWS::Serverless::Application 28 | Properties: 29 | Location: 30 | ApplicationId: arn:aws:serverlessrepo:us-east-1:445285296882:applications/acme-cert-updater 31 | SemanticVersion: 1.21.0 32 | Parameters: 33 | # S3 bucket name for saving the certificates (required) 34 | BucketName: YOUR_BUCKET_NAME 35 | 36 | # Comma separated list of domains to update the certificates (required) 37 | Domains: YOUR_DOMAINS 38 | 39 | # the S3 key of certificate 40 | # default: the first domain name of the Domains parameter 41 | CertName: YOUR_DOMAINS 42 | 43 | # Email address (required) 44 | Email: YOUR_EMAIL_ADDRESS 45 | 46 | # Amazon Route 53 Hosted Zone ID (required) 47 | HostedZone: YOUR_HOSTED_ZONE_ID 48 | 49 | # The Amazon SNS topic Amazon Resource Name (ARN) to which the updater reports events. (optional) 50 | Notification: ARN_SNS_TOPIC 51 | 52 | # url for acme server 53 | # default: https://acme-v02.api.letsencrypt.org/directory 54 | AcmeServer: https://acme-v02.api.letsencrypt.org/directory 55 | 56 | # execution environment 57 | # allowed values: production, staging 58 | # default: production 59 | Environment: production 60 | 61 | # Prefix of objects on S3 bucket. 62 | # default: "" (no prefix) 63 | Prefix: "" 64 | 65 | # Log level 66 | # allowed values: DEBUG, INFO, WARN, WARNING, ERROR, CRITICAL 67 | # default: ERROR 68 | LogLevel: ERROR 69 | ``` 70 | 71 | The following command will create a Cloudformation Stack and deploy the SAM resources. 72 | 73 | ``` 74 | aws cloudformation \ 75 | --template-file template.yaml \ 76 | --stack-name \ 77 | --capabilities CAPABILITY_AUTO_EXPAND CAPABILITY_IAM 78 | ``` 79 | 80 | ### Download the certificate 81 | 82 | [download-certificate.sh](https://github.com/shogo82148/acme-cert-updater/blob/master/download-certificate.sh) is a helper script for downloading the certificate. 83 | It downloads the certificate, and executes the given command if the certificate is renewal. 84 | Here is an example for reloading nginx. 85 | 86 | ``` 87 | ./download-certificate.sh bucket-name example.com.json /etc/ssl/example.com systemctl reload nginx 88 | ``` 89 | 90 | bash, [AWS CLI](https://aws.amazon.com/cli/), and [jq](https://stedolan.github.io/jq/) are required. 91 | 92 | ## LICENSE 93 | 94 | MIT License Copyright (c) 2019 Ichinose Shogo 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | acme-cert-updater 5 | 6 | update the certificate using ACME and Route 53 7 | 8 | Metadata: 9 | AWS::ServerlessRepo::Application: 10 | Name: acme-cert-updater 11 | Description: update the certificate using ACME and Route 53 12 | Author: ICHINOSE Shogo 13 | SpdxLicenseId: MIT 14 | LicenseUrl: LICENSE 15 | ReadmeUrl: README.md 16 | Labels: ["acme", "letsencrypt"] 17 | HomePageUrl: https://github.com/shogo82148/acme-cert-updater 18 | SemanticVersion: 1.21.0 19 | SourceCodeUrl: https://github.com/shogo82148/acme-cert-updater 20 | 21 | Parameters: 22 | Domains: 23 | Type: String 24 | Description: Comma separated list of domains to update the certificates 25 | CertName: 26 | Type: String 27 | Description: the S3 key of certificate. the default value is the first domain name. 28 | Default: "" 29 | Email: 30 | Type: String 31 | Description: Email address 32 | BucketName: 33 | Type: String 34 | Description: S3 bucket name for saving the certificates 35 | Prefix: 36 | Type: String 37 | Description: Prefix of objects on S3 bucket 38 | Default: "" 39 | Environment: 40 | Type: String 41 | AllowedValues: ["production", "staging"] 42 | Default: "production" 43 | Description: execution environment 44 | AcmeServer: 45 | Type: String 46 | Default: https://acme-v02.api.letsencrypt.org/directory 47 | Description: url for acme server 48 | HostedZone: 49 | Type: AWS::Route53::HostedZone::Id 50 | Description: Amazon Route 53 Hosted Zone ID 51 | Notification: 52 | Type: String 53 | Default: "" 54 | Description: The Amazon SNS topic Amazon Resource Name (ARN) to which the updater reports events. 55 | LogLevel: 56 | Type: String 57 | Default: ERROR 58 | AllowedValues: [DEBUG, INFO, WARN, WARNING, ERROR, CRITICAL] 59 | 60 | Conditions: 61 | # NOTE: check whether Notification is an ARN. 62 | # for backward compatibility, we should accept a topic name. 63 | # 64 | # If Notification is an ARN, e.g. "arn:aws:sns:us-east-1:123456789012:my-topic": 65 | # `!Join [ "", !Split [ "arn:", !Ref Notification ] ]` is "aws:sns:us-east-1:123456789012:my-topic" 66 | # As a result, IsNotificationArn will be **false** because "arn:aws:sns:us-east-1:123456789012:my-topic" != "aws:sns:us-east-1:123456789012:my-topic" 67 | # 68 | # If Notification is a topic name, e.g. "my-topic": 69 | # `!Join [ "", !Split [ "arn:", !Ref Notification ] ]` is "my-topic" 70 | # As a result, IsNotificationArn will be **false** because "my-topic" == "my-topic" 71 | # 72 | # it works because topic names can't contain ":" 73 | # ref. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sns-topic.html#cfn-sns-topic-topicname 74 | # > Topic names must include only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, 75 | # > and must be between 1 and 256 characters long. 76 | IsNotificationArn: !Not 77 | - !Equals 78 | - !Join ["", !Split ["arn:", !Ref Notification]] # remove "arn:" in Notification 79 | - !Ref Notification 80 | 81 | HasNotification: !Not [!Equals [!Ref Notification, ""]] 82 | 83 | Resources: 84 | AcmeCertUpdater: 85 | Type: AWS::Serverless::Function 86 | Properties: 87 | CodeUri: updater/ 88 | Handler: app.lambda_handler 89 | Runtime: python3.14 90 | Environment: 91 | Variables: 92 | UPDATER_EMAIL: !Ref Email 93 | UPDATER_BUCKET_NAME: !Ref BucketName 94 | UPDATER_PREFIX: !Ref Prefix 95 | UPDATER_ENVIRONMENT: !Ref Environment 96 | UPDATER_ACME_SERVER: !Ref AcmeServer 97 | UPDATER_NOTIFICATION: !If 98 | - IsNotificationArn 99 | - !Ref Notification 100 | - !If 101 | - HasNotification 102 | - !Sub arn:${AWS::Partition}:sns:${AWS::Region}:${AWS::AccountId}:${Notification} 103 | - "" 104 | UPDATER_LOG_LEVEL: !Ref LogLevel 105 | Timeout: 900 106 | Events: 107 | Update: 108 | Type: Schedule 109 | Properties: 110 | Schedule: "rate(12 hours)" 111 | Input: !Sub '{"domains":"${Domains}","cert_name":"${CertName}"}' 112 | Policies: 113 | - S3CrudPolicy: 114 | BucketName: !Ref BucketName 115 | - SNSPublishMessagePolicy: 116 | TopicName: !Ref Notification 117 | - Version: 2012-10-17 118 | Statement: 119 | - Effect: Allow 120 | Action: 121 | - "route53:ListHostedZones" 122 | - "route53:GetChange" 123 | Resource: 124 | - "*" 125 | - Effect: Allow 126 | Action: 127 | - route53:ChangeResourceRecordSets 128 | Resource: 129 | - !Sub arn:aws:route53:::hostedzone/${HostedZone} 130 | 131 | Certificate: 132 | Type: Custom::Certificate 133 | Properties: 134 | ServiceToken: !GetAtt AcmeCertUpdater.Arn 135 | domains: !Ref Domains 136 | cert_name: !Ref CertName 137 | 138 | Outputs: 139 | Arn: 140 | Description: the arn of updater AWS Lambda function 141 | Value: !GetAtt AcmeCertUpdater.Arn 142 | -------------------------------------------------------------------------------- /updater/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | acme-cert-updater 3 | 4 | update the certificate using ACME and Route 53 5 | """ 6 | 7 | import os 8 | import os.path 9 | import pathlib 10 | import json 11 | import string 12 | import tempfile 13 | import traceback 14 | import urllib.request 15 | from datetime import datetime 16 | from typing import Dict, Union, List 17 | from unittest import mock 18 | 19 | import logging 20 | import boto3 21 | from botocore.exceptions import ClientError 22 | import certbot.main 23 | import configobj 24 | 25 | def log_level() -> int: 26 | level = os.environ.get('UPDATER_LOG_LEVEL', 'ERROR') 27 | if level == 'DEBUG': 28 | return logging.DEBUG 29 | if level == 'INFO': 30 | return logging.INFO 31 | if level == 'WARN': 32 | return logging.WARN 33 | if level == 'WARNING': 34 | return logging.WARNING 35 | if level == 'ERROR': 36 | return logging.ERROR 37 | if level == 'CRITICAL': 38 | return logging.CRITICAL 39 | if level == 'FATAL': 40 | return logging.FATAL 41 | raise ValueError("unknown log level " + level) 42 | 43 | logger = logging.getLogger(__name__) 44 | logging.getLogger().setLevel(log_level()) 45 | 46 | class Config: 47 | """configure of acme-cert-update""" 48 | 49 | def __init__(self, event): 50 | """initialize Config""" 51 | domains = event.get('domains', '') 52 | if isinstance(domains, str): 53 | # the domains field is comma separated string 54 | self.__domains = domains.split(',') 55 | elif isinstance(domains, List): 56 | self.__domains = domains 57 | else: 58 | raise ValueError("invalid domains") 59 | self.__domains = [ 60 | domain.strip().lower() for domain in self.__domains 61 | if isinstance(domain, str) and domain.strip() != '' 62 | ] 63 | 64 | cert_name = event.get('cert_name', '') 65 | if cert_name == '': 66 | if len(self.__domains) > 0: 67 | self.__cert_name = Config._trim_wildcard(self.__domains[0]) 68 | else: 69 | self.__cert_name = '' 70 | else: 71 | self.__cert_name = cert_name.lower() 72 | 73 | @classmethod 74 | def _trim_wildcard(cls, domain: str) -> str: 75 | if domain.startswith('*.'): 76 | return domain[2:] 77 | return domain 78 | 79 | @property 80 | def domains(self) -> List[str]: 81 | """domain names for issue""" 82 | return self.__domains 83 | 84 | @property 85 | def cert_name(self) -> str: 86 | return self.__cert_name 87 | 88 | @property 89 | def email(self) -> str: 90 | """Email address""" 91 | return os.environ.get('UPDATER_EMAIL', '') 92 | 93 | @property 94 | def bucket_name(self) -> str: 95 | """S3 bucket name for saving the certificates""" 96 | return os.environ.get('UPDATER_BUCKET_NAME', '') 97 | 98 | @property 99 | def prefix(self) -> str: 100 | """Prefix of objects on S3 bucket""" 101 | return os.environ.get('UPDATER_PREFIX', '') 102 | 103 | @property 104 | def environment(self) -> str: 105 | """execution environment""" 106 | return os.environ.get('UPDATER_ENVIRONMENT', '') 107 | 108 | @property 109 | def acme_server(self) -> str: 110 | """url for acme server""" 111 | return os.environ.get( 112 | 'UPDATER_ACME_SERVER', 113 | 'https://acme-v02.api.letsencrypt.org/directory' 114 | ) 115 | 116 | @property 117 | def notification(self) -> str: 118 | """The Amazon SNS topic Amazon Resource Name (ARN) to which the updater reports events.""" 119 | return os.environ.get('UPDATER_NOTIFICATION', '') 120 | 121 | def cfn_response(url: str, body: object) -> None: 122 | """cfn_response sends the response to CloudFormation""" 123 | data = json.dumps(body).encode() 124 | headers = { 125 | 'content-type': 'application/json', 126 | 'content-length': str(len(data)), 127 | } 128 | req = urllib.request.Request(url, method='PUT', data=data, headers=headers) 129 | with urllib.request.urlopen(req) as res: 130 | res.read() # skip the body 131 | 132 | def certonly(config) -> None: 133 | """get new certificate""" 134 | with tempfile.TemporaryDirectory() as tmp: 135 | input_array = [ 136 | 'certonly', 137 | '--noninteractive', 138 | '--agree-tos', 139 | '--email', config.email, 140 | '--dns-route53', 141 | '--config-dir', os.path.join(tmp, 'config-dir/'), 142 | '--work-dir', os.path.join(tmp, 'word-dir/'), 143 | '--logs-dir', os.path.join(tmp, 'logs-dir/'), 144 | '--cert-name', config.cert_name, 145 | ] 146 | 147 | # disable the report 148 | if log_level() >= logging.WARNING: 149 | input_array.append('--quiet') 150 | 151 | for domain in config.domains: 152 | input_array.append('--domains') 153 | input_array.append(domain) 154 | 155 | if config.environment == 'production': 156 | input_array.append('--server') 157 | input_array.append(config.acme_server) 158 | else: 159 | input_array.append('--staging') 160 | 161 | certbot_main(input_array) 162 | save_cert(config, tmp) 163 | 164 | def renew(config) -> None: 165 | """update existing certificate""" 166 | with tempfile.TemporaryDirectory() as tmp: 167 | load_cert(config, tmp) 168 | 169 | flag = pathlib.Path(tmp, 'flag.txt') 170 | hook = pathlib.Path(tmp, 'config-dir', 'renewal-hooks', 'post', 'post.sh') 171 | hook.parent.mkdir(parents=True, exist_ok=True) 172 | hook.write_text("#!/usr/bin/env bash\n\ntouch '" + str(flag) + "'") 173 | hook.chmod(0o755) 174 | 175 | input_array = [ 176 | 'renew', 177 | '--noninteractive', 178 | '--no-random-sleep-on-renew', 179 | '--agree-tos', 180 | '--email', config.email, 181 | '--dns-route53', 182 | '--config-dir', os.path.join(tmp, 'config-dir/'), 183 | '--work-dir', os.path.join(tmp, 'word-dir/'), 184 | '--logs-dir', os.path.join(tmp, 'logs-dir/'), 185 | ] 186 | if config.environment != 'production': 187 | # force renewal for testing 188 | input_array.append('--force-renewal') 189 | # connect to the staging environment 190 | input_array.append('--staging') 191 | 192 | # disable the report 193 | if log_level() >= logging.WARNING: 194 | input_array.append('--quiet') 195 | 196 | certbot_main(input_array) 197 | if flag.exists(): 198 | save_cert(config, tmp) 199 | 200 | class mock_atexit: 201 | """patch certbot.util.atexit""" 202 | 203 | def __init__(self): 204 | patch = mock.patch("certbot.util.atexit") 205 | self._patch = patch 206 | self._func = [] 207 | 208 | def register(self, func, *args, **kwargs): 209 | """register dummy atexit""" 210 | self._func.append([func, args, kwargs]) 211 | 212 | def atexit_call(self): 213 | """call atexit functions""" 214 | for func, args, kwargs in reversed(self._func): 215 | func(*args, **kwargs) 216 | self._func = [] 217 | 218 | def __enter__(self): 219 | result = self._patch.start() 220 | register = result.register 221 | register.side_effect = self.register 222 | return self 223 | 224 | def __exit__(self, ex_type, ex_value, trace): 225 | self._patch.stop() 226 | self.atexit_call() 227 | 228 | def certbot_main(args: List[str]) -> None: 229 | """ 230 | certbot_main is a wrapper of certbot.main.main. 231 | certbot.main.main overwrites the global configures, 232 | so certbot_main save and restore them. 233 | """ 234 | 235 | with mock_atexit(): 236 | # disable certbot custom log handlers. 237 | with mock.patch("certbot._internal.log.pre_arg_parse_setup"): 238 | with mock.patch("certbot._internal.log.post_arg_parse_setup"): 239 | 240 | # call main function 241 | certbot.main.main(args) 242 | 243 | 244 | s3 = boto3.resource('s3') # pylint: disable=invalid-name 245 | def save_cert(config, tmp: str) -> None: 246 | """upload the certificate files to Amazon S3""" 247 | bucket_name = config.bucket_name 248 | key = build_key(config.prefix, config.cert_name + '.json') 249 | bucket = s3.Bucket(config.bucket_name) 250 | now = datetime.utcnow().isoformat() 251 | live = os.path.join(tmp, 'config-dir/live/', config.cert_name) 252 | for filename in ['cert.pem', 'chain.pem', 'fullchain.pem', 'privkey.pem']: 253 | logger.debug(f'uploading {filename}') 254 | bucket.upload_file( 255 | os.path.join(live, filename), 256 | build_key(config.prefix, config.cert_name, now, filename), 257 | ) 258 | 259 | certconfig = { 260 | 'timestamp': now, 261 | 'domain': config.cert_name, # for backward compatibility 262 | 'domains': config.domains, 263 | 'cert_name': config.cert_name, 264 | 'config': { 265 | 'account': get_files(tmp, 'config-dir/accounts'), 266 | 'csr': get_files(tmp, 'config-dir/csr'), 267 | 'keys': get_files(tmp, 'config-dir/keys'), 268 | 'renewal': get_renewal_config(tmp, config.cert_name), 269 | }, 270 | 'cert': { 271 | 'cert': build_key(config.prefix, config.cert_name, now, 'cert.pem'), 272 | 'chain': build_key(config.prefix, config.cert_name, now, 'chain.pem'), 273 | 'fullchain': build_key(config.prefix, config.cert_name, now, 'fullchain.pem'), 274 | 'privkey': build_key(config.prefix, config.cert_name, now, 'privkey.pem'), 275 | }, 276 | } 277 | 278 | logger.debug(f'uploading the certificate information to s3://{bucket_name}/{key}') 279 | bucket.put_object( 280 | Body=json.dumps(certconfig), 281 | Key=key, 282 | ContentType='application/json', 283 | ) 284 | notify_renewed(config, certconfig, key) 285 | 286 | def load_cert(config, tmp: str) -> None: 287 | """download the certificate files from Amazon S3""" 288 | bucket_name = config.bucket_name 289 | key = build_key(config.prefix, config.cert_name + '.json') 290 | logger.debug(f'downloading the certificate from s3://{bucket_name}/{key}') 291 | bucket = s3.Bucket(bucket_name) 292 | obj = bucket.Object(key) 293 | certconfig = json.load(obj.get()['Body']) 294 | 295 | set_files(tmp, 'config-dir/accounts/', certconfig['config']['account']) 296 | set_files(tmp, 'config-dir/csr/', certconfig['config']['csr']) 297 | set_files(tmp, 'config-dir/keys/', certconfig['config']['keys']) 298 | set_renewal_config(tmp, config.cert_name, certconfig['config']['renewal']) 299 | 300 | archive = os.path.join(tmp, 'config-dir', 'archive', config.cert_name) 301 | pathlib.Path(archive).mkdir(parents=True, exist_ok=True) 302 | cert = certconfig['cert'] 303 | bucket.download_file(cert['cert'], os.path.join(archive, 'cert1.pem')) 304 | bucket.download_file(cert['chain'], os.path.join(archive, 'chain1.pem')) 305 | bucket.download_file(cert['fullchain'], os.path.join(archive, 'fullchain1.pem')) 306 | bucket.download_file(cert['privkey'], os.path.join(archive, 'privkey1.pem')) 307 | 308 | live = os.path.join(tmp, 'config-dir', 'live', config.cert_name) 309 | pathlib.Path(live).mkdir(parents=True, exist_ok=True) 310 | os.symlink(os.path.join(archive, 'cert1.pem'), os.path.join(live, 'cert.pem')) 311 | os.symlink(os.path.join(archive, 'chain1.pem'), os.path.join(live, 'chain.pem')) 312 | os.symlink(os.path.join(archive, 'fullchain1.pem'), os.path.join(live, 'fullchain.pem')) 313 | os.symlink(os.path.join(archive, 'privkey1.pem'), os.path.join(live, 'privkey.pem')) 314 | 315 | def get_files(tmp: str, subdir: str) -> Dict[str, str]: 316 | """get_files gets file contents as dict""" 317 | config = {} 318 | path = pathlib.Path(tmp, subdir) 319 | for root, _, files in os.walk(str(path)): 320 | for name in files: 321 | filepath = pathlib.Path(root, name) 322 | config[str(filepath.relative_to(path))] = filepath.read_text() 323 | return config 324 | 325 | def set_files(tmp: str, subdir: str, config: Dict[str, str]) -> None: 326 | """extract config to file system""" 327 | path = pathlib.Path(tmp, subdir) 328 | for key, value in config.items(): 329 | filepath = path.joinpath(key) 330 | filepath.parent.mkdir(parents=True, exist_ok=True) 331 | filepath.write_text(value) 332 | 333 | def get_renewal_config(tmp: str, domain: str) -> configobj.ConfigObj: 334 | """return renewal config of certbot""" 335 | config = {} 336 | tmppath = pathlib.Path(tmp) 337 | cfg = configobj.ConfigObj(os.path.join(tmp, 'config-dir', 'renewal', domain + '.conf')) 338 | for key in ['archive_dir', 'cert', 'privkey', 'chain', 'fullchain']: 339 | path = pathlib.Path(cfg[key]) 340 | cfg[key] = str(path.relative_to(tmppath)) 341 | for key in ['config_dir', 'work_dir', 'logs_dir']: 342 | path = pathlib.Path(cfg['renewalparams'][key]) 343 | cfg['renewalparams'][key] = str(path.relative_to(tmppath)) 344 | for key, value in cfg.items(): 345 | config[key] = value 346 | return config 347 | 348 | def set_renewal_config(tmp: str, domain: str, config: configobj.ConfigObj) -> None: 349 | """write renewal config of certbot to file system""" 350 | tmppath = pathlib.Path(tmp) 351 | for key in ['archive_dir', 'cert', 'privkey', 'chain', 'fullchain']: 352 | config[key] = str(pathlib.Path(tmppath / config[key])) 353 | for key in ['config_dir', 'work_dir', 'logs_dir']: 354 | config['renewalparams'][key] = str(pathlib.Path(tmppath / config['renewalparams'][key])) 355 | 356 | ret = configobj.ConfigObj() 357 | conf_path = pathlib.Path(tmp, 'config-dir', 'renewal', domain + '.conf') 358 | conf_path.parent.mkdir(parents=True, exist_ok=True) 359 | ret.filename = str(conf_path) 360 | for key, value in config.items(): 361 | ret[key] = value 362 | ret.write() 363 | 364 | 365 | def build_key(*segment) -> str: 366 | """build a key of S3 objects""" 367 | path = "/".join(segment) 368 | path = path.replace("//", "/") 369 | if len(path) > 1 and path[0] == "/": 370 | path = path[1:] 371 | return path 372 | 373 | sns = boto3.client('sns') # pylint: disable=invalid-name 374 | def notify_renewed(config, certconfig: Dict[str, Union[str, Dict[str, str]]], key: str) -> None: 375 | """notify via SNS topic""" 376 | if config.notification == '': 377 | return 378 | template = string.Template("""Notification from acme-cert-updater(https://github.com/shogo82148/acme-cert-updater). 379 | The following certificate is renewed. 380 | 381 | - cert_name: $cert_name 382 | - domains: $domains 383 | - bucket: $bucket 384 | - object key: $key 385 | - cert: $cert 386 | - chain: $chain 387 | - fullchain: $fullchain 388 | - privkey: $privkey 389 | """) 390 | text_message = template.substitute( 391 | timestamp = certconfig['timestamp'], 392 | domains = ', '.join(config.domains), 393 | cert_name = config.cert_name, 394 | bucket = config.bucket_name, 395 | key = key, 396 | cert = certconfig['cert']['cert'], 397 | chain = certconfig['cert']['chain'], 398 | fullchain = certconfig['cert']['fullchain'], 399 | privkey = certconfig['cert']['privkey'], 400 | ) 401 | json_message = json.dumps({ 402 | 'type': 'renewed', 403 | 'timestamp': certconfig['timestamp'], 404 | 'domain': config.cert_name, # for backward compatibility 405 | 'domains': config.domains, 406 | 'cert_name': config.cert_name, 407 | 'bucket': config.bucket_name, 408 | 'key': key, 409 | 'cert': certconfig['cert'], 410 | }) 411 | message = json.dumps({ 412 | 'default': json_message, 413 | 'email': text_message, 414 | }) 415 | sns.publish( 416 | TopicArn=config.notification, 417 | Message=message, 418 | MessageStructure="json", 419 | ) 420 | 421 | def notify_failed(config, err) -> None: 422 | """notify via SNS topic""" 423 | if config.notification == '': 424 | return 425 | template = string.Template("""Notification from acme-cert-updater(https://github.com/shogo82148/acme-cert-updater). 426 | Certificate renewal is failed. 427 | 428 | - cert_name: $cert_name 429 | - domains: $domains 430 | 431 | Exception: 432 | $err 433 | """) 434 | text_message = template.substitute( 435 | domains = ', '.join(config.domains), 436 | cert_name = config.cert_name, 437 | err = err, 438 | ) 439 | json_message = json.dumps({ 440 | 'type': 'failed', 441 | 'domains': config.domains, 442 | 'cert_name': config.cert_name, 443 | }) 444 | message = json.dumps({ 445 | 'default': json_message, 446 | 'email': text_message, 447 | }) 448 | sns.publish( 449 | TopicArn=config.notification, 450 | Message=message, 451 | MessageStructure="json", 452 | ) 453 | 454 | def needs_init(config) -> bool: 455 | """check initialize is required""" 456 | bucket_name = config.bucket_name 457 | key = build_key(config.prefix, config.cert_name + '.json') 458 | logger.debug(f'checking s3://{bucket_name}/{key} exists.') 459 | bucket = s3.Bucket(bucket_name) 460 | obj = bucket.Object(key) 461 | try: 462 | obj.load() 463 | except ClientError: 464 | return True 465 | return False 466 | 467 | def lambda_handler(event: object, context: object): # pylint: disable=unused-argument 468 | """entry point of AWS Lambda""" 469 | 470 | if "RequestType" in event: 471 | # it looks like a request from AWS Lambda-backed custom resources 472 | handle_cfn_custom_resource(event) 473 | else: 474 | config = Config(event) 475 | handle_event(config) 476 | 477 | return {} 478 | 479 | def handle_cfn_custom_resource(event: object) -> None: 480 | """handles requests from AWS Lambda-backed custom resources""" 481 | properties = event['ResourceProperties'] 482 | config = Config(properties) 483 | resourceId = properties['domains'] 484 | ret = { 485 | 'Status': 'SUCCESS', 486 | 'StackId': event['StackId'], 487 | 'RequestId': event['RequestId'], 488 | 'LogicalResourceId': event['LogicalResourceId'], 489 | 'Data': {}, 490 | 'PhysicalResourceId': resourceId, 491 | } 492 | 493 | if event['RequestType'] == 'Delete': 494 | cfn_response(event['ResponseURL'], ret) 495 | 496 | try: 497 | if needs_init(config): 498 | try: 499 | logger.debug('update the certificate.') 500 | certonly(config) 501 | except: 502 | logger.debug('updating failed. fall back to request new certificate.') 503 | renew(config) 504 | else: 505 | logger.debug('request new certificate.') 506 | renew(config) 507 | cfn_response(event['ResponseURL'], ret) 508 | except: 509 | notify_failed(config, traceback.format_exc()) 510 | ret['Status'] = 'FAILED' 511 | cfn_response(event['ResponseURL'], ret) 512 | raise 513 | 514 | def handle_event(config: Config) -> None: 515 | """handles Amazon EventBridge events""" 516 | if len(config.domains) == 0: 517 | # nothing to do 518 | return {} 519 | 520 | try: 521 | if needs_init(config): 522 | try: 523 | logger.debug('update the certificate.') 524 | certonly(config) 525 | except: 526 | logger.debug('updating failed. fall back to request new certificate.') 527 | renew(config) 528 | else: 529 | logger.debug('request new certificate.') 530 | renew(config) 531 | except: 532 | notify_failed(config, traceback.format_exc()) 533 | raise 534 | 535 | if __name__ == "__main__": 536 | lambda_handler({}, None) 537 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "561452752a3ed64772206a02593773c0a9aa8ea2ca01bc4d98db6bc43a0ec08f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.14" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "acme": { 20 | "hashes": [ 21 | "sha256:3d71d442a02112fbcf58e4760e8b7b409d37586dccaa76416251a774facd7845", 22 | "sha256:5f905542b85a75a767dfb540ccfa78b9c758c6619338651755347ca7dd22a1fd" 23 | ], 24 | "markers": "python_version >= '3.10'", 25 | "version": "==5.2.1" 26 | }, 27 | "boto3": { 28 | "hashes": [ 29 | "sha256:0f4089e230d55f981d67376e48cefd41c3d58c7f694480f13288e6ff7b1fefbc", 30 | "sha256:65f0d98a3786ec729ba9b5f70448895b2d1d1f27949aa7af5cb4f39da341bbc4" 31 | ], 32 | "index": "pypi", 33 | "markers": "python_version >= '3.9'", 34 | "version": "==1.42.4" 35 | }, 36 | "botocore": { 37 | "hashes": [ 38 | "sha256:c3b091fd33809f187824b6434e518b889514ded5164cb379358367c18e8b0d7d", 39 | "sha256:d4816023492b987a804f693c2d76fb751fdc8755d49933106d69e2489c4c0f98" 40 | ], 41 | "index": "pypi", 42 | "markers": "python_version >= '3.9'", 43 | "version": "==1.42.4" 44 | }, 45 | "certbot": { 46 | "hashes": [ 47 | "sha256:5faed67634a3f8a62782655b5f739161804fc9999577b263ea7dd76eba62f406", 48 | "sha256:fb494826590904dd2eb0883b83822d0437a3449bd45af87254489d0a9c8a2f67" 49 | ], 50 | "index": "pypi", 51 | "markers": "python_version >= '3.10'", 52 | "version": "==5.2.1" 53 | }, 54 | "certbot-dns-route53": { 55 | "hashes": [ 56 | "sha256:37536fb5ac544f7339454a61922ff294f56bcce1a9533b4d63907f75d4e74019", 57 | "sha256:68483a5302328ae5064c6b724f0e54ea3e8af3e8a6be294b8757d362b837a3ce" 58 | ], 59 | "index": "pypi", 60 | "markers": "python_version >= '3.10'", 61 | "version": "==5.2.1" 62 | }, 63 | "certifi": { 64 | "hashes": [ 65 | "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", 66 | "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316" 67 | ], 68 | "markers": "python_version >= '3.7'", 69 | "version": "==2025.11.12" 70 | }, 71 | "cffi": { 72 | "hashes": [ 73 | "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", 74 | "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", 75 | "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", 76 | "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", 77 | "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", 78 | "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", 79 | "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", 80 | "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", 81 | "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", 82 | "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", 83 | "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", 84 | "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", 85 | "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", 86 | "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", 87 | "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", 88 | "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", 89 | "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", 90 | "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", 91 | "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", 92 | "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", 93 | "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", 94 | "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", 95 | "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", 96 | "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", 97 | "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", 98 | "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", 99 | "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", 100 | "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", 101 | "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", 102 | "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", 103 | "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", 104 | "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", 105 | "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", 106 | "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", 107 | "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", 108 | "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", 109 | "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", 110 | "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", 111 | "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", 112 | "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", 113 | "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", 114 | "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", 115 | "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", 116 | "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", 117 | "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", 118 | "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", 119 | "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", 120 | "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", 121 | "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", 122 | "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", 123 | "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", 124 | "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", 125 | "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", 126 | "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", 127 | "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", 128 | "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", 129 | "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", 130 | "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", 131 | "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", 132 | "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", 133 | "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", 134 | "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", 135 | "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", 136 | "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", 137 | "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", 138 | "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", 139 | "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", 140 | "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", 141 | "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", 142 | "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", 143 | "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", 144 | "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", 145 | "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", 146 | "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", 147 | "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", 148 | "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", 149 | "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", 150 | "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", 151 | "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", 152 | "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", 153 | "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", 154 | "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", 155 | "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", 156 | "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" 157 | ], 158 | "markers": "python_version >= '3.9'", 159 | "version": "==2.0.0" 160 | }, 161 | "charset-normalizer": { 162 | "hashes": [ 163 | "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", 164 | "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", 165 | "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", 166 | "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", 167 | "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", 168 | "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", 169 | "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", 170 | "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", 171 | "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", 172 | "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", 173 | "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", 174 | "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", 175 | "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", 176 | "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", 177 | "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", 178 | "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", 179 | "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", 180 | "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", 181 | "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", 182 | "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", 183 | "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", 184 | "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", 185 | "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", 186 | "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", 187 | "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", 188 | "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", 189 | "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", 190 | "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", 191 | "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", 192 | "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", 193 | "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", 194 | "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", 195 | "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", 196 | "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", 197 | "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", 198 | "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", 199 | "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", 200 | "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", 201 | "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", 202 | "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", 203 | "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", 204 | "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", 205 | "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", 206 | "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", 207 | "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", 208 | "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", 209 | "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", 210 | "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", 211 | "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", 212 | "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", 213 | "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", 214 | "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", 215 | "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", 216 | "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", 217 | "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", 218 | "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", 219 | "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", 220 | "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", 221 | "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", 222 | "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", 223 | "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", 224 | "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", 225 | "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", 226 | "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", 227 | "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", 228 | "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", 229 | "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", 230 | "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", 231 | "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", 232 | "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", 233 | "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", 234 | "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", 235 | "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", 236 | "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", 237 | "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", 238 | "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", 239 | "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", 240 | "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", 241 | "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", 242 | "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", 243 | "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", 244 | "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", 245 | "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", 246 | "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", 247 | "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", 248 | "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", 249 | "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", 250 | "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", 251 | "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", 252 | "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", 253 | "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", 254 | "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", 255 | "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", 256 | "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", 257 | "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", 258 | "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", 259 | "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", 260 | "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", 261 | "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", 262 | "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", 263 | "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", 264 | "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", 265 | "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", 266 | "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", 267 | "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", 268 | "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", 269 | "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", 270 | "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", 271 | "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", 272 | "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", 273 | "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", 274 | "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", 275 | "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" 276 | ], 277 | "markers": "python_version >= '3.7'", 278 | "version": "==3.4.4" 279 | }, 280 | "configargparse": { 281 | "hashes": [ 282 | "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", 283 | "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6" 284 | ], 285 | "markers": "python_version >= '3.6'", 286 | "version": "==1.7.1" 287 | }, 288 | "configobj": { 289 | "hashes": [ 290 | "sha256:03c881bbf23aa07bccf1b837005975993c4ab4427ba57f959afdd9d1a2386848", 291 | "sha256:1ba10c5b6ee16229c79a05047aeda2b55eb4e80d7c7d8ecf17ec1ca600c79882" 292 | ], 293 | "index": "pypi", 294 | "markers": "python_version >= '3.7'", 295 | "version": "==5.0.9" 296 | }, 297 | "cryptography": { 298 | "hashes": [ 299 | "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", 300 | "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", 301 | "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", 302 | "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", 303 | "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", 304 | "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", 305 | "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", 306 | "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", 307 | "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", 308 | "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", 309 | "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", 310 | "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", 311 | "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", 312 | "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", 313 | "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", 314 | "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", 315 | "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", 316 | "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", 317 | "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", 318 | "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", 319 | "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", 320 | "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", 321 | "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", 322 | "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", 323 | "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", 324 | "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", 325 | "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", 326 | "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", 327 | "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", 328 | "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", 329 | "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", 330 | "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", 331 | "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", 332 | "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", 333 | "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", 334 | "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", 335 | "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", 336 | "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", 337 | "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", 338 | "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", 339 | "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", 340 | "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", 341 | "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", 342 | "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", 343 | "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", 344 | "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", 345 | "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", 346 | "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", 347 | "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", 348 | "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", 349 | "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", 350 | "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", 351 | "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", 352 | "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018" 353 | ], 354 | "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", 355 | "version": "==46.0.3" 356 | }, 357 | "distro": { 358 | "hashes": [ 359 | "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", 360 | "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" 361 | ], 362 | "markers": "python_version >= '3.6'", 363 | "version": "==1.9.0" 364 | }, 365 | "idna": { 366 | "hashes": [ 367 | "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", 368 | "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" 369 | ], 370 | "markers": "python_version >= '3.8'", 371 | "version": "==3.11" 372 | }, 373 | "jmespath": { 374 | "hashes": [ 375 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 376 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 377 | ], 378 | "markers": "python_version >= '3.7'", 379 | "version": "==1.0.1" 380 | }, 381 | "josepy": { 382 | "hashes": [ 383 | "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", 384 | "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9" 385 | ], 386 | "markers": "python_full_version >= '3.9.2'", 387 | "version": "==2.2.0" 388 | }, 389 | "parsedatetime": { 390 | "hashes": [ 391 | "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", 392 | "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" 393 | ], 394 | "version": "==2.6" 395 | }, 396 | "pycparser": { 397 | "hashes": [ 398 | "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", 399 | "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" 400 | ], 401 | "markers": "python_version >= '3.8'", 402 | "version": "==2.23" 403 | }, 404 | "pyopenssl": { 405 | "hashes": [ 406 | "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", 407 | "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329" 408 | ], 409 | "markers": "python_version >= '3.7'", 410 | "version": "==25.3.0" 411 | }, 412 | "pyrfc3339": { 413 | "hashes": [ 414 | "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", 415 | "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f" 416 | ], 417 | "markers": "python_version >= '3.9'", 418 | "version": "==2.1.0" 419 | }, 420 | "python-dateutil": { 421 | "hashes": [ 422 | "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", 423 | "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" 424 | ], 425 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 426 | "version": "==2.9.0.post0" 427 | }, 428 | "pyyaml": { 429 | "hashes": [ 430 | "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", 431 | "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", 432 | "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", 433 | "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", 434 | "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", 435 | "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", 436 | "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", 437 | "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", 438 | "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", 439 | "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", 440 | "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", 441 | "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", 442 | "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", 443 | "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", 444 | "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", 445 | "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", 446 | "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", 447 | "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", 448 | "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", 449 | "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", 450 | "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", 451 | "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", 452 | "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", 453 | "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", 454 | "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", 455 | "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", 456 | "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", 457 | "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", 458 | "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", 459 | "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", 460 | "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", 461 | "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", 462 | "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", 463 | "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", 464 | "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", 465 | "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", 466 | "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", 467 | "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", 468 | "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", 469 | "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", 470 | "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", 471 | "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", 472 | "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", 473 | "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", 474 | "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", 475 | "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", 476 | "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", 477 | "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", 478 | "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", 479 | "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", 480 | "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", 481 | "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", 482 | "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", 483 | "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", 484 | "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", 485 | "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", 486 | "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", 487 | "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", 488 | "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", 489 | "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", 490 | "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", 491 | "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", 492 | "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", 493 | "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", 494 | "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", 495 | "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", 496 | "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", 497 | "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", 498 | "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", 499 | "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", 500 | "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", 501 | "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", 502 | "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" 503 | ], 504 | "index": "pypi", 505 | "markers": "python_version >= '3.8'", 506 | "version": "==6.0.3" 507 | }, 508 | "requests": { 509 | "hashes": [ 510 | "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", 511 | "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" 512 | ], 513 | "markers": "python_version >= '3.9'", 514 | "version": "==2.32.5" 515 | }, 516 | "s3transfer": { 517 | "hashes": [ 518 | "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", 519 | "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" 520 | ], 521 | "markers": "python_version >= '3.9'", 522 | "version": "==0.16.0" 523 | }, 524 | "six": { 525 | "hashes": [ 526 | "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", 527 | "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" 528 | ], 529 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 530 | "version": "==1.17.0" 531 | }, 532 | "urllib3": { 533 | "hashes": [ 534 | "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", 535 | "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1" 536 | ], 537 | "markers": "python_version >= '3.9'", 538 | "version": "==2.6.0" 539 | } 540 | }, 541 | "develop": { 542 | "astroid": { 543 | "hashes": [ 544 | "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", 545 | "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b" 546 | ], 547 | "markers": "python_full_version >= '3.10.0'", 548 | "version": "==4.0.2" 549 | }, 550 | "dill": { 551 | "hashes": [ 552 | "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", 553 | "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" 554 | ], 555 | "markers": "python_version >= '3.8'", 556 | "version": "==0.4.0" 557 | }, 558 | "iniconfig": { 559 | "hashes": [ 560 | "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", 561 | "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" 562 | ], 563 | "markers": "python_version >= '3.10'", 564 | "version": "==2.3.0" 565 | }, 566 | "isort": { 567 | "hashes": [ 568 | "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", 569 | "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187" 570 | ], 571 | "markers": "python_full_version >= '3.10.0'", 572 | "version": "==7.0.0" 573 | }, 574 | "librt": { 575 | "hashes": [ 576 | "sha256:02b98cb2558b32d10489abcdf5119f08b27d6cf4e587159d3fcb0a1609d98d4c", 577 | "sha256:0a0d0c70418e0c37c040a3acace252a21e25751f3fa96084facf24783d24fd5d", 578 | "sha256:0ce1f5863839c85c8e7e1467dd939d4af5e59bab8852852a9d8b7a9dbcdcaf2a", 579 | "sha256:0e7a4dcb2419b766a034a62d28708a11e92d790aa6faa74913e587ccc4c2fc55", 580 | "sha256:0fee181b2f73c14d1f80380b91945305919e409748bc386008fe56e23e9b0652", 581 | "sha256:12753c83c2e29c7bb28627bbada0cfcf19e8225c6da98eb7c590b27743115298", 582 | "sha256:15875129cce2377bd703557314b81c4e7bfc63fdcd8247b0c5bf7dc34a8d61b5", 583 | "sha256:1aa6eb96952cadb861b8fc5a41832349935a5a4bd1478b8425c023ece98af72c", 584 | "sha256:1c16a988ef540b6dba0be057c343ff7489c95080348b70b6a1fa527128cf386b", 585 | "sha256:1d156fce27e92ebd5094ff8e9fb622d945026fd552e8eda4f0acbb58164e67b6", 586 | "sha256:1e3f975f62352ee20a0b1071532bf91e77097a541ab6f68e8cdfc56e708bed11", 587 | "sha256:211a312a9ab2226ecdb509087bc6a0d0f9d8550565a0d1b848576b9119c69cda", 588 | "sha256:2471e23a12599761e2f052a84dd359ba1d2b34d018d2d8039aa0f8865ee7a563", 589 | "sha256:263cc4beae054d088292471434af6fc710eed357161f0d45c1783830cb5332b2", 590 | "sha256:2bdf11b510877003330fff88b71985c1d73f8710527256bada5c78d4c8c341ef", 591 | "sha256:2c23b7ab197ee9ed29cd0b61ac1e24e4483f24612f4626833877e19b28f95935", 592 | "sha256:350385b5f8d3f71686b4aa2181d654f01de50a0e4b11eb20fa36f5b00dc5c440", 593 | "sha256:369cf96ba818af4d14a95ce4d00f163cfa64d800ebb5a0f54556b9cb4346d97b", 594 | "sha256:37c9133c69adcf6229e3aecea56d1c77a79dd00f5d65e7f28c500590b4edcf4b", 595 | "sha256:388f794cd52ed4692ec0e3b00a07a502ef879bac90fa21f6e0035422c7b117c8", 596 | "sha256:3cd85f9b52300cc0a748a72d8eba2f7998f03e1dfb44b8db6e2ca344f175e1a9", 597 | "sha256:43028b50350caf3f27168d7a5f824d23e3300f20eb2bcb99fe03f14568dad0fc", 598 | "sha256:46293b0541a04909581084781aaa0c0c56d2b430a551717de2535e564f569127", 599 | "sha256:49bf5cb376e120db09c2ab56fde3ce4d3933f496d74c749948964e11d1c7ada6", 600 | "sha256:4ce4baf7f74a5eb676a9688cf31ec8f25835cf84a3f129b781bde55daf267cf1", 601 | "sha256:4d601771f291cd28aaefe115b0c3105d36fdd7d0d0abcc23bb17714c17b370bb", 602 | "sha256:4e5b64996f1f116b6ba9597a8ff9f098c240926abbd024d1bc8e2605b46f7590", 603 | "sha256:506fd319530866802f9e63f28e3822e24a38dcf1814b5b6f54690bfdb55ee947", 604 | "sha256:51d899c7460cb30e68f7e83f4d68915127a8c7eaada7657702287e4c542f88d4", 605 | "sha256:645721c2462136ed2783d6ba1edeafbc8f229f7229967fdaacf5f826fba99cf9", 606 | "sha256:654a2a2e6325fc4906200156c98e5ef898011d4ee998f8b4277d96356920703a", 607 | "sha256:6b70ae76272f107b6492e0a135a5af150efec29abd20f7a299aa4193e74bb9a9", 608 | "sha256:6ef7654f79590bef5cc2256ffc2e9d8fccf55752f70a45e26aaac74237ab8552", 609 | "sha256:75c787db17786f5a732a1eaf09b04d2c43f8931efe0876e594b8be77e603a2e1", 610 | "sha256:810d09701fc9615943a7d510b50d450fdf1e54a8917e268cf6fa907bc61cd8df", 611 | "sha256:867c904b6748dfa212f9de8f27537f1e51f9cc7a51474a3bdafe136d00608e45", 612 | "sha256:88011c66ef4053807e45158cce6c79f8f1a12d533b9a918a062273c57f8846b6", 613 | "sha256:89563b5aaada1750e106d0b04953b147c07ac07507e79252413a7e2d59153990", 614 | "sha256:90119009b757b3a611aba38e9ee163b49864825572325e2eec0080c42fc8bb69", 615 | "sha256:912f87f7059bd07644c675a499fff1bc3d39aea324dc4a818bf1fb163ac11fe6", 616 | "sha256:92d7d9a4ab2ac35cf3202d555125dcfa29ec55ecee10cd5b8c9de412b0ad4ce1", 617 | "sha256:938050cb83c54cbd636e3b68df8dee488740f7de557b6d3dc77998b825d544b1", 618 | "sha256:96715093db6f983ca9c7d8a4e36b450d7c989c3b07839bb7bc3b8be12cf601af", 619 | "sha256:97d3b787e78e8cc1b14513747cc677d3390493871394e3da9ac50dec99e2dc43", 620 | "sha256:99d86939d3f5be4734ff3d87923002b816e047fbe35eca731ada5ec1871afc01", 621 | "sha256:a531d4ae278713495768030ff02fc687cc174be1bf55f5084303d470e170ba7e", 622 | "sha256:a914759833137621c8fab73ecc0701921689f7bd29bbc34fd9cadbc6057a5261", 623 | "sha256:af5ab2c4cf132cedba4359551c4f05ef2da00229aaae13e3f8a337171bb700d9", 624 | "sha256:b1ed0aa6c0d97697559200f64bbf1c5f04767631d8494b2ace593f0a9353d63b", 625 | "sha256:ba50f3f01eac1066409988a7b5dcf741a474917bdef0a645ed21525f2dae0fca", 626 | "sha256:bbe9364d5b25f1fce27acaf695205a89ba2f3d79c668b03bde7315ba4b088b60", 627 | "sha256:bcd71a7ab212ca325013f968d06b72bb5ff83fb190dd582aa010e9c939a67050", 628 | "sha256:c18415a23b465fc379a4a3e6e71c28f3263a111d6a0811c53b1d50ca9e1d7642", 629 | "sha256:c3c10fad1468457b2d13d824b7cde8946a4caa76f18fe127c7e549d1730ab271", 630 | "sha256:ca62bc77d6e2f1ece0e141c28e2778ff79f1ca50f7824a2d6237abe9397997f5", 631 | "sha256:ce9fabcda1b5015dd2ff368a9d2f36586eb9fee375e3ee407f18045f4c032516", 632 | "sha256:d2af9c598b2cb88e3d0afcd5caca0fdbb322a93c9043d7c7fad758b0375a5263", 633 | "sha256:d7884f93a210e465c793023185672816d0e94a748fd8728fb7f5cb4a7e457da7", 634 | "sha256:d89460a3a0dc0a6621c17be4eb84747b80a2e68e8da1b8cc6c2d8fc0a642b50e", 635 | "sha256:da5edaf3c650fa9955d7343d1e057fdfc1adb3484621847331d8f01c84de70cc", 636 | "sha256:e20cb95262897eea692eced3398f7be6647d38244c1fa8480c0e48337aac0080", 637 | "sha256:e565e20cb349f93a4eac2509118ed75a9520dc7d757e84035a50a3307d97e3cc", 638 | "sha256:e92323133242ff29eec97538f5d1421e8b96abb3212a07b9c6cea514dd58ddba", 639 | "sha256:e95d45bfa4f207a9117ae7fb60c5cb0308eb77a924151a0b9a7d2fb70d8aec14", 640 | "sha256:eab63367bdb304e87d108cfd078b0d9bfa62f4fe3e5daf9afc5e159676cac15b", 641 | "sha256:ec5235ce0f0ab7f3006c5ea9b673d2168030911b7d3a73f751a809e12c5ae54f", 642 | "sha256:ed2f2d991efb60218502b1a32f666cebb33deb904a176e8c36fcc8f7061f49b9", 643 | "sha256:ee41eff32c0d1c08f50c32cdd2c2314366cea3912074b68db95df8cc4015eab3", 644 | "sha256:f312a192534cf162306a9f00f6d5d6f432f9f8d07f9f726111de477cec8d3ddf", 645 | "sha256:f32955c82ac5372f8d841fbe4d828f3538ef26f86ab7a275041100122513e5bf", 646 | "sha256:f5771fd63fe30dfbc94ac08eb6f590fb74964d90aba14c06ac94ed40cbff9f99", 647 | "sha256:f7ab208a759db0b607c785b8970d51ad101ebec7de4b13fbedafc4207508df85", 648 | "sha256:f8054546544f70cd27cb5e0a73c8de271c9dcc664741399acd584134310e312a", 649 | "sha256:f86024966f5bd4f962cbd54a4ad5d0e435fd3686f7edcd78c5aa84bb9427fa16", 650 | "sha256:fcfe89d3bb67df63e2cb1e00a379bbc73720b43a4b8dd94ac4ca87ef32ec0f4d", 651 | "sha256:fffb19b11f49c516b9cc4935e5ae01b07dfaf77b61f951c55ac9f51d3e9304aa" 652 | ], 653 | "markers": "python_version >= '3.9'", 654 | "version": "==0.7.0" 655 | }, 656 | "mccabe": { 657 | "hashes": [ 658 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 659 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 660 | ], 661 | "markers": "python_version >= '3.6'", 662 | "version": "==0.7.0" 663 | }, 664 | "mypy": { 665 | "hashes": [ 666 | "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", 667 | "sha256:0dde5cb375cb94deff0d4b548b993bec52859d1651e073d63a1386d392a95495", 668 | "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", 669 | "sha256:0ea4fd21bb48f0da49e6d3b37ef6bd7e8228b9fe41bbf4d80d9364d11adbd43c", 670 | "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", 671 | "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", 672 | "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", 673 | "sha256:16f76ff3f3fd8137aadf593cb4607d82634fca675e8211ad75c43d86033ee6c6", 674 | "sha256:1cf9c59398db1c68a134b0b5354a09a1e124523f00bacd68e553b8bd16ff3299", 675 | "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", 676 | "sha256:3210d87b30e6af9c8faed61be2642fcbe60ef77cec64fa1ef810a630a4cf671c", 677 | "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", 678 | "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", 679 | "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", 680 | "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", 681 | "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", 682 | "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", 683 | "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", 684 | "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", 685 | "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", 686 | "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", 687 | "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", 688 | "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", 689 | "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", 690 | "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", 691 | "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", 692 | "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", 693 | "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", 694 | "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", 695 | "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", 696 | "sha256:e2c1101ab41d01303103ab6ef82cbbfedb81c1a060c868fa7cc013d573d37ab5", 697 | "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", 698 | "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", 699 | "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", 700 | "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", 701 | "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", 702 | "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", 703 | "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d" 704 | ], 705 | "index": "pypi", 706 | "markers": "python_version >= '3.9'", 707 | "version": "==1.19.0" 708 | }, 709 | "mypy-extensions": { 710 | "hashes": [ 711 | "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", 712 | "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" 713 | ], 714 | "markers": "python_version >= '3.8'", 715 | "version": "==1.1.0" 716 | }, 717 | "packaging": { 718 | "hashes": [ 719 | "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", 720 | "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" 721 | ], 722 | "markers": "python_version >= '3.8'", 723 | "version": "==25.0" 724 | }, 725 | "pathspec": { 726 | "hashes": [ 727 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 728 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 729 | ], 730 | "markers": "python_version >= '3.8'", 731 | "version": "==0.12.1" 732 | }, 733 | "platformdirs": { 734 | "hashes": [ 735 | "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", 736 | "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31" 737 | ], 738 | "markers": "python_version >= '3.10'", 739 | "version": "==4.5.1" 740 | }, 741 | "pluggy": { 742 | "hashes": [ 743 | "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", 744 | "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" 745 | ], 746 | "markers": "python_version >= '3.9'", 747 | "version": "==1.6.0" 748 | }, 749 | "pygments": { 750 | "hashes": [ 751 | "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", 752 | "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" 753 | ], 754 | "markers": "python_version >= '3.8'", 755 | "version": "==2.19.2" 756 | }, 757 | "pylint": { 758 | "hashes": [ 759 | "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", 760 | "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2" 761 | ], 762 | "index": "pypi", 763 | "markers": "python_full_version >= '3.10.0'", 764 | "version": "==4.0.4" 765 | }, 766 | "pytest": { 767 | "hashes": [ 768 | "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", 769 | "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad" 770 | ], 771 | "index": "pypi", 772 | "markers": "python_version >= '3.10'", 773 | "version": "==9.0.1" 774 | }, 775 | "tomlkit": { 776 | "hashes": [ 777 | "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", 778 | "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" 779 | ], 780 | "markers": "python_version >= '3.8'", 781 | "version": "==0.13.3" 782 | }, 783 | "typing-extensions": { 784 | "hashes": [ 785 | "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", 786 | "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" 787 | ], 788 | "markers": "python_version >= '3.9'", 789 | "version": "==4.15.0" 790 | } 791 | } 792 | } 793 | --------------------------------------------------------------------------------