Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/esa data ecosystem #33

Merged
merged 64 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
60c8d7d
Use streaming request in with block to ensure connection is closed.
sharkinsspatial Aug 9, 2021
784e9bf
Reduce date search range now that IntHub backfill process is implemen…
sharkinsspatial Aug 9, 2021
ec38996
Increase number of concurrent connections to new Inthub max minus one.
sharkinsspatial Aug 9, 2021
04e5b3a
Merge branch 'main' of https://github.com/NASA-IMPACT/hls-sentinel2-d…
sharkinsspatial Aug 10, 2021
1f59921
Update downloader to use CDE endpoint.
sharkinsspatial Jun 9, 2023
188e9c9
Refactor token retrieval and update tests for new CDE download urls.
sharkinsspatial Jun 9, 2023
a60b72c
Use Copernicus credentials in place of IntHub2 credentials.
sharkinsspatial Jun 9, 2023
d89181c
Begin refactoring usage of pipenv due to lock resolution hanging.
sharkinsspatial Jun 11, 2023
d9b5225
Temporarily remove checksum verification for scale testing.
sharkinsspatial Jun 11, 2023
6c5c408
Upgrade deps to fix `black` error and `reponses.matchers` import error
chuckwondo Jun 26, 2023
620d3a9
Improve static typing, db session mgmt, unit tests
chuckwondo Jun 26, 2023
1ce8964
Use standalone Lambda for Copernicus Keycloak token rotation.
sharkinsspatial Aug 8, 2023
f295176
Merge branch 'fix/esa_data_ecosystem' of https://github.com/NASA-IMPA…
sharkinsspatial Aug 8, 2023
55b209b
Reduce keycloak token refresh interval to 5 minutes.
sharkinsspatial Aug 8, 2023
6a9f0b5
Update tests with new get_copernicus_token logic.
sharkinsspatial Aug 8, 2023
34a89fe
Make accepted tile IDs a set, not a list.
chuckwondo Aug 9, 2023
24bc487
Use iso8601 date parsing library
chuckwondo Aug 9, 2023
f8e1854
Use OpenSearch for fetching download links
chuckwondo Aug 14, 2023
dfef4f3
Iterate link fetcher lambda to avoid timeouts
chuckwondo Aug 18, 2023
7b7d495
Add success step to link fetcher state machine
chuckwondo Aug 18, 2023
1872e78
Re-incorporate checksum usage using new copernicus OData endpoint.
sharkinsspatial Aug 31, 2023
e6245b8
Filter out links acquired over 30 days ago
chuckwondo Sep 5, 2023
9f8d121
Filter out links acquired over 30 days ago
chuckwondo Sep 6, 2023
7eb4a74
Retry request errors (mainly for 503 responses)
chuckwondo Sep 6, 2023
a9fb327
Use tox for running integration tests
chuckwondo Sep 8, 2023
b05534f
Add missing checksum response for fixture.
sharkinsspatial Sep 9, 2023
5a4e58c
Merge branch 'fix/esa_data_ecosystem' of https://github.com/NASA-IMPA…
sharkinsspatial Sep 9, 2023
94d237d
Tidy up downloader unit tests
chuckwondo Sep 11, 2023
6a63bbf
Replace pipenv with tox in top-level Makefile
chuckwondo Sep 12, 2023
4ecbd68
Downgrade Node to 18.x, as 20.x is untested w/CDK
chuckwondo Sep 12, 2023
929c308
Remove unused import
chuckwondo Sep 12, 2023
d4976e7
Fix imports via isort
chuckwondo Sep 12, 2023
2655b34
Run cdk via tox using .env settings
chuckwondo Sep 12, 2023
23e24cf
Load .env values for integration stack
chuckwondo Sep 12, 2023
2adb1ca
Link fetcher no longer needs credentials
chuckwondo Sep 12, 2023
d8e1ce9
Fix error in link fetcher retry handler
chuckwondo Sep 12, 2023
8d5654f
Make check for HTTP client error more robust
chuckwondo Sep 15, 2023
b90c1f8
Move SFN def to asl.json file for local rendering
chuckwondo Sep 15, 2023
63cfd68
Update link fetcher int. tests to work with new search API
chuckwondo Sep 18, 2023
457f302
Add DLQ to downloader queue
chuckwondo Sep 18, 2023
899fd26
Update downloader int. tests for new API
chuckwondo Sep 19, 2023
da3f562
Revert "Move SFN def to asl.json file for local rendering"
chuckwondo Sep 25, 2023
6da5561
Reduce downloader concurrency to 15 due to new ESA throttling.
sharkinsspatial Sep 26, 2023
fae4d4c
Update OData checksum request after ESA introduced a breaking change.
sharkinsspatial Oct 3, 2023
a979157
Default search totalResults to -1 when missing
chuckwondo Oct 17, 2023
b578d6e
Update downloader integration test for checksums
chuckwondo Oct 18, 2023
d3043d3
Add retry config to link fetcher step
chuckwondo Oct 18, 2023
2a735fa
Fix null totalResults in search response
chuckwondo Oct 31, 2023
f86a75f
Set DLQ retention period equal to that of primary queue
chuckwondo Nov 2, 2023
2a98dff
Fix build warnings and errors
chuckwondo Nov 9, 2023
df112f7
Replace outdated pipenv action that fails build
chuckwondo Nov 9, 2023
9e42036
Fix dependency errors that break CI build
chuckwondo Nov 9, 2023
421f3f2
Temporarily restore top-level Pipfile for linting
chuckwondo Nov 9, 2023
4a7f4cc
Remove unused var to fix lint error
chuckwondo Nov 9, 2023
7905012
Ensure lambdas/layers get separate pipenv envs
chuckwondo Nov 9, 2023
a09dffc
Debug failing lint/test in CI for layers/db
chuckwondo Nov 9, 2023
b8f10c5
Include all deps in requirements.txt for layers/db
chuckwondo Nov 9, 2023
5425579
Fix CI lint failure
chuckwondo Nov 9, 2023
d245195
Bump Python version in CI
chuckwondo Nov 9, 2023
94c870d
Debug layers/db build failure
chuckwondo Nov 9, 2023
2895b11
Pin sqlalchemy
chuckwondo Nov 9, 2023
610cf83
Set default GH shell to bash; update action versions
chuckwondo Nov 10, 2023
89e5451
Set shell to bash in Makefile
chuckwondo Nov 10, 2023
dee34a3
Disable integration tests for now (CI broken)
chuckwondo Nov 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 94 additions & 72 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

79 changes: 42 additions & 37 deletions .github/workflows/deploy-on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,91 @@ on:
branches: [ main ]
types: [ published ]

defaults:
run:
shell: bash

jobs:
unit-tests:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Python 3.8.6
uses: actions/setup-python@v2
- name: Set up Python 3.8.18
uses: actions/setup-python@v4
with:
python-version: 3.8.6
python-version: 3.8.18

- name: Install Pipenv
uses: dschep/install-pipenv-action@v1
run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python

- name: Get pipenv venv hashes
id: hashes
run: |
echo "##[set-output name=root;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/Pipfile)"
echo "##[set-output name=alembic;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/alembic_migration/Pipfile)"
echo "##[set-output name=dategenerator;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/date_generator/Pipfile)"
echo "##[set-output name=linkfetcher;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/link_fetcher/Pipfile)"
echo "##[set-output name=downloader;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/downloader/Pipfile)"
echo "##[set-output name=mockscihubproductapi;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/mock_scihub_product_api/Pipfile)"
echo "##[set-output name=mockscihubsearchapi;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/mock_scihub_search_api/Pipfile)"
echo "##[set-output name=db;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/layers/db/Pipfile)"
echo "root=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/Pipfile)" >> $GITHUB_OUTPUT
echo "alembic=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/alembic_migration/Pipfile)" >> $GITHUB_OUTPUT
echo "dategenerator=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/date_generator/Pipfile)" >> $GITHUB_OUTPUT
echo "linkfetcher=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/link_fetcher/Pipfile)" >> $GITHUB_OUTPUT
echo "downloader=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/downloader/Pipfile)" >> $GITHUB_OUTPUT
echo "mockscihubproductapi=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/mock_scihub_product_api/Pipfile)" >> $GITHUB_OUTPUT
echo "mockscihubsearchapi=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/lambdas/mock_scihub_search_api/Pipfile)" >> $GITHUB_OUTPUT
# echo "db=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/layers/db/Pipfile)" >> $GITHUB_OUTPUT

- name: Setup root cache
uses: actions/cache@v2
uses: actions/cache@v3
id: root-cache
with:
path: /home/runner/.local/share/virtualenvs/hls-sentinel2-downloader-serverless-${{ steps.hashes.outputs.root }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/Pipfile.lock') }}-2

- name: Setup alembic_migration cache
uses: actions/cache@v2
uses: actions/cache@v3
id: alembic-cache
with:
path: /home/runner/.local/share/virtualenvs/alembic_migration-${{ steps.hashes.outputs.alembic }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/alembic_migration/Pipfile.lock') }}

- name: Setup date_generator cache
uses: actions/cache@v2
uses: actions/cache@v3
id: date-generator-cache
with:
path: /home/runner/.local/share/virtualenvs/date_generator-${{ steps.hashes.outputs.dategenerator }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/lambdas/date_generator/Pipfile.lock') }}

- name: Setup link_fetcher cache
uses: actions/cache@v2
uses: actions/cache@v3
id: link-fetcher-cache
with:
path: /home/runner/.local/share/virtualenvs/link_fetcher-${{ steps.hashes.outputs.linkfetcher }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/lambdas/link_fetcher/Pipfile.lock') }}

- name: Setup downloader cache
uses: actions/cache@v2
uses: actions/cache@v3
id: downloader-cache
with:
path: /home/runner/.local/share/virtualenvs/downloader-${{ steps.hashes.outputs.downloader }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/lambdas/downloader/Pipfile.lock') }}

- name: Setup mock_scihub_product_api cache
uses: actions/cache@v2
uses: actions/cache@v3
id: mock-scihub-product-api-cache
with:
path: /home/runner/.local/share/virtualenvs/mock_scihub_product_api-${{ steps.hashes.outputs.mockscihubproductapi }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/lambdas/mock_scihub_product_api/Pipfile.lock') }}

- name: Setup mock_scihub_search_api cache
uses: actions/cache@v2
uses: actions/cache@v3
id: mock-scihub-search-api-cache
with:
path: /home/runner/.local/share/virtualenvs/mock_scihub_search_api-${{ steps.hashes.outputs.mockscihubsearchapi }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/lambdas/mock_scihub_search_api/Pipfile.lock') }}

- name: Setup db cache
uses: actions/cache@v2
id: db-cache
with:
path: /home/runner/.local/share/virtualenvs/db-${{ steps.hashes.outputs.db }}
key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/layers/db/Pipfile.lock') }}
# - name: Setup db cache
# uses: actions/cache@v3
# id: db-cache
# with:
# path: /home/runner/.local/share/virtualenvs/db-${{ steps.hashes.outputs.db }}
# key: ${{ hashFiles('/home/runner/work/hls-sentinel2-downloader-serverless/hls-sentinel2-downloader-serverless/layers/db/Pipfile.lock') }}

- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -123,7 +127,7 @@ jobs:
make -C lambdas/mock_scihub_search_api install

- name: Install db dependencies
if: steps.db-cache.outputs.cache-hit != 'true'
# if: steps.db-cache.outputs.cache-hit != 'true'
run: |
make -C layers/db install

Expand Down Expand Up @@ -154,13 +158,14 @@ jobs:
- name: Run unit tests
run: |
make unit-tests

deploy:
runs-on: ubuntu-20.04
needs: [unit-tests]
environment:
name: prod
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Create .env
run: |
Expand All @@ -174,30 +179,30 @@ jobs:
UPLOAD_BUCKET="${{ secrets.UPLOAD_BUCKET }}"
EOF

- name: Set up Python 3.8.6
uses: actions/setup-python@v2
- name: Set up Python 3.8.18
uses: actions/setup-python@v4
with:
python-version: 3.8.6
python-version: 3.8.18

- name: Setup up Node
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: 12
node-version-file: ".nvmrc"

- name: Install AWS CDK
run: |
npm install -g aws-cdk

- name: Install Pipenv
uses: dschep/install-pipenv-action@v1
run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python

- name: Get pipenv venv hashes
id: hashes
run: |
echo "##[set-output name=root;]$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/Pipfile)"
echo "root=$(python -c 'import sys; import base64; import hashlib; print(base64.urlsafe_b64encode(hashlib.sha256(sys.argv[-1].encode()).digest()[:6]).decode()[:8])' $(pwd)/Pipfile)" >> $GITHUB_OUTPUT

- name: Setup root cache
uses: actions/cache@v2
uses: actions/cache@v3
id: root-cache
with:
path: /home/runner/.local/share/virtualenvs/hls-sentinel2-downloader-serverless-${{ steps.hashes.outputs.root }}
Expand All @@ -209,7 +214,7 @@ jobs:
pipenv install --dev

- name: Configure awscli
uses: aws-actions/configure-aws-credentials@v1
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock

# Ignoring until migration away from pipenv is complete because layers/db/Makefile
# still runs `pipenv` for the `test` rule, so it ends up generating this Pipfile.
layers/db/Pipfile

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
12
18.17.1
65 changes: 57 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
.PHONEY: install lint format diff deploy destroy diff-integration deploy-integration destroy-integration unit-tests integration-tests
SHELL := $(shell which bash)

.PHONEY:
clean
deploy
deploy-integration
destroy
destroy-integration
diff
diff-integration
format
install
integration-tests
lint
unit-tests

clean:
pipenv --rm || true
$(MAKE) -C layers/db clean
$(MAKE) -C lambdas/link_fetcher clean
$(MAKE) -C lambdas/date_generator clean
$(MAKE) -C lambdas/downloader clean
$(MAKE) -C lambdas/mock_scihub_search_api clean
$(MAKE) -C lambdas/mock_scihub_product_api clean
$(MAKE) -C alembic_migration clean

install:
$(MAKE) -C layers/db install
Expand Down Expand Up @@ -34,22 +58,22 @@ format:
$(MAKE) -C alembic_migration format

diff:
pipenv run npx cdk diff --app cdk/app.py || true
tox -e dev -- diff --app cdk/app.py || true

deploy:
pipenv run npx cdk deploy --app cdk/app.py --require-approval never
tox -e dev -- deploy --app cdk/app.py --require-approval never

destroy:
pipenv run npx cdk destroy --app cdk/app.py --force
tox -e dev -- destroy --app cdk/app.py --force

diff-integration:
pipenv run npx cdk diff '*' --app cdk/app_integration.py || true
tox -e dev -- diff '*' --app cdk/app_integration.py || true

deploy-integration:
pipenv run npx cdk deploy '*' --app cdk/app_integration.py --require-approval never
tox -e dev -- deploy '*' --app cdk/app_integration.py --require-approval never

destroy-integration:
pipenv run npx cdk destroy '*' --app cdk/app_integration.py --force
tox -e dev -- destroy '*' --app cdk/app_integration.py --force

unit-tests:
$(MAKE) -C lambdas/link_fetcher test
Expand All @@ -61,4 +85,29 @@ unit-tests:
$(MAKE) -C alembic_migration test

integration-tests:
pipenv run pytest -s integration_tests
tox -e integration

#-------------------------------------------------------------------------------
# For invocation via tox only (i.e., in tox.ini commands entries)
#-------------------------------------------------------------------------------

tox:
@if [[ -z $${TOX_ENV_DIR+x} ]]; then echo "ERROR: For tox.ini use only" >&2; exit 1; fi

# Install node in the virtualenv, if it's not installed or it's the wrong version.
# Pull node version from .nvmrc file.
install-node: tox
@NODE_VERSION=$$(<.nvmrc); \
if [[ ! $$(type node 2>/dev/null) =~ $${VIRTUAL_ENV} || ! $$(node -v) =~ $${NODE_VERSION} ]]; then \
nodeenv --node $${NODE_VERSION} --python-virtualenv; \
fi

# Install cdk in the virtualenv, if it's not installed or it's the wrong version.
# Pull CDK version from value of aws_cdk_version variable in setup.py.
install-cdk: install-node
@CDK_VERSION=$$(grep "aws_cdk_version = " setup.py | sed -Ee 's/aws_cdk_version = "(.+)"/\1/'); \
if [[ ! $$(type cdk 2>/dev/null) =~ $${VIRTUAL_ENV} || ! $$(cdk --version) =~ $${CDK_VERSION} ]]; then \
npm install --location global "aws-cdk@$${CDK_VERSION}"; \
fi
@# Acknowledge CDK notice regarding CDK v1 being in maintenance mode.
@grep -q 19836 cdk.context.json 2>/dev/null || cdk acknowledge 19836
31 changes: 3 additions & 28 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,9 @@ name = "pypi"
[packages]

[dev-packages]
flake8 = "==3.8.4"
isort = "==5.6.4"
black = "==20.8b1"
"aws-cdk.core" = "==1.105.0"
"aws-cdk.aws-lambda" = "==1.105.0"
"aws-cdk.aws-lambda-python" = "==1.105.0"
"aws-cdk.aws-secretsmanager" = "==1.105.0"
"aws-cdk.aws-sqs" = "==1.105.0"
"aws-cdk.aws-ec2" = "==1.105.0"
"aws-cdk.aws-rds" = "==1.105.0"
"aws-cdk.aws-stepfunctions" = "==1.105.0"
"aws-cdk.aws-stepfunctions-tasks" = "==1.105.0"
"aws-cdk.aws-events" = "==1.105.0"
"aws-cdk.aws-events-targets" = "==1.105.0"
"aws-cdk.aws-apigateway" = "==1.105.0"
"aws-cdk.aws-ssm" = "==1.105.0"
"aws-cdk.aws-s3" = "==1.105.0"
"aws-cdk.aws-lambda-event-sources" = "==1.105.0"
"aws-cdk.aws-logs" = "==1.105.0"
db = {editable = true, path = "./layers/db"}
psycopg2 = "==2.8.6"
boto3 = "==1.16.60"
pytest = "==6.2.1"
polling2 = "==0.4.6"
assertpy = "==1.1"
flake8 = "==6.1.0"
isort = "==5.12.0"
black = "==23.11.0"

[requires]
python_version = "3.8"

[pipenv]
allow_prereleases = true
Loading