diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 43b75bb62..e0d9c25ed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,16 +6,16 @@ name: Unit tests # events but only for the main branch on: push: - branches: [ main, 'batou-*' ] + branches: [main, "batou-*"] pull_request: - branches: [ main, 'batou-*' ] + branches: [main, "batou-*"] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: strategy: matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] # The type of runner that the job will run on runs-on: ubuntu-22.04 @@ -39,5 +39,8 @@ jobs: - name: Show environment run: set - - name: Test + - name: Test without pyrage run: bin/tox -e py -- -vv + + - name: Test with pyrage + run: bin/tox -e pyrage -- -vv diff --git a/CHANGES.d/20250217_112817_ph_speedup_batou_age_interaction.md b/CHANGES.d/20250217_112817_ph_speedup_batou_age_interaction.md new file mode 100644 index 000000000..96080ed6b --- /dev/null +++ b/CHANGES.d/20250217_112817_ph_speedup_batou_age_interaction.md @@ -0,0 +1,2 @@ +- rewrite secret handling internals to improve de- and encrypting perfomance +- breaking: drop support for python 3.7 diff --git a/doc/source/components/python.txt b/doc/source/components/python.txt index 917ef6cf5..08bae6705 100644 --- a/doc/source/components/python.txt +++ b/doc/source/components/python.txt @@ -103,7 +103,7 @@ files makes it necessary. A typical usage example: .. code-block:: python - self += Buildout(python='3.7', version='2.2', setuptools='1.0', + self += Buildout(python='3.8', version='2.2', setuptools='1.0', pip='21.1', additional_config=[Directory('profiles', source='profiles')]) diff --git a/examples/api/requirements.lock b/examples/api/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/api/requirements.lock +++ b/examples/api/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/api/requirements.txt b/examples/api/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/api/requirements.txt +++ b/examples/api/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/django/requirements.lock b/examples/django/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/django/requirements.lock +++ b/examples/django/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/django/requirements.txt b/examples/django/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/django/requirements.txt +++ b/examples/django/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/durations/requirements.lock b/examples/durations/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/durations/requirements.lock +++ b/examples/durations/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/durations/requirements.txt b/examples/durations/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/durations/requirements.txt +++ b/examples/durations/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/errors/requirements.lock b/examples/errors/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/errors/requirements.lock +++ b/examples/errors/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/errors/requirements.txt b/examples/errors/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/errors/requirements.txt +++ b/examples/errors/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/errors2/requirements.lock b/examples/errors2/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/errors2/requirements.lock +++ b/examples/errors2/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/errors2/requirements.txt b/examples/errors2/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/errors2/requirements.txt +++ b/examples/errors2/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/errorsnohost/requirements.lock b/examples/errorsnohost/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/errorsnohost/requirements.lock +++ b/examples/errorsnohost/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/errorsnohost/requirements.txt b/examples/errorsnohost/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/errorsnohost/requirements.txt +++ b/examples/errorsnohost/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/ignores/requirements.lock b/examples/ignores/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/ignores/requirements.lock +++ b/examples/ignores/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/ignores/requirements.txt b/examples/ignores/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/ignores/requirements.txt +++ b/examples/ignores/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/largetempl/requirements.lock b/examples/largetempl/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/largetempl/requirements.lock +++ b/examples/largetempl/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/largetempl/requirements.txt b/examples/largetempl/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/largetempl/requirements.txt +++ b/examples/largetempl/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/package/requirements.lock b/examples/package/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/package/requirements.lock +++ b/examples/package/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/package/requirements.txt b/examples/package/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/package/requirements.txt +++ b/examples/package/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/sensitive-values/requirements.lock b/examples/sensitive-values/requirements.lock index 120d488f7..c64f5cf6c 100644 --- a/examples/sensitive-values/requirements.lock +++ b/examples/sensitive-values/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.8.30 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.8 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/sensitive-values/requirements.txt b/examples/sensitive-values/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/sensitive-values/requirements.txt +++ b/examples/sensitive-values/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/sync_async/requirements.lock b/examples/sync_async/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/sync_async/requirements.lock +++ b/examples/sync_async/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/sync_async/requirements.txt b/examples/sync_async/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/sync_async/requirements.txt +++ b/examples/sync_async/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/tutorial-buildout/requirements.lock b/examples/tutorial-buildout/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/tutorial-buildout/requirements.lock +++ b/examples/tutorial-buildout/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/tutorial-buildout/requirements.txt b/examples/tutorial-buildout/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/tutorial-buildout/requirements.txt +++ b/examples/tutorial-buildout/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/tutorial-component/requirements.lock b/examples/tutorial-component/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/tutorial-component/requirements.lock +++ b/examples/tutorial-component/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/tutorial-component/requirements.txt b/examples/tutorial-component/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/tutorial-component/requirements.txt +++ b/examples/tutorial-component/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/tutorial-helloworld/requirements.lock b/examples/tutorial-helloworld/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/tutorial-helloworld/requirements.lock +++ b/examples/tutorial-helloworld/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/tutorial-helloworld/requirements.txt b/examples/tutorial-helloworld/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/tutorial-helloworld/requirements.txt +++ b/examples/tutorial-helloworld/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/tutorial-parallelize/components/supervisor/component.py b/examples/tutorial-parallelize/components/supervisor/component.py index 6bf2b4693..cc29c39c1 100644 --- a/examples/tutorial-parallelize/components/supervisor/component.py +++ b/examples/tutorial-parallelize/components/supervisor/component.py @@ -7,7 +7,7 @@ class Supervisor(Component): def configure(self): self.programs = self.require("programs", self.host) self += Buildout( - "supervisor", version="2.13.2", setuptools="41.4.0", python="3.7" + "supervisor", version="2.13.2", setuptools="41.4.0", python="3.8" ) self += Service("bin/supervisord", pidfile="var/supervisord.pid") diff --git a/examples/tutorial-parallelize/requirements.lock b/examples/tutorial-parallelize/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/tutorial-parallelize/requirements.lock +++ b/examples/tutorial-parallelize/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/tutorial-parallelize/requirements.txt b/examples/tutorial-parallelize/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/tutorial-parallelize/requirements.txt +++ b/examples/tutorial-parallelize/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/tutorial-provision-container/requirements.lock b/examples/tutorial-provision-container/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/tutorial-provision-container/requirements.lock +++ b/examples/tutorial-provision-container/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/tutorial-provision-container/requirements.txt b/examples/tutorial-provision-container/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/tutorial-provision-container/requirements.txt +++ b/examples/tutorial-provision-container/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/tutorial-secrets/requirements.lock b/examples/tutorial-secrets/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/tutorial-secrets/requirements.lock +++ b/examples/tutorial-secrets/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/tutorial-secrets/requirements.txt b/examples/tutorial-secrets/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/tutorial-secrets/requirements.txt +++ b/examples/tutorial-secrets/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/vagrant-multi/requirements.lock b/examples/vagrant-multi/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/vagrant-multi/requirements.lock +++ b/examples/vagrant-multi/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/vagrant-multi/requirements.txt b/examples/vagrant-multi/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/vagrant-multi/requirements.txt +++ b/examples/vagrant-multi/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/vagrant/requirements.lock b/examples/vagrant/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/vagrant/requirements.lock +++ b/examples/vagrant/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/vagrant/requirements.txt b/examples/vagrant/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/vagrant/requirements.txt +++ b/examples/vagrant/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/venvs/environments/requirements.txt b/examples/venvs/environments/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/venvs/environments/requirements.txt +++ b/examples/venvs/environments/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/examples/venvs/requirements.lock b/examples/venvs/requirements.lock index 97853ff9d..c64f5cf6c 100644 --- a/examples/venvs/requirements.lock +++ b/examples/venvs/requirements.lock @@ -1,19 +1,23 @@ -# appenv-requirements-hash: 92e251bc834f921c996b9af42180afdf00414e12ee57291b199a6127ff5e6897 +# appenv-requirements-hash: 74d5bb54e4d2a63186008bba11682322569972db5811e81bf41bc371ed891342 -e ../../ ConfigUpdater==3.2 -Jinja2==3.1.4 +Jinja2==3.1.5 MarkupSafe==2.1.5 -PyYAML==6.0.1 -certifi==2024.7.4 -charset-normalizer==3.3.2 -execnet==2.0.2 -idna==3.7 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +PyYAML==6.0.2 +bcrypt==4.2.1 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.1 +execnet==2.1.1 +idna==3.10 +importlib_metadata==8.5.0 +importlib_resources==6.4.5 py==1.11.0 +pycparser==2.22 +pyrage==1.2.1 remote-pdb==2.1.0 -requests==2.31.0 -setuptools==68.0.0 -typing_extensions==4.7.1 -urllib3==2.0.7 -zipp==3.15.0 +requests==2.32.3 +setuptools==75.3.0 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/examples/venvs/requirements.txt b/examples/venvs/requirements.txt index 4b8c40926..e2b611256 100644 --- a/examples/venvs/requirements.txt +++ b/examples/venvs/requirements.txt @@ -1,2 +1,2 @@ -# appenv-python-preference: 3.7,3.8,3.9,3.10,3.11,3.12 +# appenv-python-preference: 3.8,3.9,3.10,3.11,3.12 -e ../../ diff --git a/flake.lock b/flake.lock index 2da23239d..ba8e76bca 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1714641030, - "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=", + "lastModified": 1738453229, + "narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e", + "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nix-filter": { "locked": { - "lastModified": 1710156097, - "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=", + "lastModified": 1731533336, + "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=", "owner": "numtide", "repo": "nix-filter", - "rev": "3342559a24e85fc164b295c3444e8a139924675b", + "rev": "f7653272fd234696ae94229839a99b73c9ab7de0", "type": "github" }, "original": { @@ -35,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1715534503, - "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=", + "lastModified": 1739214665, + "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2057814051972fa1453ddfb0d98badbea9b83c06", + "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a", "type": "github" }, "original": { @@ -51,28 +51,28 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1714640452, - "narHash": "sha256-QBx10+k6JWz6u7VsohfSw8g8hjdBZEf8CFzXH1/1Z94=", + "lastModified": 1738452942, + "narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + "url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz" }, "original": { "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + "url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz" } }, "nixpkgs_2": { "locked": { - "lastModified": 1708475490, - "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", + "lastModified": 1735554305, + "narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0e74ca98a74bc7270d28838369593635a5db3260", + "rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-unstable", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -90,11 +90,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1714058656, - "narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=", + "lastModified": 1738953846, + "narHash": "sha256-yrK3Hjcr8F7qS/j2F+r7C7o010eVWWlm4T1PrbKBOxQ=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f", + "rev": "4f09b473c936d41582dd744e19f34ec27592c5fd", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0e0cf4bc8..a98fa5782 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,7 @@ systems = [ "x86_64-linux" "aarch64-darwin" + "aarch64-linux" ]; perSystem = { @@ -56,7 +57,12 @@ devShells.default = pkgs.mkShell { packages = [ - (pkgs.python3.withPackages (ps: [batou ps.tox])) + pkgs.uv + + # for the tests + pkgs.gnupg + pkgs.mercurial + pkgs.subversion ]; shellHook = '' diff --git a/nix/batou.nix b/nix/batou.nix index 09fb7568a..fc9fd77e0 100644 --- a/nix/batou.nix +++ b/nix/batou.nix @@ -1,7 +1,5 @@ { buildPythonPackage, - fetchPypi, - markupsafe, requests, pyyaml, execnet, @@ -15,7 +13,42 @@ setuptools, jinja2, src, -}: + bcrypt, + cryptography, + rustPlatform, + fetchFromGitHub, + cargo, + rustc, + setuptools-rust, +}: let + pyrage = buildPythonPackage rec { + pname = "pyrage"; + version = "1.2.3"; + + src = fetchFromGitHub { + owner = "woodruffw"; + repo = pname; + rev = "v${version}"; + hash = "sha256-asTdmH+W+tmoUMIwmmGC13j+HdTeZL8Th27pvsy7TT0="; + }; + + cargoDeps = rustPlatform.fetchCargoTarball { + inherit src; + name = "${pname}-${version}"; + hash = "sha256-F8NG8H3Nt5FinuTWdBR8/aSj+Pvin/J/AvgQHL3qknE="; + }; + + format = "pyproject"; + + nativeBuildInputs = [ + cargo + rustPlatform.cargoSetupHook + rustPlatform.maturinBuildHook + rustc + setuptools-rust + ]; + }; +in buildPythonPackage { propagatedBuildInputs = [ requests @@ -30,6 +63,9 @@ pytest setuptools jinja2 + bcrypt + cryptography + pyrage ]; pname = "batou"; diff --git a/pytest.ini b/pytest.ini index 3118695ca..c6a1dd06f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = -W error --timeout=45 --tb=native --cov=src --cov-report=html --junitxml=report.xml --instafail +addopts = -W error --timeout=100 --tb=native --cov=src --cov-report=html --junitxml=report.xml --instafail markers = slow: This is a slow test. testpaths = src junit_family=xunit2 diff --git a/setup.py b/setup.py index 950883059..956871bc4 100644 --- a/setup.py +++ b/setup.py @@ -23,15 +23,18 @@ "py>=1.11.0", "pyyaml", "remote-pdb", + "cryptography", + "bcrypt", ], extras_require={ + "pyrage": ["pyrage"], "test": [ "mock", "pytest", "pytest-coverage", "pytest-instafail", "pytest-timeout", - ] + ], }, entry_points=""" [console_scripts] diff --git a/src/batou/conftest.py b/src/batou/conftest.py index d8cd52423..bc0557f34 100644 --- a/src/batou/conftest.py +++ b/src/batou/conftest.py @@ -9,10 +9,18 @@ @pytest.fixture(autouse=True) def ensure_gpg_homedir(monkeypatch): - home = os.path.join( + old_home = os.path.join( os.path.dirname(__file__), "secrets", "tests", "fixture", "gnupg" ) - monkeypatch.setitem(os.environ, "GNUPGHOME", home) + + with tempfile.TemporaryDirectory() as home: + os.system(f"cp -r {old_home}/* {home}") + os.system(f"gpg-agent --homedir='{home}' --daemon") + monkeypatch.setitem(os.environ, "GNUPGHOME", home) + + yield + + os.system(f"gpgconf --homedir='{home}' --kill gpg-agent") @pytest.fixture(autouse=True) @@ -28,6 +36,12 @@ def ensure_age_identity(monkeypatch): monkeypatch.setitem(os.environ, "BATOU_AGE_IDENTITIES", key) +@pytest.fixture(autouse=True) +def ensure_git_isolated(monkeypatch): + monkeypatch.setitem(os.environ, "GIT_CONFIG_GLOBAL", "") + monkeypatch.setitem(os.environ, "GIT_CONFIG_SYSTEM", "") + + @pytest.fixture(autouse=True) def reset_address_defaults(): v4, v6 = batou.utils.Address.require_v4, batou.utils.Address.require_v6 diff --git a/src/batou/deploy.py b/src/batou/deploy.py index f608d60a8..603f6f4db 100644 --- a/src/batou/deploy.py +++ b/src/batou/deploy.py @@ -322,18 +322,8 @@ def deploy(self): self.loop.set_default_executor(self.taskpool) self._launch_components(reference_node.root_dependencies()) - # asyncio.Task.all_tasks was removed in Python 3.9 - # but the replacement asyncio.all_tasks is only available - # for Python 3.7 and upwards - # confer https://docs.python.org/3/whatsnew/3.7.html - # and https://docs.python.org/3.9/whatsnew/3.9.html - if sys.version_info < (3, 7): - all_tasks = asyncio.Task.all_tasks - else: - all_tasks = asyncio.all_tasks - def get_pending(): - return {t for t in all_tasks(self.loop) if not t.done()} + return {t for t in asyncio.all_tasks(self.loop) if not t.done()} pending = get_pending() while pending: diff --git a/src/batou/secrets/encryption/__init__.py b/src/batou/secrets/encryption/__init__.py new file mode 100644 index 000000000..b2c5191e3 --- /dev/null +++ b/src/batou/secrets/encryption/__init__.py @@ -0,0 +1,36 @@ +import importlib +import sys + +USE_LEGACY = None +_encrypt_module = None + + +def _pick_module(): + global _encrypt_module + + if _encrypt_module: + return _encrypt_module + + if USE_LEGACY: + module_hint = ".age_shellout" + else: + try: + import pyrage + + module_hint = ".pyrage_encryption" + except ImportError: + module_hint = ".age_shellout" + + _encrypt_module = importlib.import_module(module_hint, __name__) + return _encrypt_module + + +def __getattr__(name): + module = _pick_module() + value = getattr(module, name) + setattr(sys.modules[__name__], name, value) + return value + + +def __dir__(): + return dir(_pick_module()) diff --git a/src/batou/secrets/encryption.py b/src/batou/secrets/encryption/age_shellout.py similarity index 100% rename from src/batou/secrets/encryption.py rename to src/batou/secrets/encryption/age_shellout.py diff --git a/src/batou/secrets/encryption/pyrage_encryption.py b/src/batou/secrets/encryption/pyrage_encryption.py new file mode 100644 index 000000000..fb671719c --- /dev/null +++ b/src/batou/secrets/encryption/pyrage_encryption.py @@ -0,0 +1,539 @@ +import base64 +import errno +import fcntl +import os +import pathlib +import pty +import subprocess +import sys +import tempfile +from typing import Callable, Dict, List, Optional, Type + +import pyrage +from configupdater import ConfigUpdater +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from batou import AgeCallError, FileLockedError, GPGCallError +from batou._output import output + +debug = False + + +class EncryptedFile: + file_ending: Optional[str] = None + + def __init__(self, path: "pathlib.Path", writeable: bool = False): + self.path = path + self.writeable = writeable + self.fd = None + self.is_new: Optional[bool] = None + self._decrypted: Optional[bytes] = None + + @property + def decrypted(self) -> bytes: + if self.is_new: + self._decrypted = b"" + if self.path.stat().st_size == 0: + self._decrypted = b"" + if self._decrypted is None: + self._decrypted = self.decrypt() + if self._decrypted is None: + raise ValueError( + f"No decrypted data available for file `{self.path}`" + ) + return self._decrypted + + def decrypt(self) -> bytes: + raise NotImplementedError("decrypt() not implemented") + + @property + def cleartext(self) -> str: + return self.decrypted.decode("utf-8") + + @property + def locked(self) -> bool: + return self.fd is not None + + def write( + self, content: bytes, recipients: List[str], reencrypt: bool = False + ): + if debug: + print( + f"EncryptedFile({self.path}).write({content!r}, {recipients}, {reencrypt})", + file=sys.stderr, + ) + self._decrypted = None + self._write(content, recipients, reencrypt) + self._decrypted = content + + def _write( + self, content: bytes, recipients: List[str], reencrypt: bool = False + ): + raise NotImplementedError("_write() not implemented") + + def __enter__(self): + self._lock() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._unlock() + + def _lock(self): + if self.locked: + raise FileLockedError.from_context(self.path) + if not self.path.exists(): + self.is_new = True + self.path.touch() + self.fd = open(self.path, "r+" if self.writeable else "r") + if debug: + print(f"Locking `{self.path}`", file=sys.stderr) + try: + fcntl.lockf( + self.fd, + fcntl.LOCK_NB # non-blocking + | ( + fcntl.LOCK_EX # exclusive + if self.writeable + else fcntl.LOCK_SH # shared + ), + ) + except BlockingIOError: + raise FileLockedError.from_context(self.path) + + def _unlock(self): + if debug: + print(f"Unlocking `{self.path}`", file=sys.stderr) + if self.fd is not None: + self.fd.close() + self.fd = None + if self.is_new: + self.path.unlink() + + @property + def exists(self): + return self.path.exists() + + def delete(self): + self.path.unlink() + + +class NoBackingEncryptedFile(EncryptedFile): + def __init__(self): + super().__init__(pathlib.Path("/dev/null")) + self.is_new = True + + def decrypt(self): + return b"" + + @property + def locked(self): + return True + + def _lock(self): + pass + + def _unlock(self): + pass + + +class GPGEncryptedFile(EncryptedFile): + file_ending = ".gpg" + + def decrypt(self): + if not self.locked: + raise RuntimeError("File not locked") + args = [self.gpg(), "--decrypt", str(self.path)] + + if debug: + print(f"Running `{args}`", file=sys.stderr) + + try: + p = subprocess.run( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + except subprocess.CalledProcessError as e: + raise GPGCallError.from_context( + e.cmd, e.returncode, e.stderr + ) from e + return p.stdout + + def _write( + self, content: bytes, recipients: List[str], reencrypt: bool = False + ): + if not self.locked: + raise RuntimeError("File not locked") + if not self.writeable: + raise RuntimeError("File not writeable") + self.path.rename(str(self.path) + ".old") + old_path = pathlib.Path(str(self.path) + ".old") + # starting with python 3.8, pathlib.Path's rename() method + # returns the new path, so we need to store the old path + args = [self.gpg(), "--encrypt"] + for recipient in recipients: + args.extend(["-r", recipient]) + args.extend(["-o", str(self.path)]) + + if debug: + print(f"Running `{args}`", file=sys.stderr) + + try: + subprocess.run( + args, + input=content, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + except subprocess.CalledProcessError as e: + old_path.rename(self.path) + raise GPGCallError.from_context(e.cmd, e.returncode, e.stderr) + else: + old_path.unlink() + self.is_new = False + + _gpg = None + GPG_BINARY_CANDIDATES = ["gpg", "gpg2"] + + @classmethod + def gpg(cls): + if cls._gpg is not None: + return cls._gpg + with tempfile.TemporaryFile() as null: + for gpg in cls.GPG_BINARY_CANDIDATES: + args = [gpg, "--version"] + + if debug: + print(f"Running `{args}`", file=sys.stderr) + + try: + subprocess.check_call(args, stdout=null, stderr=null) + except (subprocess.CalledProcessError, OSError): + pass + else: + cls._gpg = gpg + return cls._gpg + raise RuntimeError( + "Could not find gpg binary." + " Is GPG installed? I tried looking for: {}".format( + ", ".join("`{}`".format(x) for x in cls.GPG_BINARY_CANDIDATES) + ) + ) + + +def expect(fd, expected): + """ + Expect a certain string on the given file descriptor. + Returns a tuple of (bool, bytes) where the bool indicates whether the + expected string was found and the actual output bytes. + """ + try: + actual = os.read(fd, len(expected)) + except OSError as e: + if e.errno == errno.EIO: + return (False, b"") + raise + return (actual == expected, actual) + + +identities = None + + +def get_identities(): + global identities + if identities is None: + identities = os.environ.get("BATOU_AGE_IDENTITIES") + identities = ( + [x.strip() for x in identities.split(",")] if identities else [] + ) + if not identities: + # ssh uses ~/.ssh/id_rsa, + # ~/.ssh/id_ecdsa, ~/.ssh/id_ecdsa_sk, ~/.ssh/id_ed25519, + # ~/.ssh/id_ed25519_sk and ~/.ssh/id_dsa + # in that order. + identities = [ + "~/.ssh/id_rsa", + "~/.ssh/id_ecdsa", + "~/.ssh/id_ecdsa_sk", + "~/.ssh/id_ed25519", + "~/.ssh/id_ed25519_sk", + "~/.ssh/id_dsa", + ] + # filter on existing files + paths = [ + os.path.expanduser(x) + for x in identities + if os.path.exists(os.path.expanduser(x)) + ] + + if debug: + print(f"Found identities: {paths}", file=sys.stderr) + + def load(id_path): + with open(id_path, "rb") as f: + key_content = f.read() + try: + priv_key = serialization.load_ssh_private_key(key_content, None) + except (ValueError, TypeError): + passphrase = get_passphrase(id_path).encode("utf-8") + priv_key = serialization.load_ssh_private_key( + key_content, + passphrase, + ) + + pkey = priv_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.OpenSSH, + serialization.NoEncryption(), + ) + return pyrage.ssh.Identity.from_buffer(pkey) + + identities = [] + for p in paths: + try: + x = load(p) + identities.append(x) + except Exception as e: + print(e, file=sys.stderr) + continue + + return identities + + +known_passphrases: Dict[str, str] = {} + + +def get_passphrase(identity: str) -> str: + """Prompt the user for a passphrase if necessary.""" + if identity in known_passphrases: + return known_passphrases[identity] + + op = os.environ.get("BATOU_AGE_IDENTITY_PASSPHRASE") + + if op and not op.startswith("op://"): + passphrase = op + elif op: + op_process = subprocess.run( + ["op", "read", op], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + passphrase = op_process.stdout.decode("utf-8").strip() + else: + import getpass + + passphrase = getpass.getpass( + "Enter passphrase for {}: ".format(identity) + ) + + known_passphrases[identity] = passphrase + return passphrase + + +class AGEEncryptedFile(EncryptedFile): + file_ending = ".age" + + def decrypt(self): + if not self.locked: + raise ValueError("File is not locked") + + for identity in get_identities(): + try: + with open(self.path, "rb") as f: + encrypted_content = f.read() + if encrypted_content: + result = pyrage.decrypt(encrypted_content, [identity]) + return result + except Exception as e: + print(f"error: {e}") + continue + + def _write( + self, content: bytes, recipients: List[str], reencrypt: bool = False + ): + if not self.locked: + raise ValueError("File is not locked") + if not self.writeable: + raise ValueError("File is not writeable") + + try: + recipients = [ + pyrage.ssh.Recipient.from_str(rec) for rec in recipients + ] + b = pyrage.encrypt(content, recipients) + + with open(self.path, "wb") as f: + f.write(b) + self.is_new = False + except pyrage.RecipientError: + self.write_legacy(content, recipients, reencrypt) + + def write_legacy( + self, content: bytes, recipients: List[str], reencryt: bool = False + ): + """ + Fallback to writing secrets with the age binary via subprocesses + """ + args = ["age", "-e"] + for recipient in recipients: + args.extend(["-r", recipient]) + args.extend(["-o", str(self.path)]) + + if debug: + print(f"Running `{args}`", file=sys.stderr) + + try: + subprocess.run( + args, + input=content, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + except subprocess.CalledProcessError as e: + raise AgeCallError.from_context(e.cmd, e.returncode, e.stderr) + except OSError: + raise RuntimeError( + "Could not find age binary. Is age installed? I tried looking for: `age`" + ) + self.is_new = False + + +class DiffableAGEEncryptedFile(EncryptedFile): + file_ending = ".age-diffable" + _decrypted_content: ConfigUpdater + _encrypted_content: ConfigUpdater + + def __init__(self, path: "pathlib.Path", writeable: bool = False): + super().__init__(path, writeable) + + def decrypt_age_string(self, content: str, ident) -> str: + b = base64.b64decode(content) + return pyrage.decrypt(b, [ident]).decode("utf-8") + + def encrypt_age_string( + self, content: str, recipients: List[pyrage.ssh.Recipient] + ) -> str: + b = pyrage.encrypt(content.encode("utf-8"), recipients) + return base64.b64encode(b).decode("utf-8") + + def encrypt_age_string_legacy( + self, content: str, recipients: List[str] + ) -> str: + # tmpfile -> AGEEncryptedFile -> write plaintext -> read ciphertext -> base64 + with tempfile.NamedTemporaryFile() as temp_file: + with AGEEncryptedFile(pathlib.Path(temp_file.name), True) as ef: + ef.write_legacy(content.encode("utf-8"), recipients) + with open(temp_file.name, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + + def decrypt(self): + # read the entire file, parse as ConfigUpdater + + for ident in get_identities(): + try: + if not self.locked: + raise ValueError("File is not locked") + + config_encrypted = ConfigUpdater().read(self.path) + config = ConfigUpdater().read(self.path) + + # for each section + for section in config.sections(): + # if section is called batou, skip + if section == "batou": + continue + # for each option + for option in config[section]: + # decrypt the value + decrypted = self.decrypt_age_string( + config[section][option].value, ident + ) + if "\n" in decrypted: + # multiline: accounts for indents + config[section][option].set_values( + decrypted.split("\n"), + prepend_newline=False, + ) + else: + config[section][option].value = decrypted + + # cache the decrypted content + self._decrypted_content = config + self._encrypted_content = config_encrypted + + # return the decrypted content as bytes + return str(config).encode("utf-8") + + except Exception as e: + print(f"error: {e}") + raise e + + def _write( + self, content: bytes, recipients: List[str], reencrypt: bool = False + ): + # parse the content as ConfigUpdater + config = ConfigUpdater() + config.read_string(content.decode("utf-8")) + + try: + recipients = [ + pyrage.ssh.Recipient.from_str(rec) for rec in recipients + ] + encrypt_method = self.encrypt_age_string + except pyrage.RecipientError: + encrypt_method = self.encrypt_age_string_legacy + + # for each section + for section in config.sections(): + # if section is called batou, skip + if section == "batou": + continue + # for each option + for option in config[section]: + new_value = config[section][option].value + assert new_value is not None + + try: + old_value = self._decrypted_content[section][option].value + except Exception: + old_value = None + + value_has_changed = new_value != old_value + + if reencrypt or value_has_changed: + new_encrypted_value = encrypt_method(new_value, recipients) + else: + new_encrypted_value = ( + self._encrypted_content[section][option].value or "" + ) + + config[section][option].value = new_encrypted_value + + # write the config to the file + with open(self.path, "w") as f: + f.write(str(config)) + + self.is_new = False + + +all_encrypted_file_types: List[Type[EncryptedFile]] = [ + NoBackingEncryptedFile, + GPGEncryptedFile, + AGEEncryptedFile, + DiffableAGEEncryptedFile, +] + + +def get_encrypted_file( + path: "pathlib.Path", writeable: bool = False +) -> EncryptedFile: + """Return the appropriate EncryptedFile object for the given path.""" + for ef in all_encrypted_file_types: + if ef.file_ending and path.name.endswith(ef.file_ending): + return ef(path, writeable) + raise ValueError(f"Unknown encrypted file type for {path}") diff --git a/src/batou/secrets/tests/test_editor.py b/src/batou/secrets/tests/test_editor.py index fab1ae6d2..2c8273639 100644 --- a/src/batou/secrets/tests/test_editor.py +++ b/src/batou/secrets/tests/test_editor.py @@ -24,10 +24,6 @@ def test_edit_gpg(tmpdir): assert editor.cleartext == "asdf" -@pytest.mark.skipif( - sys.version_info < (3, 7), - reason="age is available in tests with python 3.7 only", -) def test_edit_age(tmpdir): editor = Editor( "true", @@ -155,10 +151,6 @@ def test_edit_file_has_secret_prefix_gpg(tmpdir, encrypted_file): assert editor.file.path.name == f"secret-{filename}.gpg" -@pytest.mark.skipif( - sys.version_info < (3, 7), - reason="age is available in tests with python 3.7 only", -) def test_edit_file_has_secret_prefix_age(tmpdir, encrypted_file): filename = "asdf123" editor = Editor( diff --git a/src/batou/secrets/tests/test_manage.py b/src/batou/secrets/tests/test_manage.py index 79299270d..387a12540 100644 --- a/src/batou/secrets/tests/test_manage.py +++ b/src/batou/secrets/tests/test_manage.py @@ -48,10 +48,6 @@ def test_manage__2(tmp_path, monkeypatch, capsys): assert "306151601E813A47" in out -@pytest.mark.skipif( - sys.version_info < (3, 7), - reason="age is available in tests with python 3.7 only", -) def test_manage__2_age(tmp_path, monkeypatch, capsys): """It allows to add/remove_users in an age encrypted environment.""" shutil.copytree("examples/tutorial-secrets", tmp_path / "tutorial-secrets") @@ -116,10 +112,6 @@ def test_manage__summary__3(capsys, monkeypatch): assert err == "" -@pytest.mark.skipif( - sys.version_info < (3, 7), - reason="age is available in tests with python 3.7 only", -) def test_manage__reencrypt__1(tmp_path, monkeypatch, capsys): """It re-encrypts all files with the current members.""" shutil.copytree("examples/tutorial-secrets", tmp_path / "tutorial-secrets") diff --git a/src/batou/secrets/tests/test_secrets.py b/src/batou/secrets/tests/test_secrets.py index a263708e1..48b503909 100644 --- a/src/batou/secrets/tests/test_secrets.py +++ b/src/batou/secrets/tests/test_secrets.py @@ -58,20 +58,6 @@ def test_error_message_no_gpg_found(encrypted_file): GPGEncryptedFile.GPG_BINARY_CANDIDATES = OLD_GPG_BINARY_CANDIDATES -def test_error_message_no_age_found(encrypted_file): - c = AGEEncryptedFile(encrypted_file) - OLD_AGE_BINARY_CANDIDATES = AGEEncryptedFile.AGE_BINARY_CANDIDATES - AGEEncryptedFile.AGE_BINARY_CANDIDATES = ["foobarasdf-54875982"] - AGEEncryptedFile._age = None - with pytest.raises(RuntimeError) as e: - c.age() - assert e.value.args[0] == ( - "Could not find age binary. Is age installed? I tried looking for: " - "`foobarasdf-54875982`" - ) - AGEEncryptedFile.AGE_BINARY_CANDIDATES = OLD_AGE_BINARY_CANDIDATES - - def test_decrypt(encrypted_file): with GPGEncryptedFile(encrypted_file) as secret: with open(cleartext_file) as cleartext: diff --git a/src/batou/tests/test_deploy.py b/src/batou/tests/test_deploy.py index 1a270cbc6..6a1c18858 100644 --- a/src/batou/tests/test_deploy.py +++ b/src/batou/tests/test_deploy.py @@ -31,7 +31,7 @@ def test_main_with_errors(capsys): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `errors`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... @@ -64,7 +64,7 @@ def test_main_with_errors(capsys): ERROR: Secrets section for unknown component found Component: another-nonexisting-component-section -======================= DEPLOYMENT FAILED (during load) ======================== +... DEPLOYMENT FAILED (during load) ... """ ) # noqa: E501 line too long @@ -94,13 +94,13 @@ def test_main_fails_if_no_host_in_environment(capsys): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `errorsnohost`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... ERROR: No host found in environment. -====================== DEPLOYMENT FAILED (during connect) ====================== +... DEPLOYMENT FAILED (during connect) ... """ ) # noqa: E501 line too long diff --git a/src/batou/tests/test_encryption_switch.py b/src/batou/tests/test_encryption_switch.py new file mode 100644 index 000000000..27f2884ac --- /dev/null +++ b/src/batou/tests/test_encryption_switch.py @@ -0,0 +1,24 @@ +import importlib +import sys + +import pytest + + +@pytest.mark.skipif( + not importlib.util.find_spec("pyrage"), reason="requires pyrage" +) +def test_import_pyrage_encryption(): + from ..secrets.encryption import EncryptedFile + + assert ( + EncryptedFile.__module__ == "batou.secrets.encryption.pyrage_encryption" + ) + + +def test_import_legacy_encryption(monkeypatch): + monkeypatch.setitem(sys.modules, "pyrage", None) + + sys.modules.pop("batou.secrets.encryption", None) + from ..secrets.encryption import EncryptedFile + + assert EncryptedFile.__module__ == "batou.secrets.encryption.age_shellout" diff --git a/src/batou/tests/test_endtoend.py b/src/batou/tests/test_endtoend.py index b56e67ce7..136126144 100644 --- a/src/batou/tests/test_endtoend.py +++ b/src/batou/tests/test_endtoend.py @@ -23,7 +23,7 @@ def test_example_errors_early(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `errors`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... @@ -56,7 +56,7 @@ def test_example_errors_early(): ERROR: Secrets section for unknown component found Component: another-nonexisting-component-section -======================= DEPLOYMENT FAILED (during load) ======================== +... DEPLOYMENT FAILED (during load) ... """ ) # noqa: E501 line too long @@ -68,7 +68,7 @@ def test_example_errors_gpg_cannot_decrypt(monkeypatch): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `errors`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... @@ -100,7 +100,7 @@ def test_example_errors_gpg_cannot_decrypt(monkeypatch): ERROR: Override section for unknown component found Component: nonexisting-component-section -======================= DEPLOYMENT FAILED (during load) ======================== +... DEPLOYMENT FAILED (during load) ... """ ) # noqa: E501 line too long @@ -111,11 +111,11 @@ def test_example_errors_late(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `errors`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/1) @@ -172,8 +172,8 @@ def test_example_errors_late(): cycle1 ERROR: 11 remaining unconfigured component(s): component1, component2, component4, component5, component6, crontab, cycle1, cycle2, dnsproblem, dnsproblem2, filemode -======================= 12 ERRORS - CONFIGURATION FAILED ======================= -====================== DEPLOYMENT FAILED (during connect) ====================== +... 12 ERRORS - CONFIGURATION FAILED ... +... DEPLOYMENT FAILED (during connect) ... """ ) # noqa: E501 line too long @@ -184,12 +184,12 @@ def test_example_errors_missing_environment(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `production`... ERROR: Missing environment Environment: production -======================= DEPLOYMENT FAILED (during load) ======================== +... DEPLOYMENT FAILED (during load) ... """ ) # noqa: E501 line too long @@ -200,20 +200,20 @@ def test_example_ignores(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `ignores`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/2) ⏭️ otherhost: Connection ignored (2/2) -================================== Deploying =================================== +... Deploying ... ⚪ localhost: Scheduling component component1 ... ⏭️ localhost: Skipping component fail ... (Component ignored) ⏭️ otherhost: Skipping component fail2 ... (Host ignored) -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=...s -============================= DEPLOYMENT FINISHED ============================== +... DEPLOYMENT FINISHED ... """ ) # noqa: E501 line too long @@ -275,13 +275,13 @@ def test_diff_is_not_shown_for_keys_in_secrets(tmp_path, monkeypatch, capsys): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `tutorial`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/1) -================================== Deploying =================================== +... Deploying ... ⚪ localhost: Scheduling component hello ... 🚀 localhost: Hello > File('work/hello/hello') > Presence('hello') 🚀 localhost: Hello > File('work/hello/hello') > Content('hello') @@ -291,9 +291,9 @@ def test_diff_is_not_shown_for_keys_in_secrets(tmp_path, monkeypatch, capsys): 🚀 localhost: Hello > File('work/hello/other-secrets.yaml') > Content('other-secrets.yaml') Not showing diff as it contains sensitive data, see ...diff for the diff. -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=...s -============================= DEPLOYMENT FINISHED ============================== +... DEPLOYMENT FINISHED ... """ ) # noqa: E501 line too long @@ -317,13 +317,13 @@ def test_diff_for_keys_in_secrets_overridable(tmp_path, monkeypatch, capsys): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `local`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/1) -================================== Deploying =================================== +... Deploying ... ⚪ localhost: Scheduling component sensitivevalues ... 🚀 localhost: SensitiveValues > File('work/sensitivevalues/client_ed25519.key') > Presence('client_ed25519.key') 🚀 localhost: SensitiveValues > File('work/sensitivevalues/client_ed25519.key') > Content('client_ed25519.key') @@ -352,9 +352,9 @@ def test_diff_for_keys_in_secrets_overridable(tmp_path, monkeypatch, capsys): hostkey_sensitive_clear_ed25519.pub +++ hostkey_sensitive_clear_ed25519.pub @@ -0,0 +1 @@ hostkey_sensitive_clear_ed25519.pub +ssh-ed25519 ... batou-example-host -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=...s -============================= DEPLOYMENT FINISHED ============================== +... DEPLOYMENT FINISHED ... """ ) @@ -365,18 +365,18 @@ def test_durations_are_shown_for_components(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `default`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/1) -================================== Deploying =================================== +... Deploying ... ⚪ localhost: Scheduling component takeslongtime ... 💤 localhost: Takeslongtime [total=...s, verify=...s, update=∅, sub=∅] -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=...s -============================= DEPLOYMENT FINISHED ============================== +... DEPLOYMENT FINISHED ... """ ) @@ -387,15 +387,15 @@ def test_check_consistency_works(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `tutorial`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/1) -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=∅ -========================== CONSISTENCY CHECK FINISHED ========================== +... CONSISTENCY CHECK FINISHED ... """ ) @@ -406,13 +406,13 @@ def test_predicting_deployment_works(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `tutorial`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 localhost: Connecting via local (1/1) -======================== Predicting deployment actions ========================= +... Predicting deployment actions ... ⚪ localhost: Scheduling component hello ... 🚀 localhost: Hello > File('work/hello/hello') > Presence('hello') 🚀 localhost: Hello > File('work/hello/hello') > Content('hello') @@ -422,9 +422,9 @@ def test_predicting_deployment_works(): 🚀 localhost: Hello > File('work/hello/other-secrets.yaml') > Content('other-secrets.yaml') Not showing diff as it contains sensitive data, see .../examples/tutorial-secrets/work/.batou-diffs/...diff for the diff. -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=...s -======================== DEPLOYMENT PREDICTION FINISHED ======================== +... DEPLOYMENT PREDICTION FINISHED ... """ ) @@ -435,16 +435,16 @@ def test_check_consistency_works_with_local(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `gocept`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 test01: Connecting via local (1/2) 🌐 test02: Connecting via local (2/2) -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=∅ -====================== CONSISTENCY CHECK (local) FINISHED ====================== +... CONSISTENCY CHECK (local) FINISHED ... """ ) @@ -455,14 +455,14 @@ def test_predicting_deployment_works_with_local(): assert out == Ellipsis( """\ batou/2... (cpython 3...) -================================== Preparing =================================== +... Preparing ... 📦 main: Loading environment `gocept`... 🔍 main: Verifying repository ... 🔑 main: Loading secrets ... -================== Connecting hosts and configuring model ... ================== +... Connecting hosts and configuring model ... ... 🌐 test01: Connecting via local (1/2) 🌐 test02: Connecting via local (2/2) -======================== Predicting deployment actions ========================= +... Predicting deployment actions ... ⚪ test01: Scheduling component hello ... ⚪ test02: Scheduling component hello ... 🚀 test01: Hello > File('work/hello/hello') > Presence('hello') @@ -487,8 +487,8 @@ def test_predicting_deployment_works_with_local(): 🚀 test02: Hello > File('work/hello/other-secrets.yaml') > Content('other-secrets.yaml') Not showing diff as it contains sensitive data, see .../batou/examples/tutorial-secrets/work/.batou-diffs/...diff for the diff. -=================================== Summary ==================================== +... Summary ... Deployment took total=...s, connect=...s, deploy=...s -==================== DEPLOYMENT PREDICTION (local) FINISHED ==================== +... DEPLOYMENT PREDICTION (local) FINISHED ... """ ) diff --git a/tox.ini b/tox.ini index bf14ef2cd..09f33e17f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py38, py39, py310, py311, py312, pre-commit + py38, py39, py310, py311, py312, pre-commit, pyrage skip_missing_interpreters = true allowlist_externals=git @@ -13,6 +13,15 @@ setenv = extras = test commands = pytest {posargs} +[testenv:pyrage] +usedevelop = true +setenv = + APPENV_BEST_PYTHON = {envpython} + COLUMNS = 80 + IN_TOX_TEST = 1 +extras = test,pyrage +commands = pytest {posargs} + [testenv:pre-commit] basepython = python3.9