diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..00a697942 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +db_www_export.dump +media diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 1b10697ac..73a5c2de8 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -117,7 +117,7 @@ jobs: --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ --size Standard_B2s \ - --image UbuntuLTS \ + --image `az vm image list --all -p Canonical -f UbuntuServer -s 18.04-LTS --query [].urn -o tsv | sort | tail -n 1` \ --ssh-key-values ${{ secrets.DEV_PUB_KEYS }} && \ export NEW_IP=$(az vm list-ip-addresses --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" --query [].virtualMachine.network[].publicIpAddresses[][].ipAddress --output tsv) && \ echo "NEW_IP=$NEW_IP" >> $GITHUB_ENV && \ @@ -135,6 +135,50 @@ jobs: --access Allow \ --protocol Tcp \ --destination-port-ranges 5000 && \ + az network nsg rule create \ + --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ + --nsg-name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}NSG" \ + --name AllowPrometheusPort9157 \ + --priority 1011 \ + --access Allow \ + --protocol Tcp \ + --destination-port-ranges 9157 && \ + az vm run-command invoke \ + --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ + --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ + --command-id RunShellScript \ + --scripts "\ + set -eux + adduser prometheus-client --disabled-password --gecos '' + cd /home/prometheus-client/ + PROMETHEUS_VERSION=1.7.0 + wget https://github.com/prometheus/node_exporter/releases/download/v\$PROMETHEUS_VERSION/node_exporter-\$PROMETHEUS_VERSION.linux-amd64.tar.gz + tar -xvzf node_exporter-\$PROMETHEUS_VERSION.linux-amd64.tar.gz + echo \"\ + [Unit] + Description=Prometheus Node Exporter + Wants=network-online.target + After=network-online.target + + [Service] + User=prometheus-client + Group=prometheus-client + Type=simple + ExecStart=/home/prometheus-client/node_exporter-\$PROMETHEUS_VERSION.linux-amd64/node_exporter \\\\ + --collector.systemd \\\\ + --web.listen-address=:9157 \\\\ + --web.config.file /home/prometheus-client/web-config.yaml + + [Install] + WantedBy=multi-user.target + \" > /etc/systemd/system/prometheus-node-exporter.service + echo 'basic_auth_users: + # Do not include the dollars in the secret, as escaping is a pain + # Password is generated using htpasswd -nBC 10 "" | tr -d ':' + prom: \"\$2y\$10\$${{ secrets.PROMETHEUS_CLIENT_PASSWORD_HASHED_PARTIAL }}\" + ' > /home/prometheus-client/web-config.yaml + systemctl enable --now prometheus-node-exporter.service + " && \ az vm run-command invoke \ --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ @@ -283,7 +327,7 @@ jobs: --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ --size Standard_B2s \ - --image UbuntuLTS \ + --image `az vm image list --all -p Canonical -f UbuntuServer -s 18.04-LTS --query [].urn -o tsv | sort | tail -n 1` \ --ssh-key-values ${{ secrets.DEV_PUB_KEYS }} && \ export NEW_IP=$(az vm list-ip-addresses --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" --query [].virtualMachine.network[].publicIpAddresses[][].ipAddress --output tsv) && \ echo "NEW_IP=$NEW_IP" >> $GITHUB_ENV && \ @@ -301,6 +345,50 @@ jobs: --access Allow \ --protocol Tcp \ --destination-port-ranges 5000 && \ + az network nsg rule create \ + --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ + --nsg-name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}NSG" \ + --name AllowPrometheusPort9157 \ + --priority 1011 \ + --access Allow \ + --protocol Tcp \ + --destination-port-ranges 9157 && \ + az vm run-command invoke \ + --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ + --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ + --command-id RunShellScript \ + --scripts "\ + set -eux + adduser prometheus-client --disabled-password --gecos '' + cd /home/prometheus-client/ + PROMETHEUS_VERSION=1.7.0 + wget https://github.com/prometheus/node_exporter/releases/download/v\$PROMETHEUS_VERSION/node_exporter-\$PROMETHEUS_VERSION.linux-amd64.tar.gz + tar -xvzf node_exporter-\$PROMETHEUS_VERSION.linux-amd64.tar.gz + echo \"\ + [Unit] + Description=Prometheus Node Exporter + Wants=network-online.target + After=network-online.target + + [Service] + User=prometheus-client + Group=prometheus-client + Type=simple + ExecStart=/home/prometheus-client/node_exporter-\$PROMETHEUS_VERSION.linux-amd64/node_exporter \\\\ + --collector.systemd \\\\ + --web.listen-address=:9157 \\\\ + --web.config.file /home/prometheus-client/web-config.yaml + + [Install] + WantedBy=multi-user.target + \" > /etc/systemd/system/prometheus-node-exporter.service + echo 'basic_auth_users: + # Do not include the dollars in the secret, as escaping is a pain + # Password is generated using htpasswd -nBC 10 "" | tr -d ':' + prom: \"\$2y\$10\$${{ secrets.PROMETHEUS_CLIENT_PASSWORD_HASHED_PARTIAL }}\" + ' > /home/prometheus-client/web-config.yaml + systemctl enable --now prometheus-node-exporter.service + " && \ az vm run-command invoke \ --resource-group "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ --name "${{ env.STAGE }}-${{ env.NAME }}-${{ env.NEW_COLOUR }}" \ diff --git a/Dockerfile b/Dockerfile index c081c140b..f79d8f61c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,10 +94,16 @@ WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ COPY requirements_dev.txt /usr/src/app/ COPY entrypoint.sh /usr/src/app/ -ENV PATH=$HOME/.cargo/bin:$PATH +ENV VIRTUAL_ENV=/usr/src/venv +ENV PATH=$VIRTUAL_ENV/bin:$HOME/.cargo/bin:$PATH RUN apk -U upgrade +# Use a virtual env here, because othewise we get conflicats between Alpine's +# packages and pip's. (This has started happening because we switched to pip- +# tools which pins every dependency). RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo libwebp libwebp-tools &&\ + python3 -m venv /usr/src/venv &&\ + . /usr/src/venv/bin/activate &&\ pip3 install -r requirements_dev.txt RUN apk add --no-cache gettext diff --git a/config/nginx/website_dev.conf b/config/nginx/website_dev.conf index fd975f4f1..f85054996 100644 --- a/config/nginx/website_dev.conf +++ b/config/nginx/website_dev.conf @@ -87,3 +87,13 @@ server { return 302 https://iatiwebsitedev.blob.core.windows.net/dev-iati-website$1; } } + + +# Proxy the prometheus port from the destination server +server { + listen 9158; + + location / { + proxy_pass http://XX.XX.XX.XX:9157; + } +} diff --git a/config/nginx/website_prod.conf b/config/nginx/website_prod.conf index 8564db667..dd1b680c9 100644 --- a/config/nginx/website_prod.conf +++ b/config/nginx/website_prod.conf @@ -87,3 +87,13 @@ server { return 302 https://cdn.iatistandard.org/prod-iati-website$1; } } + + +# Proxy the prometheus port from the destination server +server { + listen 9158; + + location / { + proxy_pass http://XX.XX.XX.XX:9157; + } +} diff --git a/requirements.in b/requirements.in new file mode 100644 index 000000000..712a66d83 --- /dev/null +++ b/requirements.in @@ -0,0 +1,26 @@ +Babel==2.9.1 +beautifulsoup4==4.8.2 +bleach==3.3.0 +dj-database-url==0.5 +django-compressor==4.1 +django-extensions==3.1.3 +django-import-export==2.5.0 +django-haystack==3.0 +django-modeltranslation==0.17.2 +django-prettyjson==0.4.1 +django-recaptcha3==0.4.0 +django-storages[azure]==1.11.1 +django-widget-tweaks==1.4.8 +Django==3.2.4 +elasticsearch==6.8.2 +gunicorn==20.1.0 +opencensus-ext-azure==1.0.7 +opencensus-ext-django==0.7.4 +PyGithub==1.55 +psycopg2-binary==2.8.6 +requests==2.25.1 +wagtail-modeltranslation==0.13.0 +wagtail>=4.1,<4.2 +Wand==0.6.6 +whitenoise==5.2.0 +wrapt==1.11.* diff --git a/requirements.txt b/requirements.txt index 712a66d83..a21657bc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,242 @@ -Babel==2.9.1 +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile requirements.in +# +anyascii==0.3.2 + # via wagtail +asgiref==3.7.2 + # via django +azure-common==1.1.28 + # via + # azure-storage-blob + # azure-storage-common +azure-storage-blob==2.1.0 + # via django-storages +azure-storage-common==2.1.0 + # via azure-storage-blob +babel==2.9.1 + # via -r requirements.in beautifulsoup4==4.8.2 + # via + # -r requirements.in + # wagtail bleach==3.3.0 -dj-database-url==0.5 + # via -r requirements.in +cachetools==5.3.1 + # via google-auth +certifi==2023.7.22 + # via requests +cffi==1.15.1 + # via + # cryptography + # pynacl +chardet==4.0.0 + # via requests +cryptography==41.0.3 + # via azure-storage-common +defusedxml==0.7.1 + # via odfpy +deprecated==1.2.14 + # via pygithub +diff-match-patch==20230430 + # via django-import-export +dj-database-url==0.5.0 + # via -r requirements.in +django==3.2.4 + # via + # -r requirements.in + # django-appconf + # django-extensions + # django-filter + # django-haystack + # django-import-export + # django-modelcluster + # django-modeltranslation + # django-permissionedforms + # django-prettyjson + # django-storages + # django-taggit + # django-treebeard + # djangorestframework + # opencensus-ext-django + # wagtail + # wagtail-modeltranslation +django-appconf==1.0.5 + # via django-compressor django-compressor==4.1 + # via -r requirements.in django-extensions==3.1.3 -django-import-export==2.5.0 + # via -r requirements.in +django-filter==22.1 + # via wagtail django-haystack==3.0 + # via -r requirements.in +django-import-export==2.5.0 + # via -r requirements.in +django-modelcluster==6.0 + # via wagtail django-modeltranslation==0.17.2 + # via + # -r requirements.in + # wagtail-modeltranslation +django-permissionedforms==0.1 + # via wagtail django-prettyjson==0.4.1 + # via -r requirements.in django-recaptcha3==0.4.0 + # via -r requirements.in django-storages[azure]==1.11.1 + # via -r requirements.in +django-taggit==3.1.0 + # via wagtail +django-treebeard==4.7 + # via wagtail django-widget-tweaks==1.4.8 -Django==3.2.4 + # via -r requirements.in +djangorestframework==3.14.0 + # via wagtail +draftjs-exporter==2.1.7 + # via wagtail elasticsearch==6.8.2 + # via -r requirements.in +et-xmlfile==1.1.0 + # via openpyxl +google-api-core==2.11.1 + # via opencensus +google-auth==2.22.0 + # via google-api-core +googleapis-common-protos==1.60.0 + # via google-api-core gunicorn==20.1.0 + # via -r requirements.in +html5lib==1.1 + # via wagtail +idna==2.10 + # via requests +l18n==2021.3 + # via wagtail +markuppy==1.14 + # via tablib +odfpy==1.4.1 + # via tablib +opencensus==0.11.2 + # via + # opencensus-ext-azure + # opencensus-ext-django +opencensus-context==0.1.3 + # via opencensus opencensus-ext-azure==1.0.7 + # via -r requirements.in opencensus-ext-django==0.7.4 -PyGithub==1.55 + # via -r requirements.in +openpyxl==3.1.2 + # via + # tablib + # wagtail +packaging==23.1 + # via bleach +pillow==9.5.0 + # via wagtail +protobuf==4.24.2 + # via + # google-api-core + # googleapis-common-protos +psutil==5.9.5 + # via opencensus-ext-azure psycopg2-binary==2.8.6 + # via -r requirements.in +pyasn1==0.5.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.3.0 + # via google-auth +pycparser==2.21 + # via cffi +pygithub==1.55 + # via -r requirements.in +pyjwt==2.8.0 + # via pygithub +pynacl==1.5.0 + # via pygithub +python-dateutil==2.8.2 + # via azure-storage-common +pytz==2023.3.post1 + # via + # babel + # django + # django-modelcluster + # djangorestframework + # l18n +pyyaml==6.0.1 + # via tablib +rcssmin==1.1.0 + # via django-compressor requests==2.25.1 + # via + # -r requirements.in + # azure-storage-common + # django-recaptcha3 + # google-api-core + # opencensus-ext-azure + # pygithub + # wagtail +rjsmin==1.2.0 + # via django-compressor +rsa==4.9 + # via google-auth +six==1.16.0 + # via + # bleach + # django-modeltranslation + # django-prettyjson + # google-auth + # html5lib + # l18n + # python-dateutil +soupsieve==2.5 + # via beautifulsoup4 +sqlparse==0.4.4 + # via django +standardjson==0.3.1 + # via django-prettyjson +tablib[html,ods,xls,xlsx,yaml]==3.5.0 + # via django-import-export +telepath==0.3.1 + # via wagtail +typing-extensions==4.7.1 + # via asgiref +urllib3==1.26.16 + # via + # elasticsearch + # google-auth + # requests +wagtail==4.1.6 + # via + # -r requirements.in + # wagtail-modeltranslation wagtail-modeltranslation==0.13.0 -wagtail>=4.1,<4.2 -Wand==0.6.6 + # via -r requirements.in +wand==0.6.6 + # via -r requirements.in +webencodings==0.5.1 + # via + # bleach + # html5lib whitenoise==5.2.0 -wrapt==1.11.* + # via -r requirements.in +willow==1.4.1 + # via wagtail +wrapt==1.11.2 + # via + # -r requirements.in + # deprecated +xlrd==2.0.1 + # via tablib +xlwt==1.3.0 + # via tablib + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements_dev.in b/requirements_dev.in new file mode 100644 index 000000000..7d600797c --- /dev/null +++ b/requirements_dev.in @@ -0,0 +1,16 @@ +-r requirements.in +attrs==21.2.0 +pytest==7.1.1 +pytest-django==4.5.2 +pytest-timeout==2.1.0 +splinter==0.14.0 +pytest-splinter==3.3.1 +responses==0.13.3 +flake8==3.9.2 +pydocstyle==6.0.0 +pylint==2.4.4 +pylint-django==2.0.13 +wagtail-factories==3.1.0 +factory-boy==3.2.1 +sqlparse==0.4.2 +django-debug-toolbar==3.5.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index b57e8fdd3..822f18b27 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,16 +1,362 @@ --r requirements.txt +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile requirements_dev.in +# +anyascii==0.3.2 + # via wagtail +asgiref==3.7.2 + # via django +astroid==2.3.3 + # via pylint attrs==21.2.0 -pytest==7.1.1 -pytest-django==4.5.2 -pytest-timeout==2.1.0 -splinter==0.14.0 -pytest-splinter==3.3.1 -responses==0.13.3 + # via + # -r requirements_dev.in + # outcome + # pytest + # trio +azure-common==1.1.28 + # via + # azure-storage-blob + # azure-storage-common +azure-storage-blob==2.1.0 + # via django-storages +azure-storage-common==2.1.0 + # via azure-storage-blob +babel==2.9.1 + # via -r requirements.in +beautifulsoup4==4.8.2 + # via + # -r requirements.in + # wagtail +bleach==3.3.0 + # via -r requirements.in +cachetools==5.3.1 + # via google-auth +certifi==2023.7.22 + # via + # requests + # selenium +cffi==1.15.1 + # via + # cryptography + # pynacl +chardet==4.0.0 + # via requests +cryptography==41.0.3 + # via azure-storage-common +defusedxml==0.7.1 + # via odfpy +deprecated==1.2.14 + # via pygithub +diff-match-patch==20230430 + # via django-import-export +dj-database-url==0.5.0 + # via -r requirements.in +django==3.2.4 + # via + # -r requirements.in + # django-appconf + # django-debug-toolbar + # django-extensions + # django-filter + # django-haystack + # django-import-export + # django-modelcluster + # django-modeltranslation + # django-permissionedforms + # django-prettyjson + # django-storages + # django-taggit + # django-treebeard + # djangorestframework + # opencensus-ext-django + # wagtail + # wagtail-modeltranslation +django-appconf==1.0.5 + # via django-compressor +django-compressor==4.1 + # via -r requirements.in +django-debug-toolbar==3.5.0 + # via -r requirements_dev.in +django-extensions==3.1.3 + # via -r requirements.in +django-filter==22.1 + # via wagtail +django-haystack==3.0 + # via -r requirements.in +django-import-export==2.5.0 + # via -r requirements.in +django-modelcluster==6.0 + # via wagtail +django-modeltranslation==0.17.2 + # via + # -r requirements.in + # wagtail-modeltranslation +django-permissionedforms==0.1 + # via wagtail +django-prettyjson==0.4.1 + # via -r requirements.in +django-recaptcha3==0.4.0 + # via -r requirements.in +django-storages[azure]==1.11.1 + # via -r requirements.in +django-taggit==3.1.0 + # via wagtail +django-treebeard==4.7 + # via wagtail +django-widget-tweaks==1.4.8 + # via -r requirements.in +djangorestframework==3.14.0 + # via wagtail +draftjs-exporter==2.1.7 + # via wagtail +elasticsearch==6.8.2 + # via -r requirements.in +et-xmlfile==1.1.0 + # via openpyxl +exceptiongroup==1.1.3 + # via + # trio + # trio-websocket +factory-boy==3.2.1 + # via + # -r requirements_dev.in + # wagtail-factories +faker==19.3.1 + # via factory-boy flake8==3.9.2 + # via -r requirements_dev.in +google-api-core==2.11.1 + # via opencensus +google-auth==2.22.0 + # via google-api-core +googleapis-common-protos==1.60.0 + # via google-api-core +gunicorn==20.1.0 + # via -r requirements.in +h11==0.14.0 + # via wsproto +html5lib==1.1 + # via wagtail +idna==2.10 + # via + # requests + # trio +iniconfig==2.0.0 + # via pytest +isort==4.3.21 + # via pylint +l18n==2021.3 + # via wagtail +lazy-object-proxy==1.4.3 + # via astroid +markuppy==1.14 + # via tablib +mccabe==0.6.1 + # via + # flake8 + # pylint +odfpy==1.4.1 + # via tablib +opencensus==0.11.2 + # via + # opencensus-ext-azure + # opencensus-ext-django +opencensus-context==0.1.3 + # via opencensus +opencensus-ext-azure==1.0.7 + # via -r requirements.in +opencensus-ext-django==0.7.4 + # via -r requirements.in +openpyxl==3.1.2 + # via + # tablib + # wagtail +outcome==1.2.0 + # via trio +packaging==23.1 + # via + # bleach + # pytest +pillow==9.5.0 + # via wagtail +pluggy==1.3.0 + # via pytest +protobuf==4.24.2 + # via + # google-api-core + # googleapis-common-protos +psutil==5.9.5 + # via opencensus-ext-azure +psycopg2-binary==2.8.6 + # via -r requirements.in +py==1.11.0 + # via pytest +pyasn1==0.5.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.3.0 + # via google-auth +pycodestyle==2.7.0 + # via flake8 +pycparser==2.21 + # via cffi pydocstyle==6.0.0 + # via -r requirements_dev.in +pyflakes==2.3.1 + # via flake8 +pygithub==1.55 + # via -r requirements.in +pyjwt==2.8.0 + # via pygithub pylint==2.4.4 + # via + # -r requirements_dev.in + # pylint-django + # pylint-plugin-utils pylint-django==2.0.13 -wagtail-factories==3.1.0 -factory-boy==3.2.1 + # via -r requirements_dev.in +pylint-plugin-utils==0.8.2 + # via pylint-django +pynacl==1.5.0 + # via pygithub +pysocks==1.7.1 + # via urllib3 +pytest==7.1.1 + # via + # -r requirements_dev.in + # pytest-django + # pytest-splinter + # pytest-timeout +pytest-django==4.5.2 + # via -r requirements_dev.in +pytest-splinter==3.3.1 + # via -r requirements_dev.in +pytest-timeout==2.1.0 + # via -r requirements_dev.in +python-dateutil==2.8.2 + # via + # azure-storage-common + # faker +pytz==2023.3.post1 + # via + # babel + # django + # django-modelcluster + # djangorestframework + # l18n +pyyaml==6.0.1 + # via tablib +rcssmin==1.1.0 + # via django-compressor +requests==2.25.1 + # via + # -r requirements.in + # azure-storage-common + # django-recaptcha3 + # google-api-core + # opencensus-ext-azure + # pygithub + # responses + # wagtail +responses==0.13.3 + # via -r requirements_dev.in +rjsmin==1.2.0 + # via django-compressor +rsa==4.9 + # via google-auth +selenium==4.12.0 + # via + # pytest-splinter + # splinter +six==1.16.0 + # via + # astroid + # bleach + # django-modeltranslation + # django-prettyjson + # google-auth + # html5lib + # l18n + # python-dateutil + # responses + # splinter +sniffio==1.3.0 + # via trio +snowballstemmer==2.2.0 + # via pydocstyle +sortedcontainers==2.4.0 + # via trio +soupsieve==2.5 + # via beautifulsoup4 +splinter==0.14.0 + # via + # -r requirements_dev.in + # pytest-splinter sqlparse==0.4.2 -django-debug-toolbar==3.5.0 + # via + # -r requirements_dev.in + # django + # django-debug-toolbar +standardjson==0.3.1 + # via django-prettyjson +tablib[html,ods,xls,xlsx,yaml]==3.5.0 + # via django-import-export +telepath==0.3.1 + # via wagtail +tomli==2.0.1 + # via pytest +trio==0.22.2 + # via + # selenium + # trio-websocket +trio-websocket==0.10.4 + # via selenium +typing-extensions==4.7.1 + # via + # asgiref + # faker +urllib3[socks]==1.26.16 + # via + # elasticsearch + # google-auth + # pytest-splinter + # requests + # responses + # selenium +wagtail==4.1.6 + # via + # -r requirements.in + # wagtail-factories + # wagtail-modeltranslation +wagtail-factories==3.1.0 + # via -r requirements_dev.in +wagtail-modeltranslation==0.13.0 + # via -r requirements.in +wand==0.6.6 + # via -r requirements.in +webencodings==0.5.1 + # via + # bleach + # html5lib +whitenoise==5.2.0 + # via -r requirements.in +willow==1.4.1 + # via wagtail +wrapt==1.11.2 + # via + # -r requirements.in + # astroid + # deprecated +wsproto==1.2.0 + # via trio-websocket +xlrd==2.0.1 + # via tablib +xlwt==1.3.0 + # via tablib + +# The following packages are considered to be unsafe in a requirements file: +# setuptools