diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..678cb6151 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +docs/ +tests/ +tests_integration/ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..e96011d08 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +.github/workflows/test-integration.yml @ytausch +conda_forge_tick/contexts.py @ytausch +conda_forge_tick/git_utils.py @ytausch +conda_forge_tick/models/* @ytausch +tests/github_api/* @ytausch +tests/conda_forge_tick/* @ytausch +tests/model/* @ytausch +tests/test_contexts.py @ytausch +tests/test_git_utils.py @ytausch +tests_integration/* @ytausch diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 000000000..c039eb1dc --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,90 @@ +name: Integration Tests +on: + pull_request: + branches: + - main + merge_group: null + + +# Integration tests interact with GitHub resources in the integration test infrastructure and therefore +# cannot run concurrently with other integration tests. +concurrency: + group: cf-scripts-integration-tests + cancel-in-progress: false + +defaults: + run: + shell: bash -leo pipefail {0} + +jobs: + integration-tests: + name: Run Integration Tests + # if triggered by pull_request, only run on non-fork PRs (secrets access needed) + # Nevertheless, this check is always run in the merge queue. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: cf-scripts + submodules: 'true' + + - name: Build Docker Image + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 + with: + context: cf-scripts + push: false + load: true + tags: conda-forge-tick:test + + - name: Setup micromamba + uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4 + with: + environment-file: cf-scripts/conda-lock.yml + environment-name: cf-scripts + condarc-file: cf-scripts/autotick-bot/condarc + + - name: Run pip install + working-directory: cf-scripts + run: | + pip install --no-deps --no-build-isolation -e . + + - name: Run mitmproxy certificate setup wizard + working-directory: cf-scripts + run: | + # place a script in the mitmproxy directory that will be run by the setup wizard + # to trust the mitmproxy certificate + cat < ./tests_integration/.mitmproxy/${{ env.MITMPROXY_WIZARD_HEADLESS_TRUST_SCRIPT }} + #!/usr/bin/env bash + set -euo pipefail + sudo cp "\$1" /usr/local/share/ca-certificates/mitmproxy.crt + sudo update-ca-certificates + EOF + + ./tests_integration/mitmproxy_setup_wizard.sh + env: + MITMPROXY_WIZARD_HEADLESS: true + MITMPROXY_WIZARD_HEADLESS_TRUST_SCRIPT: mitmproxy_trust_script.sh + + - name: Set up git identity + run: | + git config --global user.name "regro-cf-autotick-bot-staging" + git config --global user.email "regro-cf-autotick-bot-staging@users.noreply.github.com" + + - name: Run Integration Tests with pytest + working-directory: cf-scripts + run: | + pytest -s -v \ + --dist=no \ + tests_integration + env: + BOT_TOKEN: ${{ secrets.GH_TOKEN_STAGING_BOT_USER }} + TEST_SETUP_TOKEN: ${{ secrets.GH_TOKEN_STAGING_BOT_USER }} + + - name: Print Proxy Logs + run: cat /tmp/mitmproxy.log + if: always() diff --git a/.gitignore b/.gitignore index b93237255..1485c489b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ docs/_build/ node_attrs/* graph.json +!tests_integration/resources/empty-graph/graph.json pr_json/* pr_status/* status/* @@ -61,3 +62,4 @@ pixi.toml .repodata_cache/ venv oryx-build-commands.txt +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..2167a7d22 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "tests_integration/definitions/conda_forge_pinning/resources/feedstock"] + path = tests_integration/lib/_definitions/conda_forge_pinning/resources/feedstock + url = https://github.com/conda-forge/conda-forge-pinning-feedstock.git +[submodule "tests_integration/definitions/pydantic/resources/feedstock"] + path = tests_integration/lib/_definitions/pydantic/resources/feedstock + url = https://github.com/conda-forge/pydantic-feedstock.git +[submodule "tests_integration/definitions/pydantic/resources/feedstock_v1"] + path = tests_integration/lib/_definitions/pydantic/resources/feedstock_v1 + url = https://github.com/conda-forge/pydantic-feedstock.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13a0f9360..c3f719b9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,11 @@ repos: rev: v1 hooks: - id: typos + exclude: | + (?x)^( + ^tests_integration\/definitions\/.*\/resources\/.*| + ^docs\/assets\/.* + )$ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.8 diff --git a/README.md b/README.md index 2f2963961..d6edaaad7 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,8 @@ If your migrator needs special configuration, you should write a new factory fun - `CF_FEEDSTOCK_OPS_CONTAINER_NAME`: the name of the container to use in the bot, otherwise defaults to `ghcr.io/regro/conda-forge-tick` - `CF_FEEDSTOCK_OPS_CONTAINER_TAG`: set this to override the default container tag used in production runs, otherwise the value of `__version__` is used +Additional environment variables are described in [the settings module](conda_forge_tick/settings.py). + ### Getting a Working Environment The bot has an abstract set of requirements stored in the `environment.yml` file in this repo. @@ -299,6 +301,10 @@ Currently, the following commands are supported and tested: - `update-upstream-versions` +### Integration Tests + +See [tests_integration/README.md](tests_integration/README.md). + ### Structure of the Bot's Jobs #### History diff --git a/autotick-bot/install_bot_code.sh b/autotick-bot/install_bot_code.sh index 5a1e4e722..e1459285a 100644 --- a/autotick-bot/install_bot_code.sh +++ b/autotick-bot/install_bot_code.sh @@ -43,6 +43,7 @@ done if [[ "${clone_graph}" == "true" ]]; then cf_graph_repo=${CF_TICK_GRAPH_GITHUB_BACKEND_REPO:-"regro/cf-graph-countyfair"} cf_graph_remote="https://github.com/${cf_graph_repo}.git" + # please make sure the cloning depth is always identical to the one used in the integration tests (test_integration.py) git clone --depth=5 "${cf_graph_remote}" cf-graph else echo "Skipping cloning of cf-graph" diff --git a/conda-lock.yml b/conda-lock.yml index 198441ea7..5237e2f98 100644 --- a/conda-lock.yml +++ b/conda-lock.yml @@ -3,9 +3,9 @@ metadata: - url: conda-forge used_env_vars: [] content_hash: - linux-64: 5b7eabb1a46c6f88f0f3dd8f6dda32adb1512f233541c460d63230888dd149c6 - osx-64: 5b82a6aebafe8fcda8346d44d8fdb2fda1d65535991978b9dbcaf23227b53bc6 - osx-arm64: 76d11b091e40c45ea65ce586fa2a88040e5711134d62999f6a13275ee4efcc0d + linux-64: b6e1bb8b05c8e2e978adf6045bbfbf3dd667be28ad370051e7def217c59677cf + osx-64: 976ee67b87113aaa96914dd74febe852a4f446b5cfd13d7970d596ddd49f0054 + osx-arm64: 955bee3a78e887405cec719f9153364a9fb08a16cf094727fe339313c3e243ff platforms: - osx-arm64 - linux-64 @@ -273,6 +273,139 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda version: 0.2.5 + - category: main + dependencies: + argon2-cffi-bindings: '' + python: '>=3.9' + typing-extensions: '' + hash: + md5: a7ee488b71c30ada51c48468337b85ba + sha256: 7af62339394986bc470a7a231c7f37ad0173ffb41f6bc0e8e31b0be9e3b9d20f + manager: conda + name: argon2-cffi + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_1.conda + version: 23.1.0 + - category: main + dependencies: + argon2-cffi-bindings: '' + python: '>=3.9' + typing-extensions: '' + hash: + md5: a7ee488b71c30ada51c48468337b85ba + sha256: 7af62339394986bc470a7a231c7f37ad0173ffb41f6bc0e8e31b0be9e3b9d20f + manager: conda + name: argon2-cffi + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_1.conda + version: 23.1.0 + - category: main + dependencies: + argon2-cffi-bindings: '' + python: '>=3.9' + typing-extensions: '' + hash: + md5: a7ee488b71c30ada51c48468337b85ba + sha256: 7af62339394986bc470a7a231c7f37ad0173ffb41f6bc0e8e31b0be9e3b9d20f + manager: conda + name: argon2-cffi + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_1.conda + version: 23.1.0 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + cffi: '>=1.0.1' + libgcc: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 18143eab7fcd6662c604b85850f0db1e + sha256: d1af1fbcb698c2e07b0d1d2b98384dd6021fa55c8bcb920e3652e0b0c393881b + manager: conda + name: argon2-cffi-bindings + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py311h9ecbd09_5.conda + version: 21.2.0 + - category: main + dependencies: + __osx: '>=10.13' + cffi: '>=1.0.1' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 29b46bd410067f668c4cef7fdc78fe25 + sha256: fa5eb633b320e10fc2138f3d842d8a8ca72815f106acbab49a68ec9783e4d70d + manager: conda + name: argon2-cffi-bindings + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-21.2.0-py311h3336109_5.conda + version: 21.2.0 + - category: main + dependencies: + __osx: '>=11.0' + cffi: '>=1.0.1' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 1e8260965552c6ec86453b7d15a598de + sha256: 6eabd1bcefc235b7943688d865519577d7668a2f4dc3a24ee34d81eb4bfe77d1 + manager: conda + name: argon2-cffi-bindings + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-21.2.0-py311h460d6c5_5.conda + version: 21.2.0 + - category: main + dependencies: + python: '>=3.7' + typing_extensions: '>=4' + hash: + md5: 596932155bf88bb6837141550cb721b0 + sha256: 63f85717fd38912a69be5a03d35a648c404cb86843cd4a1302c380c0e7744e30 + manager: conda + name: asgiref + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/asgiref-3.7.2-pyhd8ed1ab_0.conda + version: 3.7.2 + - category: main + dependencies: + python: '>=3.7' + typing_extensions: '>=4' + hash: + md5: 596932155bf88bb6837141550cb721b0 + sha256: 63f85717fd38912a69be5a03d35a648c404cb86843cd4a1302c380c0e7744e30 + manager: conda + name: asgiref + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/asgiref-3.7.2-pyhd8ed1ab_0.conda + version: 3.7.2 + - category: main + dependencies: + python: '>=3.7' + typing_extensions: '>=4' + hash: + md5: 596932155bf88bb6837141550cb721b0 + sha256: 63f85717fd38912a69be5a03d35a648c404cb86843cd4a1302c380c0e7744e30 + manager: conda + name: asgiref + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/asgiref-3.7.2-pyhd8ed1ab_0.conda + version: 3.7.2 - category: main dependencies: at-spi2-core: '>=2.40.0,<2.41.0a0' @@ -541,6 +674,51 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda version: 1.2.0 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: f4bf34159bdc05e00ec3ff1335539671 + sha256: 3e3fccfe53b4973e379f1593f8cd2a6a61b0a0f30bd521bd24467f995777c315 + manager: conda + name: bcrypt + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/bcrypt-4.3.0-py311h9e33e62_0.conda + version: 4.3.0 + - category: main + dependencies: + __osx: '>=10.13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 832437e99c434257bdc86f5ec66b7422 + sha256: 10b2cc01a0a56f9db38a5b16dc37c93b484057aa7d5d2bc071fe062e3a0a622e + manager: conda + name: bcrypt + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/bcrypt-4.3.0-py311h3b9c2be_0.conda + version: 4.3.0 + - category: main + dependencies: + __osx: '>=11.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: e9e5de6d88db912afdb0f7414cf0cb8b + sha256: dd0e8a2ff62e77fe4cd33bd8d668558f7bcd564af00aac7706908981ea6862da + manager: conda + name: bcrypt + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-4.3.0-py311h3ff9189_0.conda + version: 4.3.0 - category: main dependencies: python: '>=3.9' @@ -744,53 +922,50 @@ package: version: 1.38.21 - category: main dependencies: - __glibc: '>=2.17,<3.0.a0' - libgcc: '>=13' - libstdcxx: '>=13' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: d21daab070d76490cb39a8f1d1729d79 - sha256: 949913bbd1f74d1af202d3e4bff2e0a4e792ec00271dc4dd08641d4221aa2e12 + md5: ced5340f5dc6cff43a80deac8d0e398f + sha256: e2c0a391839914a1b3611b661ebd1736dd747f43a4611254d9333d9db3163ec7 manager: conda name: brotli-python optional: false platform: linux-64 url: - https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hfdbb021_2.conda - version: 1.1.0 + https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.0.9-py311ha362b79_9.conda + version: 1.0.9 - category: main dependencies: - __osx: '>=10.13' - libcxx: '>=17' + libcxx: '>=14.0.6' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: d75f06ee06001794aa83a05e885f1520 - sha256: 004cefbd18f581636a8dcb1964fb73478f15d496769226ec896c1d4a0161b7d8 + md5: 034ddcc806d421524fbc46778447e87c + sha256: 0d49fcc6eddfc5d87844419b9de4267a83e870331102bf01ca41e404e4374293 manager: conda name: brotli-python optional: false platform: osx-64 url: - https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py311hd89902b_2.conda - version: 1.1.0 + https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.0.9-py311h814d153_9.conda + version: 1.0.9 - category: main dependencies: - __osx: '>=11.0' - libcxx: '>=17' + libcxx: '>=14.0.6' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: c8793a23206344faa25f4e0b5d0e7908 - sha256: f507d65e740777a629ceacb062c768829ab76fde01446b191699a734521ecaad + md5: 34c36b315dc70cde887ea8c3991b994d + sha256: a0f54181606c26b754567feac9d0595b7d5de5d199aa15129dcfa3eed10ef3b7 manager: conda name: brotli-python optional: false platform: osx-arm64 url: - https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py311h3f08180_2.conda - version: 1.1.0 + https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.0.9-py311ha397e9f_9.conda + version: 1.0.9 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -2792,56 +2967,53 @@ package: version: 0.4.1 - category: main dependencies: - __glibc: '>=2.17,<3.0.a0' cffi: '>=1.12' - libgcc: '>=13' - openssl: '>=3.5.0,<4.0a0' + libgcc-ng: '>=12' + openssl: '>=3.1.0,<4.0a0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: 2d1de50ca2588224ec5e94e9b4ad5fd6 - sha256: 02d0776daff4c07f305d5af5f9e06026ec50f5cec6e90858d6342bdfb90cf320 + md5: 4df4df92db0b9168c11b72460baec870 + sha256: e0f62e90e664ce33054c7839ee10a975e0a80010c2691e99679319f60decca9f manager: conda name: cryptography optional: false platform: linux-64 url: - https://conda.anaconda.org/conda-forge/linux-64/cryptography-45.0.2-py311hafd3f86_0.conda - version: 45.0.2 + https://conda.anaconda.org/conda-forge/linux-64/cryptography-40.0.2-py311h9b4c7bb_0.conda + version: 40.0.2 - category: main dependencies: - __osx: '>=10.13' cffi: '>=1.12' - openssl: '>=3.5.0,<4.0a0' + openssl: '>=3.1.0,<4.0a0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: e4e38cb3d7b9bd01a1bc8ea748aada31 - sha256: c199fbdcc45cc302bea6d40cb52cf9fb9e7db10cacbd4e34312e9b00612953e3 + md5: 724b75f84bb1b5d932627d090a527168 + sha256: 2700217dfc3a48e8715d7f4e94870448a37d8e9bb25c4130e83c4807c2f1f3c3 manager: conda name: cryptography optional: false platform: osx-64 url: - https://conda.anaconda.org/conda-forge/osx-64/cryptography-45.0.2-py311h336e25c_0.conda - version: 45.0.2 + https://conda.anaconda.org/conda-forge/osx-64/cryptography-40.0.2-py311h61927ef_0.conda + version: 40.0.2 - category: main dependencies: - __osx: '>=11.0' cffi: '>=1.12' - openssl: '>=3.5.0,<4.0a0' + openssl: '>=3.1.0,<4.0a0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: f17d8b84e6ab22b5aaca22085b5a2973 - sha256: c706d83c056219fc60a30065ec0c26588c82894971217189725ee60339ac5359 + md5: 09ebc937e6441f174bf76ea8f3b789ce + sha256: 2c10a11166f3199795efb6ceceb4dd4557c38f40d568df8af2b829e4597dc360 manager: conda name: cryptography optional: false platform: osx-arm64 url: - https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-45.0.2-py311h8be0713_0.conda - version: 45.0.2 + https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-40.0.2-py311h507f6e9_0.conda + version: 40.0.2 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -3277,43 +3449,52 @@ package: version: 1.9.0 - category: main dependencies: - python: '>=3.9,<4.0.0' + cryptography: '>=2.6,<42.0' + httpcore: '>=0.17.3' + idna: '>=2.1,<4.0' + python: '>=3.8.0,<4.0.0' sniffio: '' hash: - md5: 5fbd60d61d21b4bd2f9d7a48fe100418 - sha256: 3ec40ccf63f2450c5e6c7dd579e42fc2e97caf0d8cd4ba24aa434e6fc264eda0 + md5: a0059139087e108074f4b48b5e94730e + sha256: 11feaf50685db60b7b0b2c3253930fe8c38c6ff1b7a40aafbf37e5a3f4dc97fc manager: conda name: dnspython optional: false platform: linux-64 - url: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.7.0-pyhff2d567_1.conda - version: 2.7.0 + url: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.6.1-pyhd8ed1ab_0.conda + version: 2.6.1 - category: main dependencies: - python: '>=3.9,<4.0.0' + cryptography: '>=2.6,<42.0' + httpcore: '>=0.17.3' + idna: '>=2.1,<4.0' + python: '>=3.8.0,<4.0.0' sniffio: '' hash: - md5: 5fbd60d61d21b4bd2f9d7a48fe100418 - sha256: 3ec40ccf63f2450c5e6c7dd579e42fc2e97caf0d8cd4ba24aa434e6fc264eda0 + md5: a0059139087e108074f4b48b5e94730e + sha256: 11feaf50685db60b7b0b2c3253930fe8c38c6ff1b7a40aafbf37e5a3f4dc97fc manager: conda name: dnspython optional: false platform: osx-64 - url: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.7.0-pyhff2d567_1.conda - version: 2.7.0 + url: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.6.1-pyhd8ed1ab_0.conda + version: 2.6.1 - category: main dependencies: - python: '>=3.9,<4.0.0' + cryptography: '>=2.6,<42.0' + httpcore: '>=0.17.3' + idna: '>=2.1,<4.0' + python: '>=3.8.0,<4.0.0' sniffio: '' hash: - md5: 5fbd60d61d21b4bd2f9d7a48fe100418 - sha256: 3ec40ccf63f2450c5e6c7dd579e42fc2e97caf0d8cd4ba24aa434e6fc264eda0 + md5: a0059139087e108074f4b48b5e94730e + sha256: 11feaf50685db60b7b0b2c3253930fe8c38c6ff1b7a40aafbf37e5a3f4dc97fc manager: conda name: dnspython optional: false platform: osx-arm64 - url: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.7.0-pyhff2d567_1.conda - version: 2.7.0 + url: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.6.1-pyhd8ed1ab_0.conda + version: 2.6.1 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -3399,6 +3580,90 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda version: '0.5' + - category: main + dependencies: + dnspython: '>=2.0.0' + idna: '>=2.0.0' + python: '>=3.9' + hash: + md5: da16dd3b0b71339060cd44cb7110ddf9 + sha256: b91a19eb78edfc2dbb36de9a67f74ee2416f1b5273dd7327abe53f2dbf864736 + manager: conda + name: email-validator + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/email-validator-2.2.0-pyhd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + dnspython: '>=2.0.0' + idna: '>=2.0.0' + python: '>=3.9' + hash: + md5: da16dd3b0b71339060cd44cb7110ddf9 + sha256: b91a19eb78edfc2dbb36de9a67f74ee2416f1b5273dd7327abe53f2dbf864736 + manager: conda + name: email-validator + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/email-validator-2.2.0-pyhd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + dnspython: '>=2.0.0' + idna: '>=2.0.0' + python: '>=3.9' + hash: + md5: da16dd3b0b71339060cd44cb7110ddf9 + sha256: b91a19eb78edfc2dbb36de9a67f74ee2416f1b5273dd7327abe53f2dbf864736 + manager: conda + name: email-validator + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/email-validator-2.2.0-pyhd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + email-validator: '>=2.2.0,<2.2.1.0a0' + hash: + md5: 0794f8807ff2c6f020422cacb1bd7bfa + sha256: e0d0fdf587aa0ed0ff08b2bce3ab355f46687b87b0775bfba01cc80a859ee6a2 + manager: conda + name: email_validator + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/email_validator-2.2.0-hd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + email-validator: '>=2.2.0,<2.2.1.0a0' + hash: + md5: 0794f8807ff2c6f020422cacb1bd7bfa + sha256: e0d0fdf587aa0ed0ff08b2bce3ab355f46687b87b0775bfba01cc80a859ee6a2 + manager: conda + name: email_validator + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/email_validator-2.2.0-hd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + email-validator: '>=2.2.0,<2.2.1.0a0' + hash: + md5: 0794f8807ff2c6f020422cacb1bd7bfa + sha256: e0d0fdf587aa0ed0ff08b2bce3ab355f46687b87b0775bfba01cc80a859ee6a2 + manager: conda + name: email_validator + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/email_validator-2.2.0-hd8ed1ab_1.conda + version: 2.2.0 - category: main dependencies: appdirs: '' @@ -3601,6 +3866,120 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda version: 2.1.1 + - category: main + dependencies: + email_validator: '>=2.0.0' + fastapi-cli: '>=0.0.5' + httpx: '>=0.23.0' + jinja2: '>=3.1.5' + pydantic: '>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0' + python: '' + python-multipart: '>=0.0.18' + starlette: '>=0.40.0,<0.47.0' + typing_extensions: '>=4.8.0' + uvicorn-standard: '>=0.12.0' + hash: + md5: 4bc12ece07c8c717e19fd790bfec100d + sha256: d72da6ea523d80968f0cca4ba4fb6c31fc27450d07e419f039da9b99654a56e6 + manager: conda + name: fastapi + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/fastapi-0.115.12-pyh29332c3_0.conda + version: 0.115.12 + - category: main + dependencies: + email_validator: '>=2.0.0' + fastapi-cli: '>=0.0.5' + httpx: '>=0.23.0' + jinja2: '>=3.1.5' + pydantic: '>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0' + python: '>=3.9' + python-multipart: '>=0.0.18' + starlette: '>=0.40.0,<0.47.0' + typing_extensions: '>=4.8.0' + uvicorn-standard: '>=0.12.0' + hash: + md5: 4bc12ece07c8c717e19fd790bfec100d + sha256: d72da6ea523d80968f0cca4ba4fb6c31fc27450d07e419f039da9b99654a56e6 + manager: conda + name: fastapi + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/fastapi-0.115.12-pyh29332c3_0.conda + version: 0.115.12 + - category: main + dependencies: + email_validator: '>=2.0.0' + fastapi-cli: '>=0.0.5' + httpx: '>=0.23.0' + jinja2: '>=3.1.5' + pydantic: '>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0' + python: '>=3.9' + python-multipart: '>=0.0.18' + starlette: '>=0.40.0,<0.47.0' + typing_extensions: '>=4.8.0' + uvicorn-standard: '>=0.12.0' + hash: + md5: 4bc12ece07c8c717e19fd790bfec100d + sha256: d72da6ea523d80968f0cca4ba4fb6c31fc27450d07e419f039da9b99654a56e6 + manager: conda + name: fastapi + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/fastapi-0.115.12-pyh29332c3_0.conda + version: 0.115.12 + - category: main + dependencies: + python: '>=3.9' + rich-toolkit: '>=0.11.1' + typer: '>=0.12.3' + uvicorn-standard: '>=0.15.0' + hash: + md5: d960e0ea9e1c561aa928f6c4439f04c7 + sha256: 300683731013b7221922339cd40430bb3c2ddeeb658fd7e37f5099ffe64e4db0 + manager: conda + name: fastapi-cli + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.7-pyhd8ed1ab_0.conda + version: 0.0.7 + - category: main + dependencies: + python: '>=3.9' + rich-toolkit: '>=0.11.1' + typer: '>=0.12.3' + uvicorn-standard: '>=0.15.0' + hash: + md5: d960e0ea9e1c561aa928f6c4439f04c7 + sha256: 300683731013b7221922339cd40430bb3c2ddeeb658fd7e37f5099ffe64e4db0 + manager: conda + name: fastapi-cli + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.7-pyhd8ed1ab_0.conda + version: 0.0.7 + - category: main + dependencies: + python: '>=3.9' + rich-toolkit: '>=0.11.1' + typer: '>=0.12.3' + uvicorn-standard: '>=0.15.0' + hash: + md5: d960e0ea9e1c561aa928f6c4439f04c7 + sha256: 300683731013b7221922339cd40430bb3c2ddeeb658fd7e37f5099ffe64e4db0 + manager: conda + name: fastapi-cli + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.7-pyhd8ed1ab_0.conda + version: 0.0.7 - category: main dependencies: python: '>=3.9' @@ -3717,27 +4096,81 @@ package: version: 3.8.1 - category: main dependencies: - __glibc: '>=2.17,<3.0.a0' - libgcc: '>=13' - libstdcxx: '>=13' + blinker: '>=1.6.2' + click: '>=8.1.3' + importlib-metadata: '>=3.6.0' + itsdangerous: '>=2.1.2' + jinja2: '>=3.1.2' + python: '>=3.8' + werkzeug: '>=2.3.7' hash: - md5: 288a90e722fd7377448b00b2cddcb90d - sha256: 2db2a6a1629bc2ac649b31fd990712446394ce35930025e960e1765a9249af5d + md5: 9b0d29067484a8dfacfae85b8fba81bc + sha256: 4f84ffdc5471236e8225db86c7508426b46aa2c3802d58ca40b3c3e174533b39 manager: conda - name: fmt + name: flask optional: false platform: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/fmt-11.1.4-h07f6e7f_1.conda - version: 11.1.4 + url: https://conda.anaconda.org/conda-forge/noarch/flask-2.3.3-pyhd8ed1ab_0.conda + version: 2.3.3 - category: main dependencies: - __osx: '>=10.13' - libcxx: '>=18' + blinker: '>=1.6.2' + click: '>=8.1.3' + importlib-metadata: '>=3.6.0' + itsdangerous: '>=2.1.2' + jinja2: '>=3.1.2' + python: '>=3.8' + werkzeug: '>=2.3.7' hash: - md5: dce40ffbf3008b17cb090ad05b13e036 - sha256: 89bf2bcc03738a3d4904dfc9736000033003d4a04647d93d2c867568bf2339ef + md5: 9b0d29067484a8dfacfae85b8fba81bc + sha256: 4f84ffdc5471236e8225db86c7508426b46aa2c3802d58ca40b3c3e174533b39 manager: conda - name: fmt + name: flask + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/flask-2.3.3-pyhd8ed1ab_0.conda + version: 2.3.3 + - category: main + dependencies: + blinker: '>=1.6.2' + click: '>=8.1.3' + importlib-metadata: '>=3.6.0' + itsdangerous: '>=2.1.2' + jinja2: '>=3.1.2' + python: '>=3.8' + werkzeug: '>=2.3.7' + hash: + md5: 9b0d29067484a8dfacfae85b8fba81bc + sha256: 4f84ffdc5471236e8225db86c7508426b46aa2c3802d58ca40b3c3e174533b39 + manager: conda + name: flask + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/flask-2.3.3-pyhd8ed1ab_0.conda + version: 2.3.3 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + libstdcxx: '>=13' + hash: + md5: 288a90e722fd7377448b00b2cddcb90d + sha256: 2db2a6a1629bc2ac649b31fd990712446394ce35930025e960e1765a9249af5d + manager: conda + name: fmt + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/fmt-11.1.4-h07f6e7f_1.conda + version: 11.1.4 + - category: main + dependencies: + __osx: '>=10.13' + libcxx: '>=18' + hash: + md5: dce40ffbf3008b17cb090ad05b13e036 + sha256: 89bf2bcc03738a3d4904dfc9736000033003d4a04647d93d2c867568bf2339ef + manager: conda + name: fmt optional: false platform: osx-64 url: https://conda.anaconda.org/conda-forge/osx-64/fmt-11.1.4-hbf61d64_1.conda @@ -4860,40 +5293,40 @@ package: python: '>=3.9' typing_extensions: '' hash: - md5: 4b69232755285701bc86a5afe4d9933a - sha256: f64b68148c478c3bfc8f8d519541de7d2616bf59d44485a5271041d40c061887 + md5: 7ee49e89531c0dcbba9466f6d115d585 + sha256: 622516185a7c740d5c7f27016d0c15b45782c1501e5611deec63fd70344ce7c8 manager: conda name: h11 optional: false platform: linux-64 - url: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - version: 0.16.0 + url: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda + version: 0.14.0 - category: main dependencies: python: '>=3.9' typing_extensions: '' hash: - md5: 4b69232755285701bc86a5afe4d9933a - sha256: f64b68148c478c3bfc8f8d519541de7d2616bf59d44485a5271041d40c061887 + md5: 7ee49e89531c0dcbba9466f6d115d585 + sha256: 622516185a7c740d5c7f27016d0c15b45782c1501e5611deec63fd70344ce7c8 manager: conda name: h11 optional: false platform: osx-64 - url: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - version: 0.16.0 + url: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda + version: 0.14.0 - category: main dependencies: python: '>=3.9' typing_extensions: '' hash: - md5: 4b69232755285701bc86a5afe4d9933a - sha256: f64b68148c478c3bfc8f8d519541de7d2616bf59d44485a5271041d40c061887 + md5: 7ee49e89531c0dcbba9466f6d115d585 + sha256: 622516185a7c740d5c7f27016d0c15b45782c1501e5611deec63fd70344ce7c8 manager: conda name: h11 optional: false platform: osx-arm64 - url: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - version: 0.16.0 + url: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda + version: 0.14.0 - category: main dependencies: hpack: '>=4.1,<5' @@ -5222,55 +5655,101 @@ package: version: 4.1.0 - category: main dependencies: - anyio: '>=4.0,<5.0' + anyio: '>=3.0,<5.0' certifi: '' - h11: '>=0.16' + h11: '>=0.13,<0.15' h2: '>=3,<5' - python: '' + python: '>=3.8' sniffio: 1.* hash: - md5: 4f14640d58e2cc0aa0819d9d8ba125bb - sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 2ca8e6dbc86525c8b95e3c0ffa26442e + sha256: c84d012a245171f3ed666a8bf9319580c269b7843ffa79f26468842da3abd5df manager: conda name: httpcore optional: false platform: linux-64 - url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - version: 1.0.9 + url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda + version: 1.0.7 - category: main dependencies: - anyio: '>=4.0,<5.0' + anyio: '>=3.0,<5.0' certifi: '' - h11: '>=0.16' + h11: '>=0.13,<0.15' h2: '>=3,<5' - python: '>=3.9' + python: '>=3.8' sniffio: 1.* hash: - md5: 4f14640d58e2cc0aa0819d9d8ba125bb - sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 2ca8e6dbc86525c8b95e3c0ffa26442e + sha256: c84d012a245171f3ed666a8bf9319580c269b7843ffa79f26468842da3abd5df manager: conda name: httpcore optional: false platform: osx-64 - url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - version: 1.0.9 + url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda + version: 1.0.7 - category: main dependencies: - anyio: '>=4.0,<5.0' + anyio: '>=3.0,<5.0' certifi: '' - h11: '>=0.16' + h11: '>=0.13,<0.15' h2: '>=3,<5' - python: '>=3.9' + python: '>=3.8' sniffio: 1.* hash: - md5: 4f14640d58e2cc0aa0819d9d8ba125bb - sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 2ca8e6dbc86525c8b95e3c0ffa26442e + sha256: c84d012a245171f3ed666a8bf9319580c269b7843ffa79f26468842da3abd5df manager: conda name: httpcore optional: false platform: osx-arm64 - url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - version: 1.0.9 + url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda + version: 1.0.7 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: c16a94f3d0c6a2a495b3071cff3f598d + sha256: 1775083ed07111778559e9a0b47033c13cbe6f1c489eaceff204f6cf7a9e02da + manager: conda + name: httptools + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/httptools-0.6.4-py311h9ecbd09_0.conda + version: 0.6.4 + - category: main + dependencies: + __osx: '>=10.13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: eee2f4ab03514d6ca5cd33ac2c3a1846 + sha256: bb796dfdbf36aedf07471a3b0911803cac5b9cb2e1bdf8301a633ba3f8dd9d4e + manager: conda + name: httptools + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/httptools-0.6.4-py311h4d7f069_0.conda + version: 0.6.4 + - category: main + dependencies: + __osx: '>=11.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 4aca39fe9eb4224026c907e1aa8156fb + sha256: 47af7c9e41ea0327f12757527cea28c430ef84aade923d81cc397ebb2bf9eb28 + manager: conda + name: httptools + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.6.4-py311h917b07b_0.conda + version: 0.6.4 - category: main dependencies: anyio: '' @@ -5669,6 +6148,45 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/isodate-0.7.2-pyhd8ed1ab_1.conda version: 0.7.2 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 7ac5f795c15f288984e32add616cdc59 + sha256: 1684b7b16eec08efef5302ce298c606b163c18272b69a62b666fbaa61516f170 + manager: conda + name: itsdangerous + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 7ac5f795c15f288984e32add616cdc59 + sha256: 1684b7b16eec08efef5302ce298c606b163c18272b69a62b666fbaa61516f170 + manager: conda + name: itsdangerous + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda + version: 2.2.0 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 7ac5f795c15f288984e32add616cdc59 + sha256: 1684b7b16eec08efef5302ce298c606b163c18272b69a62b666fbaa61516f170 + manager: conda + name: itsdangerous + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda + version: 2.2.0 - category: main dependencies: more-itertools: '' @@ -6062,6 +6580,45 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda version: 2025.4.1 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: fa5ce9990186f1999a77fe7e1ff76e72 + sha256: 8986592c083706f7c791e6325c5cbd43c1c90df1ec2680f74beb23f0830454da + manager: conda + name: kaitaistruct + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/kaitaistruct-0.10-pyhd8ed1ab_1.conda + version: '0.10' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: fa5ce9990186f1999a77fe7e1ff76e72 + sha256: 8986592c083706f7c791e6325c5cbd43c1c90df1ec2680f74beb23f0830454da + manager: conda + name: kaitaistruct + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/kaitaistruct-0.10-pyhd8ed1ab_1.conda + version: '0.10' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: fa5ce9990186f1999a77fe7e1ff76e72 + sha256: 8986592c083706f7c791e6325c5cbd43c1c90df1ec2680f74beb23f0830454da + manager: conda + name: kaitaistruct + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/kaitaistruct-0.10-pyhd8ed1ab_1.conda + version: '0.10' - category: main dependencies: __linux: '' @@ -6249,6 +6806,45 @@ package: url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda version: '2.43' + - category: main + dependencies: + pyasn1: '>=0.4.6' + python: '>=3.9' + hash: + md5: 7319a76eaab1e21f84ad7949ff2f66e7 + sha256: 0816b267189241330b85bbe9524423255cd4daf8dd4d97765213b49991d1c33d + manager: conda + name: ldap3 + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/ldap3-2.9.1-pyhd8ed1ab_1.conda + version: 2.9.1 + - category: main + dependencies: + pyasn1: '>=0.4.6' + python: '>=3.9' + hash: + md5: 7319a76eaab1e21f84ad7949ff2f66e7 + sha256: 0816b267189241330b85bbe9524423255cd4daf8dd4d97765213b49991d1c33d + manager: conda + name: ldap3 + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/ldap3-2.9.1-pyhd8ed1ab_1.conda + version: 2.9.1 + - category: main + dependencies: + pyasn1: '>=0.4.6' + python: '>=3.9' + hash: + md5: 7319a76eaab1e21f84ad7949ff2f66e7 + sha256: 0816b267189241330b85bbe9524423255cd4daf8dd4d97765213b49991d1c33d + manager: conda + name: ldap3 + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/ldap3-2.9.1-pyhd8ed1ab_1.conda + version: 2.9.1 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -6289,6 +6885,49 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda version: 4.0.0 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + hash: + md5: c48fc56ec03229f294176923c3265c05 + sha256: 945396726cadae174a661ce006e3f74d71dbd719219faf7cc74696b267f7b0b5 + manager: conda + name: libabseil + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.2-cxx17_he02047a_1.conda + version: '20240116.2' + - category: main + dependencies: + __osx: '>=10.13' + libcxx: '>=16' + hash: + md5: d6c78ca84abed3fea5f308ac83b8f54e + sha256: 396d18f39d5207ecae06fddcbc6e5f20865718939bc4e0ea9729e13952833aac + manager: conda + name: libabseil + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/libabseil-20240116.2-cxx17_hf036a51_1.conda + version: '20240116.2' + - category: main + dependencies: + __osx: '>=11.0' + libcxx: '>=16' + hash: + md5: f16963d88aed907af8b90878b8d8a05c + sha256: a9517c8683924f4b3b9380cdaa50fdd2009cd8d5f3918c92f64394238189d3cb + manager: conda + name: libabseil + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20240116.2-cxx17_h00cdb27_1.conda + version: '20240116.2' - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -7778,6 +8417,55 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.47-h3783ad8_0.conda version: 1.6.47 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libgcc: '>=13' + libstdcxx: '>=13' + libzlib: '>=1.3.1,<2.0a0' + hash: + md5: 06def97690ef90781a91b786cb48a0a9 + sha256: 8b5e4e31ed93bf36fd14e9cf10cd3af78bb9184d0f1f87878b8d28c0374aa4dc + manager: conda + name: libprotobuf + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-hd5b35b9_1.conda + version: 4.25.3 + - category: main + dependencies: + __osx: '>=10.13' + libabseil: '>=20240116.2,<20240117.0a0' + libcxx: '>=17' + libzlib: '>=1.3.1,<2.0a0' + hash: + md5: 64ad501f0fd74955056169ec9c42c5c0 + sha256: f509cb24a164b84553b28837ec1e8311ceb0212a1dbb8c7fd99ca383d461ea6c + manager: conda + name: libprotobuf + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-4.25.3-hd4aba4c_1.conda + version: 4.25.3 + - category: main + dependencies: + __osx: '>=11.0' + libabseil: '>=20240116.2,<20240117.0a0' + libcxx: '>=17' + libzlib: '>=1.3.1,<2.0a0' + hash: + md5: fa77986d9170450c014586ab87e144f8 + sha256: f51bde2dfe73968ab3090c1098f520b65a8d8f11e945cb13bf74d19e30966b61 + manager: conda + name: libprotobuf + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-4.25.3-hc39d83c_1.conda + version: 4.25.3 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -8135,6 +8823,43 @@ package: platform: linux-64 url: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda version: 2.38.1 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + hash: + md5: 771ee65e13bc599b0b62af5359d80169 + sha256: b4a8890023902aef9f1f33e3e35603ad9c2f16c21fdb58e968fa6c1bd3e94c0b + manager: conda + name: libuv + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.50.0-hb9d3cd8_0.conda + version: 1.50.0 + - category: main + dependencies: + __osx: '>=11.0' + hash: + md5: c86c7473f79a3c06de468b923416aa23 + sha256: ec9da0a005c668c0964e0a6546c21416bab608569b5863edbdf135cee26e67d8 + manager: conda + name: libuv + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.50.0-h4cb831e_0.conda + version: 1.50.0 + - category: main + dependencies: + __osx: '>=11.0' + hash: + md5: 20717343fb30798ab7c23c2e92b748c1 + sha256: d13fb49d4c8262bf2c44ffb2c77bb2b5d0f85fc6de76bdb75208efeccb29fce6 + manager: conda + name: libuv + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.50.0-h5505292_0.conda + version: 1.50.0 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -8845,6 +9570,118 @@ package: url: https://conda.anaconda.org/conda-forge/osx-arm64/menuinst-2.2.0-py311h267d04e_0.conda version: 2.2.0 + - category: main + dependencies: + asgiref: '>=3.2.10,<3.8' + brotli-python: '>=1.0,<1.1' + certifi: '>=2019.9.11' + cryptography: '>=38,<41.1' + flask: '>=1.1.1,<2.4' + h11: '>=0.11,<0.15' + h2: '>=4.1,<5' + hyperframe: '>=6.0,<7' + kaitaistruct: '>=0.10,<0.11' + ldap3: '>=2.8,<2.10' + libgcc-ng: '>=12' + msgpack-python: '>=1.0.0,<1.1.0' + passlib: '>=1.6.5,<1.8' + protobuf: '>=3.14,<5' + publicsuffix2: '>=2.20190812,<3' + pylsqpack: '>=0.3.3,<0.4.0' + pyopenssl: '>=22.1,<23.2' + pyparsing: '>=2.4.2,<3.2' + pyperclip: '>=1.6.0,<1.9' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + ruamel.yaml: '>=0.16,<0.18' + sortedcontainers: '>=2.3,<2.5' + tornado: '>=6.2,<7' + wsproto: '>=1.0,<1.3' + zstandard: '>=0.11,<0.22' + hash: + md5: 286dfce5e12ff1d688b43bb7e11fb6bb + sha256: f199c24c7e77788191d66a8ff09eb3f31896f6c6dc1185dfe2fac6f43d94b859 + manager: conda + name: mitmproxy + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/mitmproxy-10.1.0-py311h46250e7_1.conda + version: 10.1.0 + - category: main + dependencies: + asgiref: '>=3.2.10,<3.8' + brotli-python: '>=1.0,<1.1' + certifi: '>=2019.9.11' + cryptography: '>=38,<41.1' + flask: '>=1.1.1,<2.4' + h11: '>=0.11,<0.15' + h2: '>=4.1,<5' + hyperframe: '>=6.0,<7' + kaitaistruct: '>=0.10,<0.11' + ldap3: '>=2.8,<2.10' + msgpack-python: '>=1.0.0,<1.1.0' + passlib: '>=1.6.5,<1.8' + protobuf: '>=3.14,<5' + publicsuffix2: '>=2.20190812,<3' + pylsqpack: '>=0.3.3,<0.4.0' + pyopenssl: '>=22.1,<23.2' + pyparsing: '>=2.4.2,<3.2' + pyperclip: '>=1.6.0,<1.9' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + ruamel.yaml: '>=0.16,<0.18' + sortedcontainers: '>=2.3,<2.5' + tornado: '>=6.2,<7' + wsproto: '>=1.0,<1.3' + zstandard: '>=0.11,<0.22' + hash: + md5: a7d334de4fad6050bfe8898c69d98111 + sha256: cdf2b6bd3543f3b3ef466eb1b0b5b1488c482cddbd34c112b0d1b6a46b263e99 + manager: conda + name: mitmproxy + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/mitmproxy-10.1.0-py311h5e0f0e4_0.conda + version: 10.1.0 + - category: main + dependencies: + asgiref: '>=3.2.10,<3.8' + brotli-python: '>=1.0,<1.1' + certifi: '>=2019.9.11' + cryptography: '>=38,<41.1' + flask: '>=1.1.1,<2.4' + h11: '>=0.11,<0.15' + h2: '>=4.1,<5' + hyperframe: '>=6.0,<7' + kaitaistruct: '>=0.10,<0.11' + ldap3: '>=2.8,<2.10' + msgpack-python: '>=1.0.0,<1.1.0' + passlib: '>=1.6.5,<1.8' + protobuf: '>=3.14,<5' + publicsuffix2: '>=2.20190812,<3' + pylsqpack: '>=0.3.3,<0.4.0' + pyopenssl: '>=22.1,<23.2' + pyparsing: '>=2.4.2,<3.2' + pyperclip: '>=1.6.0,<1.9' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + ruamel.yaml: '>=0.16,<0.18' + sortedcontainers: '>=2.3,<2.5' + tornado: '>=6.2,<7' + wsproto: '>=1.0,<1.3' + zstandard: '>=0.11,<0.22' + hash: + md5: 0059bf2757092ddcd33e9da3343f8816 + sha256: 0c54d7ca50d4b46c937ceb67fa8702d7cd249b4789d30af590a134f9094336bf + manager: conda + name: mitmproxy + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/mitmproxy-10.1.0-py311h94f323b_0.conda + version: 10.1.0 - category: main dependencies: python: '>=3.9' @@ -8892,15 +9729,15 @@ package: python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: 682f76920687f7d9283039eb542fdacf - sha256: 9033fa7084cbfd10e1b7ed3b74cee17169a0731ec98244d05c372fc4a935d5c9 + md5: b7f5e94f0a10f39bea5ded40b9adb73c + sha256: f74a33bd12a538e0aa1f0289e4263e0a3d18b125985dc08dac28250e3b197f5d manager: conda name: msgpack-python optional: false platform: linux-64 url: - https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.0-py311hd18a35c_0.conda - version: 1.1.0 + https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.0.8-py311hd18a35c_1.conda + version: 1.0.8 - category: main dependencies: __osx: '>=10.13' @@ -8908,15 +9745,15 @@ package: python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: 6804cd42195bf94efd1b892688c96412 - sha256: b56b1e7d156b88cc0c62734acf56d4ee809723614f659e4203028e7eeac16a78 + md5: d354e6628d55888eb84a036eed8f369d + sha256: 4358505cef3440617baf48ac379a3b31a327793fb1dd9a4086872b3553fa9cbd manager: conda name: msgpack-python optional: false platform: osx-64 url: - https://conda.anaconda.org/conda-forge/osx-64/msgpack-python-1.1.0-py311hf2f7c97_0.conda - version: 1.1.0 + https://conda.anaconda.org/conda-forge/osx-64/msgpack-python-1.0.8-py311hf2f7c97_1.conda + version: 1.0.8 - category: main dependencies: __osx: '>=11.0' @@ -8924,15 +9761,15 @@ package: python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: 6c826762702474fb0def6cedd2db5316 - sha256: aafa8572c72283801148845772fd9d494765bdcf1b8ae6f435e1caff4f1c97f3 + md5: ce5a4c818a8ac6bc6ebff30f3d2814eb + sha256: 17b3cff3dff93eb16b889698ffd2edb28f3a0fbf94e1d6629531bd3a01b575ae manager: conda name: msgpack-python optional: false platform: osx-arm64 url: - https://conda.anaconda.org/conda-forge/osx-arm64/msgpack-python-1.1.0-py311h2c37856_0.conda - version: 1.1.0 + https://conda.anaconda.org/conda-forge/osx-arm64/msgpack-python-1.0.8-py311h2c37856_1.conda + version: 1.0.8 - category: main dependencies: certifi: '>=2017.4.17' @@ -9513,7 +10350,52 @@ package: version: 1.4.2 - category: main dependencies: - libgcc-ng: '>=9.3.0' + argon2-cffi: '>=19.2.0' + bcrypt: '>=3.1.0' + cryptography: '' + python: '>=3.9' + hash: + md5: fba64c154edb7d7935af0d46d97ff536 + sha256: 2adfe01cdab93c39c4d8dfe3de74a31ae6fded21213f26925208ce6053cea93d + manager: conda + name: passlib + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/passlib-1.7.4-pyhd8ed1ab_2.conda + version: 1.7.4 + - category: main + dependencies: + argon2-cffi: '>=19.2.0' + bcrypt: '>=3.1.0' + cryptography: '' + python: '>=3.9' + hash: + md5: fba64c154edb7d7935af0d46d97ff536 + sha256: 2adfe01cdab93c39c4d8dfe3de74a31ae6fded21213f26925208ce6053cea93d + manager: conda + name: passlib + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/passlib-1.7.4-pyhd8ed1ab_2.conda + version: 1.7.4 + - category: main + dependencies: + argon2-cffi: '>=19.2.0' + bcrypt: '>=3.1.0' + cryptography: '' + python: '>=3.9' + hash: + md5: fba64c154edb7d7935af0d46d97ff536 + sha256: 2adfe01cdab93c39c4d8dfe3de74a31ae6fded21213f26925208ce6053cea93d + manager: conda + name: passlib + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/passlib-1.7.4-pyhd8ed1ab_2.conda + version: 1.7.4 + - category: main + dependencies: + libgcc-ng: '>=9.3.0' hash: md5: 4c1bbbec45149a186b915c67d086ed3b sha256: fc30d1b643c35d82abd294cde6b34f7b9e952856c0386f4f069c3a2b7feb28dd @@ -10046,6 +10928,64 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/progressbar2-4.5.0-pyhd8ed1ab_1.conda version: 4.5.0 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libgcc: '>=13' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + hash: + md5: 27089f71e28d01bcc070460d822d5acb + sha256: 3e06dcdd3ec2e73fb456d5c2fdf9c8829d7f70c15d724f9920a24276a0a1d6b5 + manager: conda + name: protobuf + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/protobuf-4.25.3-py311hbffca5d_1.conda + version: 4.25.3 + - category: main + dependencies: + __osx: '>=10.13' + libabseil: '>=20240116.2,<20240117.0a0' + libcxx: '>=17' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + hash: + md5: 56584ed8b577c2e07f5c57a2bd3f5912 + sha256: d33640b917b5f2b0184142ece692a764de840b7f2fa44d211fd56c1170c57e7b + manager: conda + name: protobuf + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/protobuf-4.25.3-py311h6b31176_1.conda + version: 4.25.3 + - category: main + dependencies: + __osx: '>=11.0' + libabseil: '>=20240116.2,<20240117.0a0' + libcxx: '>=17' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + hash: + md5: dacdcae7ce1a0d2f10351fb7b406bf7e + sha256: 4c7221018c88b9979fd25f97369d4635dee16fc42dd6a9079362edf97eaa5a48 + manager: conda + name: protobuf + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-4.25.3-py311hd7a3543_1.conda + version: 4.25.3 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -10144,6 +11084,45 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda version: 0.7.0 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 6cffc01a8288495554ad5c5935e09d25 + sha256: db5e0d1d214ac2253556cbb1e242900ed75a3877eac1c4a68625ba45830a5942 + manager: conda + name: publicsuffix2 + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/publicsuffix2-2.20191221-pyhd8ed1ab_1.conda + version: '2.20191221' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 6cffc01a8288495554ad5c5935e09d25 + sha256: db5e0d1d214ac2253556cbb1e242900ed75a3877eac1c4a68625ba45830a5942 + manager: conda + name: publicsuffix2 + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/publicsuffix2-2.20191221-pyhd8ed1ab_1.conda + version: '2.20191221' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 6cffc01a8288495554ad5c5935e09d25 + sha256: db5e0d1d214ac2253556cbb1e242900ed75a3877eac1c4a68625ba45830a5942 + manager: conda + name: publicsuffix2 + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/publicsuffix2-2.20191221-pyhd8ed1ab_1.conda + version: '2.20191221' - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -10245,6 +11224,42 @@ package: url: https://conda.anaconda.org/conda-forge/osx-arm64/py-rattler-0.9.0-py311h8be0713_0.conda version: 0.9.0 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 09bb17ed307ad6ab2fd78d32372fdd4e + sha256: d06051df66e9ab753683d7423fcef873d78bb0c33bd112c3d5be66d529eddf06 + manager: conda + name: pyasn1 + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda + version: 0.6.1 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 09bb17ed307ad6ab2fd78d32372fdd4e + sha256: d06051df66e9ab753683d7423fcef873d78bb0c33bd112c3d5be66d529eddf06 + manager: conda + name: pyasn1 + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda + version: 0.6.1 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: 09bb17ed307ad6ab2fd78d32372fdd4e + sha256: d06051df66e9ab753683d7423fcef873d78bb0c33bd112c3d5be66d529eddf06 + manager: conda + name: pyasn1 + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda + version: 0.6.1 - category: main dependencies: {} hash: @@ -10783,6 +11798,52 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.10.1-pyhd8ed1ab_0.conda version: 2.10.1 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 869b15d0e4c7f7449c290336760a653f + sha256: 27ae4676bb24a09c7b571a032f5b6f2d0f7e990e4171b47e4ce2cdf8b4592000 + manager: conda + name: pylsqpack + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/pylsqpack-0.3.19-py311h9ecbd09_0.conda + version: 0.3.19 + - category: main + dependencies: + __osx: '>=10.13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: ecc356b8988200c207571a5eea534f1e + sha256: 5c48e59a7db29829901ddf05c457a8122353d222ef48815b27b326521ecd346c + manager: conda + name: pylsqpack + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/pylsqpack-0.3.19-py311h4d7f069_0.conda + version: 0.3.19 + - category: main + dependencies: + __osx: '>=11.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: d1ea0c8738fc40b84477bfbae265b415 + sha256: a67074ad65ae4a5d7e3bbf26a4e486b296fee9910573950f178600e63ac73859 + manager: conda + name: pylsqpack + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/pylsqpack-0.3.19-py311h917b07b_0.conda + version: 0.3.19 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -10933,6 +11994,195 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/pynamodb-6.0.2-pyhd8ed1ab_0.conda version: 6.0.2 + - category: main + dependencies: + __osx: '>=10.13' + libffi: '>=3.4,<4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + hash: + md5: 3b2f520d27fa7cf9c6c73fb43c69a321 + sha256: 7cc9dd5c836631c733173c88187231bfc0438135e0ddf94e866e45b3d10592bd + manager: conda + name: pyobjc-core + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-11.0-py311hfbc4093_0.conda + version: '11.0' + - category: main + dependencies: + __osx: '>=11.0' + libffi: '>=3.4,<4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + hash: + md5: cc865b09e7a02328840b163fb8856731 + sha256: 7eb9c40a460ea769f024aaf45dae9fde7ca41137ca82154c50c8aead8a32ff88 + manager: conda + name: pyobjc-core + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.0-py311hab620ed_0.conda + version: '11.0' + - category: main + dependencies: + __osx: '>=10.13' + libffi: '>=3.4,<4.0a0' + pyobjc-core: 11.0.* + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: d16654f6b3f602bb0acab446c55bcafb + sha256: 94e00e4c9b5c5d8b2374321a0f908b7812b06ac8c9cb99242ddaa4ea0091f0be + manager: conda + name: pyobjc-framework-cocoa + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-11.0-py311hfbc4093_0.conda + version: '11.0' + - category: main + dependencies: + __osx: '>=11.0' + libffi: '>=3.4,<4.0a0' + pyobjc-core: 11.0.* + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 39da4013010bd559600f775ebf6a5915 + sha256: 33635759c626103696963a4d439f01cc534fe94c318ce5a14c7b9ddbe8dfb78c + manager: conda + name: pyobjc-framework-cocoa + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.0-py311hab620ed_0.conda + version: '11.0' + - category: main + dependencies: + cryptography: '>=38.0.0,<41' + python: '>=3.6' + hash: + md5: 0b34aa3ab7e7ccb1765a03dd9ed29938 + sha256: 458428cb867f70f2af2a4ed59d382291ea3eb3f10490196070a15d1d71d5432a + manager: conda + name: pyopenssl + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/pyopenssl-23.1.1-pyhd8ed1ab_0.conda + version: 23.1.1 + - category: main + dependencies: + cryptography: '>=38.0.0,<41' + python: '>=3.6' + hash: + md5: 0b34aa3ab7e7ccb1765a03dd9ed29938 + sha256: 458428cb867f70f2af2a4ed59d382291ea3eb3f10490196070a15d1d71d5432a + manager: conda + name: pyopenssl + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/pyopenssl-23.1.1-pyhd8ed1ab_0.conda + version: 23.1.1 + - category: main + dependencies: + cryptography: '>=38.0.0,<41' + python: '>=3.6' + hash: + md5: 0b34aa3ab7e7ccb1765a03dd9ed29938 + sha256: 458428cb867f70f2af2a4ed59d382291ea3eb3f10490196070a15d1d71d5432a + manager: conda + name: pyopenssl + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/pyopenssl-23.1.1-pyhd8ed1ab_0.conda + version: 23.1.1 + - category: main + dependencies: + python: '>=3.6' + hash: + md5: 4d91352a50949d049cf9714c8563d433 + sha256: 8714a83f1aeac278b3eb33c7cb880c95c9a5924e7a5feeb9e87e7d0837afa085 + manager: conda + name: pyparsing + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda + version: 3.1.4 + - category: main + dependencies: + python: '>=3.6' + hash: + md5: 4d91352a50949d049cf9714c8563d433 + sha256: 8714a83f1aeac278b3eb33c7cb880c95c9a5924e7a5feeb9e87e7d0837afa085 + manager: conda + name: pyparsing + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda + version: 3.1.4 + - category: main + dependencies: + python: '>=3.6' + hash: + md5: 4d91352a50949d049cf9714c8563d433 + sha256: 8714a83f1aeac278b3eb33c7cb880c95c9a5924e7a5feeb9e87e7d0837afa085 + manager: conda + name: pyparsing + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda + version: 3.1.4 + - category: main + dependencies: + __linux: '' + python: '>=3.6' + xclip: '' + xsel: '' + hash: + md5: 2acdfb68ee42274329494c93fcf92ce6 + sha256: c4404821044f2fd2c33a1159d525368672d04e369d8c16b8cc8488a4a8bd5be5 + manager: conda + name: pyperclip + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/pyperclip-1.8.2-pyha804496_3.conda + version: 1.8.2 + - category: main + dependencies: + __osx: '' + pyobjc-framework-cocoa: '' + python: '>=3.6' + hash: + md5: aca7616a492c45b55b97d1b4e882ebea + sha256: 026d26b5e624de4b1f1c359224f57421b7ceaaecba4ffe265b65ef6d1253f4cb + manager: conda + name: pyperclip + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/pyperclip-1.8.2-pyh534df25_3.conda + version: 1.8.2 + - category: main + dependencies: + __osx: '' + pyobjc-framework-cocoa: '' + python: '>=3.6' + hash: + md5: aca7616a492c45b55b97d1b4e882ebea + sha256: 026d26b5e624de4b1f1c359224f57421b7ceaaecba4ffe265b65ef6d1253f4cb + manager: conda + name: pyperclip + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/pyperclip-1.8.2-pyh534df25_3.conda + version: 1.8.2 - category: main dependencies: python: '>=3.9' @@ -11248,6 +12498,51 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda version: 3.6.1 + - category: main + dependencies: + psutil: '' + pytest: '>=2.8' + python: '>=3.9' + hash: + md5: 8c469458938c01ef5c9cd1357bbc62ac + sha256: 8bc8e82da035fe5dc1966c9d5f5d9d290f5c77075e2b3b6873e0b270e8c61853 + manager: conda + name: pytest-xprocess + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + version: 1.0.2 + - category: main + dependencies: + psutil: '' + pytest: '>=2.8' + python: '>=3.9' + hash: + md5: 8c469458938c01ef5c9cd1357bbc62ac + sha256: 8bc8e82da035fe5dc1966c9d5f5d9d290f5c77075e2b3b6873e0b270e8c61853 + manager: conda + name: pytest-xprocess + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + version: 1.0.2 + - category: main + dependencies: + psutil: '' + pytest: '>=2.8' + python: '>=3.9' + hash: + md5: 8c469458938c01ef5c9cd1357bbc62ac + sha256: 8bc8e82da035fe5dc1966c9d5f5d9d290f5c77075e2b3b6873e0b270e8c61853 + manager: conda + name: pytest-xprocess + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/pytest-xprocess-1.0.2-pyhd8ed1ab_1.conda + version: 1.0.2 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -11264,6 +12559,7 @@ package: libzlib: '>=1.3.1,<2.0a0' ncurses: '>=6.5,<7.0a0' openssl: '>=3.5.0,<4.0a0' + pip: '' readline: '>=8.2,<9.0a0' tk: '>=8.6.13,<8.7.0a0' tzdata: '' @@ -11288,6 +12584,7 @@ package: libzlib: '>=1.3.1,<2.0a0' ncurses: '>=6.5,<7.0a0' openssl: '>=3.5.0,<4.0a0' + pip: '' readline: '>=8.2,<9.0a0' tk: '>=8.6.13,<8.7.0a0' tzdata: '' @@ -11312,6 +12609,7 @@ package: libzlib: '>=1.3.1,<2.0a0' ncurses: '>=6.5,<7.0a0' openssl: '>=3.5.0,<4.0a0' + pip: '' readline: '>=8.2,<9.0a0' tk: '>=8.6.13,<8.7.0a0' tzdata: '' @@ -11650,6 +12948,45 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/python-libarchive-c-5.3-pyhe01879c_0.conda version: '5.3' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: a28c984e0429aff3ab7386f7de56de6f + sha256: 1b03678d145b1675b757cba165a0d9803885807792f7eb4495e48a38858c3cca + manager: conda + name: python-multipart + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + version: 0.0.20 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: a28c984e0429aff3ab7386f7de56de6f + sha256: 1b03678d145b1675b757cba165a0d9803885807792f7eb4495e48a38858c3cca + manager: conda + name: python-multipart + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + version: 0.0.20 + - category: main + dependencies: + python: '>=3.9' + hash: + md5: a28c984e0429aff3ab7386f7de56de6f + sha256: 1b03678d145b1675b757cba165a0d9803885807792f7eb4495e48a38858c3cca + manager: conda + name: python-multipart + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda + version: 0.0.20 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -12402,6 +13739,54 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/rich-14.0.0-pyh29332c3_0.conda version: 14.0.0 + - category: main + dependencies: + click: '>=8.1.7' + python: '' + rich: '>=13.7.1' + typing_extensions: '>=4.12.2' + hash: + md5: 4ba15ae9388b67d09782798347481f69 + sha256: e558f8c254a9ff9164d069110da162fc79497d70c60f2c09a5d3d0d7101c5628 + manager: conda + name: rich-toolkit + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.11.3-pyh29332c3_0.conda + version: 0.11.3 + - category: main + dependencies: + click: '>=8.1.7' + python: '>=3.9' + rich: '>=13.7.1' + typing_extensions: '>=4.12.2' + hash: + md5: 4ba15ae9388b67d09782798347481f69 + sha256: e558f8c254a9ff9164d069110da162fc79497d70c60f2c09a5d3d0d7101c5628 + manager: conda + name: rich-toolkit + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.11.3-pyh29332c3_0.conda + version: 0.11.3 + - category: main + dependencies: + click: '>=8.1.7' + python: '>=3.9' + rich: '>=13.7.1' + typing_extensions: '>=4.12.2' + hash: + md5: 4ba15ae9388b67d09782798347481f69 + sha256: e558f8c254a9ff9164d069110da162fc79497d70c60f2c09a5d3d0d7101c5628 + manager: conda + name: rich-toolkit + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.11.3-pyh29332c3_0.conda + version: 0.11.3 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -12487,53 +13872,53 @@ package: version: 0.25.1 - category: main dependencies: - __glibc: '>=2.17,<3.0.a0' - libgcc: '>=13' + libgcc-ng: '>=12' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* ruamel.yaml.clib: '>=0.1.2' + setuptools: '' hash: - md5: a3188715e28c25f1404b84c702e6fdf4 - sha256: 11922e4b99d1d16a0ec18daccee4a1b83243000022d4e67ab957e15f3b4aa644 + md5: 59518a18bdf00ee4379797459d2c76ee + sha256: b33e0e83f834b948ce5c77c0df727b6cd027a388c3b4e4498b34b83751ba7c05 manager: conda name: ruamel.yaml optional: false platform: linux-64 url: - https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.10-py311h9ecbd09_0.conda - version: 0.18.10 + https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.17.40-py311h459d7ec_0.conda + version: 0.17.40 - category: main dependencies: - __osx: '>=10.13' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* ruamel.yaml.clib: '>=0.1.2' + setuptools: '' hash: - md5: 7f11b35a61a8c90eea12a917b52895b9 - sha256: a623d6fdcaf22a6173b79dd167ee67b7dadf31f2f80081e70f3b2b8a84948299 + md5: 4796cc1d45c88ace8f5bb7950167a568 + sha256: 0260c295cdfae417f107fe2fae66bae4eb260195885d81d8b1cd4fc6d81c849b manager: conda name: ruamel.yaml optional: false platform: osx-64 url: - https://conda.anaconda.org/conda-forge/osx-64/ruamel.yaml-0.18.10-py311h4d7f069_0.conda - version: 0.18.10 + https://conda.anaconda.org/conda-forge/osx-64/ruamel.yaml-0.17.40-py311he705e18_0.conda + version: 0.17.40 - category: main dependencies: - __osx: '>=11.0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* ruamel.yaml.clib: '>=0.1.2' + setuptools: '' hash: - md5: 99b00011b5162250638eae2ea0b033e8 - sha256: 88ec95e9631b1eeec551455320f87e87cc3b8370379bc48aabc7eb550288c4c8 + md5: 640b6933c680860877b32a4c11076ab9 + sha256: bf44774cbd03eb8d396a078b4af1b194f8600f8ca7ea7ca0e3d61dc16b400cb4 manager: conda name: ruamel.yaml optional: false platform: osx-arm64 url: - https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.10-py311h917b07b_0.conda - version: 0.18.10 + https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.17.40-py311h05b510d_0.conda + version: 0.17.40 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -13222,33 +14607,78 @@ package: manager: conda name: soupsieve optional: false - platform: linux-64 + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.7-pyhd8ed1ab_0.conda + version: '2.7' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: fb32097c717486aa34b38a9db57eb49e + sha256: 7518506cce9a736042132f307b3f4abce63bf076f5fb07c1f4e506c0b214295a + manager: conda + name: soupsieve + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.7-pyhd8ed1ab_0.conda + version: '2.7' + - category: main + dependencies: + python: '>=3.9' + hash: + md5: fb32097c717486aa34b38a9db57eb49e + sha256: 7518506cce9a736042132f307b3f4abce63bf076f5fb07c1f4e506c0b214295a + manager: conda + name: soupsieve + optional: false + platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.7-pyhd8ed1ab_0.conda version: '2.7' - category: main dependencies: + anyio: '>=3.6.2,<5' + python: '' + typing_extensions: '>=3.10.0' + hash: + md5: 36ec80c2b37e52760ab41be7c2bd1fd3 + sha256: d41b9b2719a2a0176930df21d7fec7b758058e7fafd53dc900b5706cd627fa3a + manager: conda + name: starlette + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/starlette-0.46.2-pyh81abbef_0.conda + version: 0.46.2 + - category: main + dependencies: + anyio: '>=3.6.2,<5' python: '>=3.9' + typing_extensions: '>=3.10.0' hash: - md5: fb32097c717486aa34b38a9db57eb49e - sha256: 7518506cce9a736042132f307b3f4abce63bf076f5fb07c1f4e506c0b214295a + md5: 36ec80c2b37e52760ab41be7c2bd1fd3 + sha256: d41b9b2719a2a0176930df21d7fec7b758058e7fafd53dc900b5706cd627fa3a manager: conda - name: soupsieve + name: starlette optional: false platform: osx-64 - url: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.7-pyhd8ed1ab_0.conda - version: '2.7' + url: + https://conda.anaconda.org/conda-forge/noarch/starlette-0.46.2-pyh81abbef_0.conda + version: 0.46.2 - category: main dependencies: + anyio: '>=3.6.2,<5' python: '>=3.9' + typing_extensions: '>=3.10.0' hash: - md5: fb32097c717486aa34b38a9db57eb49e - sha256: 7518506cce9a736042132f307b3f4abce63bf076f5fb07c1f4e506c0b214295a + md5: 36ec80c2b37e52760ab41be7c2bd1fd3 + sha256: d41b9b2719a2a0176930df21d7fec7b758058e7fafd53dc900b5706cd627fa3a manager: conda - name: soupsieve + name: starlette optional: false platform: osx-arm64 - url: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.7-pyhd8ed1ab_0.conda - version: '2.7' + url: + https://conda.anaconda.org/conda-forge/noarch/starlette-0.46.2-pyh81abbef_0.conda + version: 0.46.2 - category: main dependencies: python: '>=3.9' @@ -14359,6 +15789,163 @@ package: platform: osx-arm64 url: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.7.7-hb4c02be_0.conda version: 0.7.7 + - category: main + dependencies: + __unix: '' + click: '>=7.0' + h11: '>=0.8' + python: '>=3.9' + typing_extensions: '>=4.0' + hash: + md5: 7e9f164470d693a5d2537c6b2ce1d9ea + sha256: d6c504920400354a89e597c5d355288e77481d638cca0489fea3530167895f15 + manager: conda + name: uvicorn + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.34.2-pyh31011fe_0.conda + version: 0.34.2 + - category: main + dependencies: + __unix: '' + click: '>=7.0' + h11: '>=0.8' + python: '>=3.9' + typing_extensions: '>=4.0' + hash: + md5: 7e9f164470d693a5d2537c6b2ce1d9ea + sha256: d6c504920400354a89e597c5d355288e77481d638cca0489fea3530167895f15 + manager: conda + name: uvicorn + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.34.2-pyh31011fe_0.conda + version: 0.34.2 + - category: main + dependencies: + __unix: '' + click: '>=7.0' + h11: '>=0.8' + python: '>=3.9' + typing_extensions: '>=4.0' + hash: + md5: 7e9f164470d693a5d2537c6b2ce1d9ea + sha256: d6c504920400354a89e597c5d355288e77481d638cca0489fea3530167895f15 + manager: conda + name: uvicorn + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.34.2-pyh31011fe_0.conda + version: 0.34.2 + - category: main + dependencies: + __unix: '' + httptools: '>=0.6.3' + python-dotenv: '>=0.13' + pyyaml: '>=5.1' + uvicorn: 0.34.2 + uvloop: '>=0.14.0,!=0.15.0,!=0.15.1' + watchfiles: '>=0.13' + websockets: '>=10.4' + hash: + md5: 62676324fa57eb76b542a6a2e85d35e2 + sha256: c323cc4986f4fea91dedbee68dce8071cade48be2e71cf9c575faf3f3ccc42a9 + manager: conda + name: uvicorn-standard + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.34.2-h31011fe_0.conda + version: 0.34.2 + - category: main + dependencies: + __unix: '' + httptools: '>=0.6.3' + python-dotenv: '>=0.13' + pyyaml: '>=5.1' + uvicorn: 0.34.2 + uvloop: '>=0.14.0,!=0.15.0,!=0.15.1' + watchfiles: '>=0.13' + websockets: '>=10.4' + hash: + md5: 62676324fa57eb76b542a6a2e85d35e2 + sha256: c323cc4986f4fea91dedbee68dce8071cade48be2e71cf9c575faf3f3ccc42a9 + manager: conda + name: uvicorn-standard + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.34.2-h31011fe_0.conda + version: 0.34.2 + - category: main + dependencies: + __unix: '' + httptools: '>=0.6.3' + python-dotenv: '>=0.13' + pyyaml: '>=5.1' + uvicorn: 0.34.2 + uvloop: '>=0.14.0,!=0.15.0,!=0.15.1' + watchfiles: '>=0.13' + websockets: '>=10.4' + hash: + md5: 62676324fa57eb76b542a6a2e85d35e2 + sha256: c323cc4986f4fea91dedbee68dce8071cade48be2e71cf9c575faf3f3ccc42a9 + manager: conda + name: uvicorn-standard + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.34.2-h31011fe_0.conda + version: 0.34.2 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + libuv: '>=1.49.2,<2.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 66890e34ed6a9bd84f1c189043a928f8 + sha256: 9421eeb1e15b99985bb15dec9cf0f337d332106cea584a147449c91c389a4418 + manager: conda + name: uvloop + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.21.0-py311h9ecbd09_1.conda + version: 0.21.0 + - category: main + dependencies: + __osx: '>=10.13' + libuv: '>=1.49.2,<2.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: fdf82f7e7a4561819bcbfca2c2e7031c + sha256: cc0fe2730e413fc449cb3d0b6b48576dd53283b099f182b844f34f97b459ac59 + manager: conda + name: uvloop + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/uvloop-0.21.0-py311h1314207_1.conda + version: 0.21.0 + - category: main + dependencies: + __osx: '>=11.0' + libuv: '>=1.49.2,<2.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: bc9ca85e86e305b58432c4791b732ae6 + sha256: f42e2ca33beedef252d234d3aac7642432bf8545a6d37c11e58a69f6aee36898 + manager: conda + name: uvloop + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/uvloop-0.21.0-py311hae2e1ce_1.conda + version: 0.21.0 - category: main dependencies: distlib: '>=0.3.7,<1' @@ -14452,6 +16039,55 @@ package: url: https://conda.anaconda.org/conda-forge/noarch/vsts-python-api-0.1.25-pyhd8ed1ab_2.conda version: 0.1.25 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + anyio: '>=3.0.0' + libgcc: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 896e5c200fddca2b878a0113a58345f9 + sha256: b639615b2b943dd2e01296e5f99af62f92453c5b93568fb4509cd85fc4139b93 + manager: conda + name: watchfiles + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.0.5-py311h9e33e62_0.conda + version: 1.0.5 + - category: main + dependencies: + __osx: '>=10.13' + anyio: '>=3.0.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: c2741ddc8d98730e1b0940fd0179216a + sha256: dca0ef5b3d6b642dc1e21a7845a6ac5fcb590e53cf0fb5219d48111f37b5d30d + manager: conda + name: watchfiles + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/watchfiles-1.0.5-py311h3b9c2be_0.conda + version: 1.0.5 + - category: main + dependencies: + __osx: '>=11.0' + anyio: '>=3.0.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 5f46b36eba293a884fe980d0c5b37606 + sha256: 2cfb284d572c6b623ac22053ba6fc082d20c9d86c54c6b2697b41ba69c6859ea + manager: conda + name: watchfiles + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/watchfiles-1.0.5-py311h3ff9189_0.conda + version: 1.0.5 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -14468,6 +16104,91 @@ package: platform: linux-64 url: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.23.1-h3e06ad9_1.conda version: 1.23.1 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 208bf8e44ef767b25a51ba1beef0613c + sha256: 3284007ea6eaadef71e475e93124e10362d0a3376e32d2d7023bfef01ee96a66 + manager: conda + name: websockets + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py311h9ecbd09_0.conda + version: 15.0.1 + - category: main + dependencies: + __osx: '>=10.13' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 706ed76fe488bd51f542c1f68347059c + sha256: 785eee203cbf8f536d7f840c6ca4ea9883f8a86d62f29e8711c5a0bcd0bf4148 + manager: conda + name: websockets + optional: false + platform: osx-64 + url: + https://conda.anaconda.org/conda-forge/osx-64/websockets-15.0.1-py311h4d7f069_0.conda + version: 15.0.1 + - category: main + dependencies: + __osx: '>=11.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + hash: + md5: 603430a5c35ae5c87310e096e0868551 + sha256: 9452f7288c50c3fef9f2ccc850a2b5e8fbe6148a7ec36f16939d0734eb728f1e + manager: conda + name: websockets + optional: false + platform: osx-arm64 + url: + https://conda.anaconda.org/conda-forge/osx-arm64/websockets-15.0.1-py311h917b07b_0.conda + version: 15.0.1 + - category: main + dependencies: + markupsafe: '>=2.1.1' + python: '>=3.9' + hash: + md5: 0a9b57c159d56b508613cc39022c1b9e + sha256: cd9a603beae0b237be7d9dfae8ae0b36ad62666ac4bb073969bce7da6f55157c + manager: conda + name: werkzeug + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.3-pyhd8ed1ab_1.conda + version: 3.1.3 + - category: main + dependencies: + markupsafe: '>=2.1.1' + python: '>=3.9' + hash: + md5: 0a9b57c159d56b508613cc39022c1b9e + sha256: cd9a603beae0b237be7d9dfae8ae0b36ad62666ac4bb073969bce7da6f55157c + manager: conda + name: werkzeug + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.3-pyhd8ed1ab_1.conda + version: 3.1.3 + - category: main + dependencies: + markupsafe: '>=2.1.1' + python: '>=3.9' + hash: + md5: 0a9b57c159d56b508613cc39022c1b9e + sha256: cd9a603beae0b237be7d9dfae8ae0b36ad62666ac4bb073969bce7da6f55157c + manager: conda + name: werkzeug + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.3-pyhd8ed1ab_1.conda + version: 3.1.3 - category: main dependencies: libgcc-ng: '>=12' @@ -14598,6 +16319,45 @@ package: url: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.2-py311h917b07b_0.conda version: 1.17.2 + - category: main + dependencies: + h11: '>=0.9.0,<1.0' + python: '>=3.9' + hash: + md5: 2c7536a04d9c21e1dd05bd4a3b1e3a39 + sha256: 37b89ef8dc05b6e06c73b60d0bc130f81d1be3a8c8eed5807c27984484ec175e + manager: conda + name: wsproto + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/noarch/wsproto-1.2.0-pyhd8ed1ab_1.conda + version: 1.2.0 + - category: main + dependencies: + h11: '>=0.9.0,<1.0' + python: '>=3.9' + hash: + md5: 2c7536a04d9c21e1dd05bd4a3b1e3a39 + sha256: 37b89ef8dc05b6e06c73b60d0bc130f81d1be3a8c8eed5807c27984484ec175e + manager: conda + name: wsproto + optional: false + platform: osx-64 + url: https://conda.anaconda.org/conda-forge/noarch/wsproto-1.2.0-pyhd8ed1ab_1.conda + version: 1.2.0 + - category: main + dependencies: + h11: '>=0.9.0,<1.0' + python: '>=3.9' + hash: + md5: 2c7536a04d9c21e1dd05bd4a3b1e3a39 + sha256: 37b89ef8dc05b6e06c73b60d0bc130f81d1be3a8c8eed5807c27984484ec175e + manager: conda + name: wsproto + optional: false + platform: osx-arm64 + url: https://conda.anaconda.org/conda-forge/noarch/wsproto-1.2.0-pyhd8ed1ab_1.conda + version: 1.2.0 - category: main dependencies: python: '>=3.9' @@ -14665,6 +16425,21 @@ package: url: https://conda.anaconda.org/conda-forge/osx-arm64/xattr-1.1.0-py311h460d6c5_1.conda version: 1.1.0 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + xorg-libx11: '>=1.8.10,<2.0a0' + xorg-libxmu: '>=1.2.1,<2.0a0' + hash: + md5: 60617f7654d84993ff0ccdfc55209b69 + sha256: 7795c9b28a643a7279e6008dfe625cda3c8ee8fa6e178d390d7e213fe4291a5d + manager: conda + name: xclip + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/xclip-0.13-hb9d3cd8_4.conda + version: '0.13' - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -14867,6 +16642,23 @@ package: url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda version: 1.1.5 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + xorg-libx11: '>=1.8.10,<2.0a0' + xorg-libxext: '>=1.3.6,<2.0a0' + xorg-libxt: '>=1.3.0,<2.0a0' + hash: + md5: f35a9a2da717ade815ffa70c0e8bdfbd + sha256: 467cba5106e628068487dcbc2ba2dbd6a434e75d752eaf0895086e9fe65e6a8d + manager: conda + name: xorg-libxmu + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/xorg-libxmu-1.2.1-hb9d3cd8_1.conda + version: 1.2.1 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -14899,6 +16691,23 @@ package: url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda version: 0.9.12 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + xorg-libice: '>=1.1.1,<2.0a0' + xorg-libsm: '>=1.2.4,<2.0a0' + xorg-libx11: '>=1.8.10,<2.0a0' + hash: + md5: 279b0de5f6ba95457190a1c459a64e31 + sha256: a8afba4a55b7b530eb5c8ad89737d60d60bc151a03fbef7a2182461256953f0e + manager: conda + name: xorg-libxt + optional: false + platform: linux-64 + url: + https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda + version: 1.3.1 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' @@ -14916,6 +16725,20 @@ package: url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda version: 1.2.5 + - category: main + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc: '>=13' + xorg-libx11: '>=1.8.10,<2.0a0' + hash: + md5: 16566b426488305d7fc8b084d5db94e9 + sha256: e13cab6260ccf8619547fea51b403301ea9ed0f667fa7e9e4f39c7d016d8caa4 + manager: conda + name: xsel + optional: false + platform: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/xsel-1.2.1-hb9d3cd8_6.conda + version: 1.2.1 - category: main dependencies: libgcc-ng: '>=9.4.0' @@ -15104,53 +16927,50 @@ package: version: 1.3.1 - category: main dependencies: - __glibc: '>=2.17,<3.0.a0' - cffi: '>=1.11' - libgcc: '>=13' + cffi: '>=1.8' + libgcc-ng: '>=12' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: ca02de88df1cc3cfc8f24766ff50cb3c - sha256: 76d28240cc9fa0c3cb2cde750ecaf98716ce397afaf1ce90f8d18f5f43a122f1 + md5: 056b3271f46abaa4673c8c6783283a07 + sha256: 8aac43cc4fbdcc420fe8a22c764b67f6ac9168b103bfd10d79a82b748304ddf6 manager: conda name: zstandard optional: false platform: linux-64 url: - https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py311h9ecbd09_2.conda - version: 0.23.0 + https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.19.0-py311hd4cff14_0.tar.bz2 + version: 0.19.0 - category: main dependencies: - __osx: '>=10.13' - cffi: '>=1.11' + cffi: '>=1.8' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: 2712198232a6fcc673f9eef62fce85d5 - sha256: 72ab78bbde3396ffb2b81a2513f48a27c128ddc4e06a8d3dbcfa790479deab40 + md5: 96e4e2aa960398abbe5c4a6cf22269b8 + sha256: b470229c05df4d96d27904def00660b5dfa7ad57bf2b9dfd826325233f9e8510 manager: conda name: zstandard optional: false platform: osx-64 url: - https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py311h4d7f069_2.conda - version: 0.23.0 + https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.19.0-py311h5547dcb_0.tar.bz2 + version: 0.19.0 - category: main dependencies: - __osx: '>=11.0' - cffi: '>=1.11' + cffi: '>=1.8' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* hash: - md5: 9fd87c9aae7db68b4a3427886b5f3eea - sha256: 7c7f7e24ff49dc6ecb804373bedca663d3c24d57cac55524be8c83da90313928 + md5: ece21cb47a93c985aa4b44219c4c8c8b + sha256: 43eaee70cd406468d96d1643b75d16e0da3955a9c1d37056767134b91b61d515 manager: conda name: zstandard optional: false platform: osx-arm64 url: - https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311h917b07b_2.conda - version: 0.23.0 + https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.19.0-py311he2be06e_0.tar.bz2 + version: 0.19.0 - category: main dependencies: __glibc: '>=2.17,<3.0.a0' diff --git a/conda_forge_tick/auto_tick.py b/conda_forge_tick/auto_tick.py index 8141b4e7a..5882e6bd2 100644 --- a/conda_forge_tick/auto_tick.py +++ b/conda_forge_tick/auto_tick.py @@ -76,7 +76,6 @@ logger = logging.getLogger(__name__) -BOT_HOME_DIR: str = os.getcwd() START_TIME = None TIMEOUT = int(os.environ.get("TIMEOUT", 600)) @@ -545,9 +544,6 @@ def run( pr_json: dict The PR json object for recreating the PR as needed """ - # sometimes we get weird directory issues so make sure we reset - os.chdir(BOT_HOME_DIR) - migrator_name = get_migrator_name(migrator) is_version_migration = isinstance(migrator, Version) _increment_pre_pr_migrator_attempt( @@ -993,6 +989,7 @@ def _is_migrator_done(_mg_start, good_prs, time_per, pr_limit, tried_prs): def _run_migrator(migrator, mctx, temp, time_per, git_backend: GitPlatformBackend): _mg_start = time.time() + initial_working_dir = os.getcwd() migrator_name = get_migrator_name(migrator) @@ -1136,7 +1133,7 @@ def _run_migrator(migrator, mctx, temp, time_per, git_backend: GitPlatformBacken gc.collect() # sometimes we get weird directory issues so make sure we reset - os.chdir(BOT_HOME_DIR) + os.chdir(initial_working_dir) # Write graph partially through dump_graph(mctx.graph) diff --git a/conda_forge_tick/deploy.py b/conda_forge_tick/deploy.py index b147c68e4..1caf5fabf 100644 --- a/conda_forge_tick/deploy.py +++ b/conda_forge_tick/deploy.py @@ -201,6 +201,7 @@ def _get_pth_commit_message(pth): def _reset_and_restore_file(pth): subprocess.run(["git", "reset", "--", pth], capture_output=True, text=True) subprocess.run(["git", "restore", "--", pth], capture_output=True, text=True) + subprocess.run(["git", "clean", "-f", "--", pth], capture_output=True, text=True) def deploy(ctx: CliContext, dirs_to_deploy: list[str] = None): diff --git a/conda_forge_tick/feedstock_parser.py b/conda_forge_tick/feedstock_parser.py index 30123461e..b8a1996d5 100644 --- a/conda_forge_tick/feedstock_parser.py +++ b/conda_forge_tick/feedstock_parser.py @@ -19,7 +19,11 @@ ) from requests.models import Response -from conda_forge_tick.settings import ENV_CONDA_FORGE_ORG, settings +from conda_forge_tick.settings import ( + ENV_CONDA_FORGE_ORG, + ENV_GRAPH_GITHUB_BACKEND_REPO, + settings, +) if typing.TYPE_CHECKING: from mypy_extensions import TestTypedDict @@ -705,6 +709,8 @@ def load_feedstock_containerized( extra_container_args=[ "-e", f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", ], ) diff --git a/conda_forge_tick/lazy_json_backends.py b/conda_forge_tick/lazy_json_backends.py index 6b0dd2ece..27e0eea9f 100644 --- a/conda_forge_tick/lazy_json_backends.py +++ b/conda_forge_tick/lazy_json_backends.py @@ -204,7 +204,7 @@ class GithubLazyJsonBackend(LazyJsonBackend): _n_requests = 0 def __init__(self) -> None: - self.base_url = settings().graph_github_backend_raw_base_url + self._base_url = settings().graph_github_backend_raw_base_url @property def base_url(self) -> str: diff --git a/conda_forge_tick/migration_runner.py b/conda_forge_tick/migration_runner.py index 99014b212..5e34e88eb 100644 --- a/conda_forge_tick/migration_runner.py +++ b/conda_forge_tick/migration_runner.py @@ -19,6 +19,11 @@ from conda_forge_tick.contexts import ClonedFeedstockContext from conda_forge_tick.lazy_json_backends import LazyJson, dumps +from conda_forge_tick.settings import ( + ENV_CONDA_FORGE_ORG, + ENV_GRAPH_GITHUB_BACKEND_REPO, + settings, +) logger = logging.getLogger(__name__) @@ -175,7 +180,14 @@ def run_migration_containerized( if isinstance(node_attrs, LazyJson) else dumps(node_attrs) ), - extra_container_args=["-e", "RUN_URL"], + extra_container_args=[ + "-e", + "RUN_URL", + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) sync_dirs( @@ -232,8 +244,11 @@ def run_migration_local( - pr_title: The PR title for the migration. - pr_body: The PR body for the migration. """ - # it would be better if we don't re-instantiate ClonedFeedstockContext ourselves and let - # FeedstockContext.reserve_clone_directory be the only way to create a ClonedFeedstockContext + # Instead of mimicking the ClonedFeedstockContext which is already available in the call hierarchy of this function, + # we should instead pass the ClonedFeedstockContext object to this function. This would allow the following issue. + # POSSIBLE BUG: The feedstock_ctx object is mimicked and any attributes not listed here might have incorrect + # default values that were actually overridden. FOR EXAMPLE, DO NOT use the git_repo_owner attribute of the + # feedstock_ctx object below. Instead, refactor to make this function accept a ClonedFeedstockContext object. feedstock_ctx = ClonedFeedstockContext( feedstock_name=feedstock_name, attrs=node_attrs, diff --git a/conda_forge_tick/os_utils.py b/conda_forge_tick/os_utils.py index 31f93e3f1..e48a410d2 100644 --- a/conda_forge_tick/os_utils.py +++ b/conda_forge_tick/os_utils.py @@ -72,7 +72,8 @@ def clean_disk_space(ci_service: str = "github-actions") -> None: with tempfile.TemporaryDirectory() as tempdir, pushd(tempdir): with open("clean_disk.sh", "w") as f: if ci_service == "github-actions": - f.write("""\ + f.write( + """\ #!/bin/bash # clean disk space @@ -89,14 +90,16 @@ def clean_disk_space(ci_service: str = "github-actions") -> None: ; do sudo rsync --stats -a --delete /opt/empty_dir/ $d || true done - sudo apt-get purge -y -f firefox \ + # dpkg does not fail if the package is not installed + sudo dpkg --remove -y -f firefox \ google-chrome-stable \ microsoft-edge-stable sudo apt-get autoremove -y >& /dev/null sudo apt-get autoclean -y >& /dev/null sudo docker image prune --all --force df -h -""") +""" + ) else: raise ValueError(f"Unknown CI service: {ci_service}") diff --git a/conda_forge_tick/provide_source_code.py b/conda_forge_tick/provide_source_code.py index 92086c661..4ea70e918 100644 --- a/conda_forge_tick/provide_source_code.py +++ b/conda_forge_tick/provide_source_code.py @@ -14,6 +14,12 @@ ) from conda_forge_feedstock_ops.os_utils import chmod_plus_rwX, sync_dirs +from conda_forge_tick.settings import ( + ENV_CONDA_FORGE_ORG, + ENV_GRAPH_GITHUB_BACKEND_REPO, + settings, +) + logger = logging.getLogger(__name__) CONDA_BUILD_SPECIAL_KEYS = ( @@ -94,6 +100,12 @@ def provide_source_code_containerized(recipe_dir): args, mount_readonly=False, mount_dir=tmpdir, + extra_container_args=[ + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) yield tmp_source_dir diff --git a/conda_forge_tick/settings.py b/conda_forge_tick/settings.py index a09c6b642..470099073 100644 --- a/conda_forge_tick/settings.py +++ b/conda_forge_tick/settings.py @@ -15,6 +15,12 @@ Note: This must match the field name in the `BotSettings` class. """ +ENV_GRAPH_GITHUB_BACKEND_REPO = ENVIRONMENT_PREFIX + "GRAPH_GITHUB_BACKEND_REPO" +""" +The environment variable used to set the `graph_github_backend_repo` setting. +Note: This must match the field name in the `BotSettings` class. +""" + Fraction = Annotated[float, Field(ge=0.0, le=1.0)] @@ -45,6 +51,7 @@ class BotSettings(BaseSettings): ) """ The GitHub repository to deploy to. Default: "regro/cf-graph-countyfair". + If you change the field name, you must also update the `ENV_GRAPH_GITHUB_BACKEND_REPO` constant. """ graph_repo_default_branch: str = "master" @@ -58,7 +65,7 @@ def graph_github_backend_raw_base_url(self) -> str: The base URL for the GitHub raw view of the graph_github_backend_repo repository. Example: https://github.com/regro/cf-graph-countyfair/raw/master. """ - return f"https://github.com/{self.graph_github_backend_repo}/raw/{self.graph_repo_default_branch}" + return f"https://github.com/{self.graph_github_backend_repo}/raw/{self.graph_repo_default_branch}/" github_runner_debug: bool = Field(False, alias="RUNNER_DEBUG") """ diff --git a/conda_forge_tick/solver_checks.py b/conda_forge_tick/solver_checks.py index 8d7a889b5..5a8e08573 100644 --- a/conda_forge_tick/solver_checks.py +++ b/conda_forge_tick/solver_checks.py @@ -11,6 +11,12 @@ ) from conda_forge_feedstock_ops.os_utils import sync_dirs +from conda_forge_tick.settings import ( + ENV_CONDA_FORGE_ORG, + ENV_GRAPH_GITHUB_BACKEND_REPO, + settings, +) + logger = logging.getLogger(__name__) @@ -146,6 +152,12 @@ def _is_recipe_solvable_containerized( args, mount_readonly=True, mount_dir=tmp_feedstock_dir, + extra_container_args=[ + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) # When tempfile removes tempdir, it tries to reset permissions on subdirs. diff --git a/conda_forge_tick/update_recipe/version.py b/conda_forge_tick/update_recipe/version.py index d4c0b5006..701488f69 100644 --- a/conda_forge_tick/update_recipe/version.py +++ b/conda_forge_tick/update_recipe/version.py @@ -30,6 +30,11 @@ from conda_forge_tick.hashing import hash_url from conda_forge_tick.lazy_json_backends import loads from conda_forge_tick.recipe_parser import CONDA_SELECTOR, CondaMetaYAML +from conda_forge_tick.settings import ( + ENV_CONDA_FORGE_ORG, + ENV_GRAPH_GITHUB_BACKEND_REPO, + settings, +) from conda_forge_tick.url_transforms import gen_transformed_urls from conda_forge_tick.utils import sanitize_string @@ -665,6 +670,12 @@ def _update_version_feedstock_dir_containerized(feedstock_dir, version, hash_typ mount_readonly=False, mount_dir=tmpdir, json_loads=loads, + extra_container_args=[ + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) sync_dirs( diff --git a/conda_forge_tick/update_upstream_versions.py b/conda_forge_tick/update_upstream_versions.py index 26ae92895..26c68ff47 100644 --- a/conda_forge_tick/update_upstream_versions.py +++ b/conda_forge_tick/update_upstream_versions.py @@ -31,6 +31,8 @@ from conda_forge_tick.executors import executor from conda_forge_tick.lazy_json_backends import LazyJson, dumps from conda_forge_tick.settings import ( + ENV_CONDA_FORGE_ORG, + ENV_GRAPH_GITHUB_BACKEND_REPO, settings, ) from conda_forge_tick.update_sources import ( @@ -241,6 +243,12 @@ def get_latest_version_containerized( return run_container_operation( args, input=json_blob, + extra_container_args=[ + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) diff --git a/conda_forge_tick/utils.py b/conda_forge_tick/utils.py index a66c170c7..df3ab6645 100644 --- a/conda_forge_tick/utils.py +++ b/conda_forge_tick/utils.py @@ -41,6 +41,7 @@ from . import sensitive_env from .lazy_json_backends import LazyJson from .recipe_parser import CondaMetaYAML +from .settings import ENV_CONDA_FORGE_ORG, ENV_GRAPH_GITHUB_BACKEND_REPO, settings if typing.TYPE_CHECKING: from mypy_extensions import TypedDict @@ -323,6 +324,12 @@ def parse_recipe_yaml_containerized( args, input=text, mount_readonly=True, + extra_container_args=[ + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) @@ -846,6 +853,12 @@ def _run(_args, _mount_dir): input=text, mount_readonly=True, mount_dir=_mount_dir, + extra_container_args=[ + "-e", + f"{ENV_CONDA_FORGE_ORG}={settings().conda_forge_org}", + "-e", + f"{ENV_GRAPH_GITHUB_BACKEND_REPO}={settings().graph_github_backend_repo}", + ], ) if (cbc_path is not None and os.path.exists(cbc_path)) or ( @@ -1362,7 +1375,7 @@ def change_log_level(logger, new_level): logger.setLevel(saved_logger_level) -def run_command_hiding_token(args: list[str], token: str) -> int: +def run_command_hiding_token(args: list[str], token: str, **kwargs) -> int: """Run a command and hide the token in the output. Prints the outputs (stdout and stderr) of the subprocess.CompletedProcess object. @@ -1376,13 +1389,25 @@ def run_command_hiding_token(args: list[str], token: str) -> int: The command to run. token The token to hide in the output. + kwargs + additional arguments for subprocess.run Returns ------- int The return code of the command. + + Raises + ------ + ValueError + If the kwargs contain 'text', 'stdout', or 'stderr'. """ - p = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if kwargs.keys() & {"text", "stdout", "stderr"}: + raise ValueError("text, stdout, and stderr are not allowed in kwargs") + + p = subprocess.run( + args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs + ) out, err = p.stdout, p.stderr diff --git a/environment.yml b/environment.yml index c1c1a01e9..f73feab82 100644 --- a/environment.yml +++ b/environment.yml @@ -14,7 +14,7 @@ dependencies: - conda - conda-lock - conda-forge-feedstock-check-solvable >=0.8.0 - - conda-forge-feedstock-ops >=0.9.0 + - conda-forge-feedstock-ops >=0.12.0 - conda-forge-pinning - conda-libmamba-solver - conda-forge-metadata >=0.3.0 @@ -24,6 +24,7 @@ dependencies: - curl - depfinder - distributed + - fastapi - feedparser - frozendict - git @@ -32,6 +33,7 @@ dependencies: - jinja2 - lockfile - mamba >=0.23 + - mitmproxy - msgpack-python - networkx !=2.8.1 - numpy @@ -63,6 +65,7 @@ dependencies: - yaml - pip - pytest <8.1.0 + - pytest-xprocess - codecov - requests-mock - pre-commit diff --git a/tests/test_lazy_json_backends.py b/tests/test_lazy_json_backends.py index 0624022ea..e1566e1fb 100644 --- a/tests/test_lazy_json_backends.py +++ b/tests/test_lazy_json_backends.py @@ -624,7 +624,7 @@ def test_lazy_json_backends_hashmap(tmpdir): def test_github_base_url() -> None: github_backend = GithubLazyJsonBackend() - assert github_backend.base_url == settings().graph_github_backend_raw_base_url + "/" + assert github_backend.base_url == settings().graph_github_backend_raw_base_url github_backend.base_url = "https://github.com/lorem/ipsum" assert github_backend.base_url == "https://github.com/lorem/ipsum" + "/" diff --git a/tests/test_settings.py b/tests/test_settings.py index 64511b9e1..00a9930a3 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -27,7 +27,7 @@ def test_parse(self, temporary_environment): assert bot_settings.graph_repo_default_branch == "mybranch" assert ( bot_settings.graph_github_backend_raw_base_url - == "https://github.com/graph-owner/graph-repo/raw/mybranch" + == "https://github.com/graph-owner/graph-repo/raw/mybranch/" ) assert bot_settings.github_runner_debug is True assert bot_settings.frac_update_upstream_versions == 0.5 diff --git a/tests_integration/.mitmproxy/.gitignore b/tests_integration/.mitmproxy/.gitignore new file mode 100644 index 000000000..72eeef24f --- /dev/null +++ b/tests_integration/.mitmproxy/.gitignore @@ -0,0 +1,9 @@ +# mitmproxy certificate files (for integration tests) +mitmproxy-ca.key +mitmproxy-ca.crt +mitmproxy-ca.pem +mitmproxy-cert-bundle.pem +mitmproxy-dhparam.pem + +# trust script generated by CI +mitmproxy_trust_script.sh diff --git a/tests_integration/.mitmproxy/README.md b/tests_integration/.mitmproxy/README.md new file mode 100644 index 000000000..9704504bf --- /dev/null +++ b/tests_integration/.mitmproxy/README.md @@ -0,0 +1,4 @@ +# mitmproxy confdir + +This directory is intended to be used as configuration directory for `mitmproxy`. +Check the README in [the parent directory](..) for more information. diff --git a/tests_integration/README.md b/tests_integration/README.md new file mode 100644 index 000000000..8be3d97f7 --- /dev/null +++ b/tests_integration/README.md @@ -0,0 +1,173 @@ +# Integration Tests + +This directory contains integration tests for the autotick-bot. +The tests are run against actual GitHub repositories, and are used to verify that the +bot works as expected in an environment closely resembling production. + +## Test Environment +The integration tests operate in a testing environment consisting of three real GitHub entities: + +- [conda-forge-bot-staging](https://github.com/conda-forge-bot-staging) (organization) mimics the +[conda-forge](https://github.com/conda-forge) organization and will contain a selection of test feedstocks +(see below how we create them) +- [regro-cf-autotick-bot-staging](https://github.com/regro-cf-autotick-bot-staging) (user) mimics the +[regro-cf-autotick-bot](https://github.com/regro-cf-autotick-bot) account and is a test environment in which the bot +will create forks of the conda-forge-bot-staging repositories +- [regro-staging](https://github.com/regro-staging) (organization) (named after the [regro](https://github.com/regro) +account) contains a special version of the [cf-graph-countyfair](https://github.com/regro/cf-graph-countyfair) which +the bot uses during testing. + +## Test Cases Definition +The integration tests are defined in the [lib/_definitions](lib/_definitions) directory. The following directory structure is +used (using `pydantic` and `llvmdev` as example feedstocks): + +```text +definitions/ +├── pydantic/ +│ ├── resources/ +│ │ ├── feedstock +│ │ └── ... (entirely custom) +│ └── __init__.py +├── llvmdev/ +│ ├── resources/ +│ └── __init__.py +└── ... +``` + +Each feedstock has its own Python module containing the test cases for that feedstock. +**The test cases are always defined in the top-level `__init__.py` file of the feedstock directory.** + +For storing resources, a `resources` directory is used for each feedstock directory. +Inside the `resources` directory, you can use an arbitrary directory structure to store the resources. + +Usually, we include a specific revision of the original feedstock as a submodule in the `resources` directory. + +A test case always tests the entire pipeline of the bot and not any intermediate states that could be checked +in the cf-graph. See the [pytest test definition](test_integration.py) for more details. +Also, a test case is always bound to one specific feedstock. + +### Test Case Definition +To define a test case, create a subclass of `tests_integration.lib.TestCase` in the `__init__.py` file of +your feedstock. You can name it arbitrarily. +Referring to the minimal `VersionUpdate` test case in the +[pydantic module](lib/_definitions/pydantic/__init__.py), +your class has to implement three methods: + +1. `get_router()` should return an `APIRouter` object to define mock responses for specific HTTP requests. All web requests are intercepted by an HTTP proxy. +Refer to `tests_integration.lib.get_transparent_urls` to define URLs that should not be intercepted. + +2. `prepare(helper: AbstractIntegrationTestHelper)` for setting up your test case. Usually, you will want to +overwrite the feedstock repository in the test environment. The `AbstractIntegrationTestHelper` provides methods to interact +with the test environment. + +3. A function `validate(helper: AbstractIntegrationTestHelper)` for validating the state after the bot has run. +The `AbstractIntegrationTestHelper` provides convenience methods such as `assert_version_pr_present` to check for the presence +of a version update PR. + +The creation of GitHub repositories in the test environment is done automatically based on the directory structure. + +### Adding to Test Case Lists + +> [!IMPORTANT] +> Please make sure to add any added test cases to the `ALL_TEST_CASES` list in the respective `__init__.py` file of the feedstock. +> You also need to add any added feedstock to the `TEST_CASE_MAPPING` dictionary in the `definitions/__init__.py` file. + +### How Test Cases are Run + +Importantly, the integration test workflow does not execute the test cases directly. +Instead, it groups them into test scenarios and executes those at once. +A test scenario assigns one test case to every feedstock and runs them in parallel. + +Thus, test cases of different feedstocks can run simultaneously, but the different test cases for the same feedstock +are always run sequentially. + +The generation of test scenarios is done in [_collect_test_scenarios.py](lib/_collect_test_scenarios.py). It is pseudo-random, +ensuring that faulty interactions between test cases are detected eventually. + +In detail, the process of collecting test scenarios is as follows: + +#### 1. Collect Test Cases +For each feedstock, collect the available test cases in lexically sorted order. + +| Feedstock A | Feedstock B | +|-------------|-------------| +| Test Case 1 | Test Case 1 | +| Test Case 2 | Test Case 2 | +| Test Case 3 | | +| Test Case 4 | | +| Test Case 5 | | + + +#### 2. Fill Test Scenarios +The number of test scenarios is equal to the maximum number of test cases for a feedstock. +Feedstocks that have fewer test cases repeat their test cases to supply exactly one test case per scenario. +In the example below, the last instance of `test_case_2.py` for Feedstock B is not needed and thus discarded. + + +| Feedstock A | Feedstock B | +|------------------------------|------------------------------| +| Test Case 1 | Test Case 1 | +| Test Case 2 | Test Case 2 | +| Test Case 3 | Test Case 1 | +| Test Case 4 | Test Case 2 | +| Test Case 5 | Test Case 1 | +| ✂️ everything is cut here ✂️ | ✂️ everything is cut here ✂️ | +| | Test Case 2 (discarded 🗑️) | + +#### 3. Shuffle Test Scenarios +For each feedstock, we shuffle the test cases (rows) individually to ensure a random combination of test cases. +The shuffling is done pseudo-randomly based on `GITHUB_RUN_ID` (which persists for re-runs of the same workflow). + +Finally, we get the test scenarios as the rows of the table below. +Each test scenario executes exactly one test case per feedstock, in parallel. + +| | Feedstock A | Feedstock B | +|------------|-------------|-------------| +| Scenario 1 | Test Case 3 | Test Case 2 | +| Scenario 2 | Test Case 1 | Test Case 1 | +| Scenario 3 | Test Case 4 | Test Case 1 | +| Scenario 4 | Test Case 2 | Test Case 2 | +| Scenario 5 | Test Case 5 | Test Case 1 | + + +## Environment Variables +The tests expect the following environment variables: + +| Variable | Description | +|--------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `BOT_TOKEN` | Classic PAT for `cf-regro-autotick-bot-staging`. Used to interact with the test environment. | +| `TEST_SETUP_TOKEN` | Classic PAT for `cf-regro-autotick-bot-staging` used to setup the test environment. Typically, this is identical to `BOT_TOKEN`. | +| `GITHUB_RUN_ID` | Set by GitHub. ID of the current run. Used as random seed. | + + +We do not use `BOT_TOKEN` instead of `TEST_SETUP_TOKEN` for setting up the test environment to allow for future separation of the two tokens. +Furthermore, `BOT_TOKEN` is hidden by the sensitive env logic of `conda_forge_tick` and we want the test environment to not need to rely on this logic. + + +### GitHub Token Permissions +The bot token (which you can should use as the test setup token) should have the following scopes: `repo`, `workflow`, `delete_repo`. + +## Running the Integration Tests Locally + +To run the integration tests locally, you currently need to have a valid token for the `cf-regro-autotick-bot-staging` account. +Besides that, run the following setup wizard to set up self-signed certificates for the HTTP proxy: + +```bash +./mitmproxy_setup_wizard.sh +``` + +After that, run the following command to run the tests +(you need to be in the `tests_integration` directory): + +```bash +pytest -s -v --dist=no tests_integration +``` + +Remember to set the environment variables from above beforehand. + +## Debugging CI Issues + +The proxy setup of the integration tests is quite complex, and you can experience issues that only occur on GitHub Actions +and not locally. + +To debug them, consider to [use vscode-server-action](https://gist.github.com/ytausch/612106cfbc2cc660130d247fa2f3a673). diff --git a/tests_integration/__init__.py b/tests_integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/lib/__init__.py b/tests_integration/lib/__init__.py new file mode 100644 index 000000000..98f3be6ef --- /dev/null +++ b/tests_integration/lib/__init__.py @@ -0,0 +1,37 @@ +from ._collect_test_scenarios import get_all_test_scenario_ids, get_test_scenario +from ._definitions.base_classes import AbstractIntegrationTestHelper, TestCase +from ._integration_test_helper import IntegrationTestHelper +from ._run_test_cases import ( + close_all_open_pull_requests, + reset_cf_graph, + run_all_prepare_functions, + run_all_validate_functions, +) +from ._setup_repositories import prepare_all_accounts +from ._shared import ( + ENV_TEST_SCENARIO_ID, + VIRTUAL_PROXY_HOSTNAME, + VIRTUAL_PROXY_PORT, + get_global_router, + get_transparent_urls, + setup_logging, +) + +__all__ = [ + "get_all_test_scenario_ids", + "get_test_scenario", + "IntegrationTestHelper", + "close_all_open_pull_requests", + "reset_cf_graph", + "run_all_prepare_functions", + "run_all_validate_functions", + "prepare_all_accounts", + "get_global_router", + "get_transparent_urls", + "setup_logging", + "ENV_TEST_SCENARIO_ID", + "VIRTUAL_PROXY_HOSTNAME", + "VIRTUAL_PROXY_PORT", + "AbstractIntegrationTestHelper", + "TestCase", +] diff --git a/tests_integration/lib/_collect_test_scenarios.py b/tests_integration/lib/_collect_test_scenarios.py new file mode 100644 index 000000000..25f3c99be --- /dev/null +++ b/tests_integration/lib/_collect_test_scenarios.py @@ -0,0 +1,61 @@ +import os +import random + +from ._definitions import TEST_CASE_MAPPING, TestCase +from ._shared import ENV_GITHUB_RUN_ID + + +def get_number_of_test_scenarios() -> int: + return max(len(test_cases) for test_cases in TEST_CASE_MAPPING.values()) + + +def get_all_test_scenario_ids() -> list[int]: + return list(range(get_number_of_test_scenarios())) + + +def init_random(): + random.seed(int(os.environ.get(ENV_GITHUB_RUN_ID, 0))) + + +def get_test_scenario(scenario_id: int) -> dict[str, TestCase]: + """ + Get the test scenario for the given ID. + The scenario is a dictionary with the feedstock name as key and the test case name as value. + + Test scenarios are pseudo-randomly generated with the GitHub run ID as seed. + + Raises + ------ + ValueError + If the scenario ID is invalid (i.e. not between 0 and n_scenarios - 1). + """ + init_random() + + n_scenarios = get_number_of_test_scenarios() + + if n_scenarios < 0 or scenario_id >= n_scenarios: + raise ValueError( + f"Invalid scenario ID: {scenario_id}. Must be between 0 and {n_scenarios - 1}." + ) + + # make sure that each feedstock has exactly n_scenarios test cases + # We have to cut the additional test cases here to avoid that some test cases are not run. + test_cases_extended = { + feedstock: ( + test_cases + * (n_scenarios // len(test_cases) + (n_scenarios % len(test_cases) > 0)) + )[:n_scenarios] + for feedstock, test_cases in TEST_CASE_MAPPING.items() + } + + for test_cases in test_cases_extended.values(): + # in-place + random.shuffle(test_cases) + + # At this point, test_cases_extended[feedstock][i] is the test case for + # feedstock "feedstock" in the i-th test scenario. + # We need to return the i-th test scenario, so we set i to scenario_id. + return { + feedstock: test_cases_extended[feedstock][scenario_id] + for feedstock in test_cases_extended + } diff --git a/tests_integration/lib/_definitions/__init__.py b/tests_integration/lib/_definitions/__init__.py new file mode 100644 index 000000000..b5340e6f5 --- /dev/null +++ b/tests_integration/lib/_definitions/__init__.py @@ -0,0 +1,17 @@ +from . import conda_forge_pinning, pydantic +from .base_classes import AbstractIntegrationTestHelper, GitHubAccount, TestCase + +TEST_CASE_MAPPING: dict[str, list[TestCase]] = { + "conda-forge-pinning": conda_forge_pinning.ALL_TEST_CASES, + "pydantic": pydantic.ALL_TEST_CASES, +} +""" +Maps from feedstock name to a list of all test cases for that feedstock. +""" + +__all__ = [ + "AbstractIntegrationTestHelper", + "GitHubAccount", + "TestCase", + "TEST_CASE_MAPPING", +] diff --git a/tests_integration/lib/_definitions/base_classes.py b/tests_integration/lib/_definitions/base_classes.py new file mode 100644 index 000000000..2e17e8593 --- /dev/null +++ b/tests_integration/lib/_definitions/base_classes.py @@ -0,0 +1,121 @@ +""" +Module providing base classes for the integration tests. + +Both _definitions and lib refer to this module. +""" + +from abc import ABC +from enum import StrEnum +from pathlib import Path + +from fastapi import APIRouter + + +class GitHubAccount(StrEnum): + CONDA_FORGE_ORG = "conda-forge-bot-staging" + BOT_USER = "regro-cf-autotick-bot-staging" + REGRO_ORG = "regro-staging" + + +class AbstractIntegrationTestHelper(ABC): + """Abstract base class for the IntegrationTestHelper in tests_integration.lib. + Without this class, we cannot refer to IntegrationTestHelper in the definitions module + because it would create a circular import. So we refer to this class instead + and make sure that IntegrationTestHelper inherits from this class. + """ + + def overwrite_feedstock_contents( + self, feedstock_name: str, source_dir: Path, branch: str = "main" + ): + """ + Overwrite the contents of the feedstock with the contents of the source directory. + This prunes the entire git history. + + Parameters + ---------- + feedstock_name + The name of the feedstock repository, without the "-feedstock" suffix. + source_dir + The directory containing the new contents of the feedstock. + branch + The branch to overwrite. + """ + pass + + def overwrite_github_repository( + self, + owner_account: GitHubAccount, + repo_name: str, + source_dir: Path, + branch: str = "main", + ): + """ + Overwrite the contents of the repository with the contents of the source directory. + This prunes the entire git history. + + Parameters + ---------- + owner_account + The owner of the repository. + repo_name + The name of the repository. + source_dir + The directory containing the new contents of the repository. + branch + The branch to overwrite. + """ + pass + + def assert_version_pr_present( + self, + feedstock: str, + new_version: str, + new_hash: str, + old_version: str, + old_hash: str, + ) -> None: + """ + Assert that the bot has opened a version update PR. + + Parameters + ---------- + feedstock + The feedstock we expect the PR for, without the -feedstock suffix. + new_version + The new version that is expected. + new_hash + The new SHA-256 source artifact hash. + old_version + The old version of the feedstock, to check that it no longer appears in the recipe. + old_hash + The old SHA-256 source artifact hash, to check that it no longer appears in the recipe. + + + Raises + ------ + AssertionError + If the assertion fails. + """ + pass + + +class TestCase(ABC): + """ + Abstract base class for a single test case in a scenario. + Per test case, there is exactly one instance of this class statically created + in the definition of the ALL_TEST_CASES list of the feedstock module. + Note that a test case (i.e. an instance of this class) might be run multiple times, + so be careful with state you keep in the instance. + """ + + def get_router(self) -> APIRouter: + """Return the FastAPI router for the test case.""" + pass + + def prepare(self, helper: AbstractIntegrationTestHelper): + """Prepare the test case using the given helper.""" + pass + + def validate(self, helper: AbstractIntegrationTestHelper): + """Validate the test case using the given helper.""" + pass diff --git a/tests_integration/lib/_definitions/conda_forge_pinning/__init__.py b/tests_integration/lib/_definitions/conda_forge_pinning/__init__.py new file mode 100644 index 000000000..4d0aa7eec --- /dev/null +++ b/tests_integration/lib/_definitions/conda_forge_pinning/__init__.py @@ -0,0 +1,20 @@ +from pathlib import Path + +from fastapi import APIRouter + +from ..base_classes import AbstractIntegrationTestHelper, TestCase + + +class SetupPinnings(TestCase): + def get_router(self) -> APIRouter: + return APIRouter() + + def prepare(self, helper: AbstractIntegrationTestHelper): + feedstock_dir = Path(__file__).parent / "resources" / "feedstock" + helper.overwrite_feedstock_contents("conda-forge-pinning", feedstock_dir) + + def validate(self, helper: AbstractIntegrationTestHelper): + pass + + +ALL_TEST_CASES: list[TestCase] = [SetupPinnings()] diff --git a/tests_integration/lib/_definitions/conda_forge_pinning/resources/feedstock b/tests_integration/lib/_definitions/conda_forge_pinning/resources/feedstock new file mode 160000 index 000000000..3e96d074a --- /dev/null +++ b/tests_integration/lib/_definitions/conda_forge_pinning/resources/feedstock @@ -0,0 +1 @@ +Subproject commit 3e96d074a01bb5790dead82728f02f9acc809104 diff --git a/tests_integration/lib/_definitions/pydantic/__init__.py b/tests_integration/lib/_definitions/pydantic/__init__.py new file mode 100644 index 000000000..fecddb524 --- /dev/null +++ b/tests_integration/lib/_definitions/pydantic/__init__.py @@ -0,0 +1,45 @@ +import json +from pathlib import Path + +from fastapi import APIRouter + +from ..base_classes import AbstractIntegrationTestHelper, TestCase + +PYPI_SIMPLE_API_RESPONSE = json.loads( + Path(__file__) + .parent.joinpath("resources/pypi_simple_api_response.json") + .read_text() +) + + +class VersionUpdate(TestCase): + def get_router(self) -> APIRouter: + router = APIRouter() + + @router.get("/pypi.org/pypi/pydantic/json") + def handle_pypi_json_api(): + return { + # rest omitted + "info": {"name": "pydantic", "version": "2.10.2"} + } + + return router + + def prepare(self, helper: AbstractIntegrationTestHelper): + feedstock_dir = Path(__file__).parent / "resources" / "feedstock" + helper.overwrite_feedstock_contents("pydantic", feedstock_dir) + + feedstock_v1_dir = Path(__file__).parent / "resources" / "feedstock_v1" + helper.overwrite_feedstock_contents("pydantic", feedstock_v1_dir, branch="1.x") + + def validate(self, helper: AbstractIntegrationTestHelper): + helper.assert_version_pr_present( + "pydantic", + new_version="2.10.2", + new_hash="2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", + old_version="2.10.1", + old_hash="a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", + ) + + +ALL_TEST_CASES: list[TestCase] = [VersionUpdate()] diff --git a/tests_integration/lib/_definitions/pydantic/resources/feedstock b/tests_integration/lib/_definitions/pydantic/resources/feedstock new file mode 160000 index 000000000..d347fade3 --- /dev/null +++ b/tests_integration/lib/_definitions/pydantic/resources/feedstock @@ -0,0 +1 @@ +Subproject commit d347fade36be6541e73e649fcc448316c7e7e696 diff --git a/tests_integration/lib/_definitions/pydantic/resources/feedstock_v1 b/tests_integration/lib/_definitions/pydantic/resources/feedstock_v1 new file mode 160000 index 000000000..7e5827b31 --- /dev/null +++ b/tests_integration/lib/_definitions/pydantic/resources/feedstock_v1 @@ -0,0 +1 @@ +Subproject commit 7e5827b317b91157fed6a2a8587b5fbcde0f409c diff --git a/tests_integration/lib/_definitions/pydantic/resources/pypi_simple_api_response.json b/tests_integration/lib/_definitions/pydantic/resources/pypi_simple_api_response.json new file mode 100644 index 000000000..5e0e6eb36 --- /dev/null +++ b/tests_integration/lib/_definitions/pydantic/resources/pypi_simple_api_response.json @@ -0,0 +1,78 @@ +{ + "alternate-locations": [], + "files": [ + { + "core-metadata": { + "sha256": "8891ad1ba9f1dd99bdad181d0ad82ef51517615ed7065bf934397a45ef8d91a3" + }, + "data-dist-info-metadata": { + "sha256": "8891ad1ba9f1dd99bdad181d0ad82ef51517615ed7065bf934397a45ef8d91a3" + }, + "filename": "pydantic-2.10.1-py3-none-any.whl", + "hashes": { + "sha256": "a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e" + }, + "provenance": "https://pypi.org/integrity/pydantic/2.10.1/pydantic-2.10.1-py3-none-any.whl/provenance", + "requires-python": ">=3.8", + "size": 455329, + "upload-time": "2024-11-22T00:58:40.347020Z", + "url": "https://files.pythonhosted.org/packages/e0/fc/fda48d347bd50a788dd2a0f318a52160f911b86fc2d8b4c86f4d7c9bceea/pydantic-2.10.1-py3-none-any.whl", + "yanked": false + }, + { + "core-metadata": false, + "data-dist-info-metadata": false, + "filename": "pydantic-2.10.1.tar.gz", + "hashes": { + "sha256": "a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560" + }, + "provenance": "https://pypi.org/integrity/pydantic/2.10.1/pydantic-2.10.1.tar.gz/provenance", + "requires-python": ">=3.8", + "size": 783717, + "upload-time": "2024-11-22T00:58:43.709945Z", + "url": "https://files.pythonhosted.org/packages/c4/bd/7fc610993f616d2398958d0028d15eaf53bde5f80cb2edb7aa4f1feaf3a7/pydantic-2.10.1.tar.gz", + "yanked": false + }, + { + "core-metadata": { + "sha256": "4065bed9e76ea559db6875b14516a3dca5e9dda81fa1d296efa542f90422c558" + }, + "data-dist-info-metadata": { + "sha256": "4065bed9e76ea559db6875b14516a3dca5e9dda81fa1d296efa542f90422c558" + }, + "filename": "pydantic-2.10.2-py3-none-any.whl", + "hashes": { + "sha256": "cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e" + }, + "provenance": "https://pypi.org/integrity/pydantic/2.10.2/pydantic-2.10.2-py3-none-any.whl/provenance", + "requires-python": ">=3.8", + "size": 456364, + "upload-time": "2024-11-26T13:02:27.147110Z", + "url": "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", + "yanked": false + }, + { + "core-metadata": false, + "data-dist-info-metadata": false, + "filename": "pydantic-2.10.2.tar.gz", + "hashes": { + "sha256": "2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa" + }, + "provenance": "https://pypi.org/integrity/pydantic/2.10.2/pydantic-2.10.2.tar.gz/provenance", + "requires-python": ">=3.8", + "size": 785401, + "upload-time": "2024-11-26T13:02:29.793774Z", + "url": "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", + "yanked": false + } + ], + "meta": { + "_last-serial": 27108195, + "api-version": "1.3" + }, + "name": "pydantic", + "versions": [ + "2.10.1", + "2.10.2" + ] +} diff --git a/tests_integration/lib/_integration_test_helper.py b/tests_integration/lib/_integration_test_helper.py new file mode 100644 index 000000000..c5f2c06b2 --- /dev/null +++ b/tests_integration/lib/_integration_test_helper.py @@ -0,0 +1,141 @@ +import logging +import shutil +import subprocess +import tempfile +from pathlib import Path +from tempfile import TemporaryDirectory + +from github import Github + +from conda_forge_tick.git_utils import GitCli +from conda_forge_tick.utils import ( + run_command_hiding_token, +) + +from ._definitions import AbstractIntegrationTestHelper, GitHubAccount +from ._shared import ( + FEEDSTOCK_SUFFIX, + get_github_token, +) + +LOGGER = logging.getLogger(__name__) + + +class IntegrationTestHelper(AbstractIntegrationTestHelper): + def overwrite_feedstock_contents( + self, feedstock_name: str, source_dir: Path, branch: str = "main" + ): + self.overwrite_github_repository( + GitHubAccount.CONDA_FORGE_ORG, + feedstock_name + FEEDSTOCK_SUFFIX, + source_dir, + branch, + ) + + def overwrite_github_repository( + self, + owner_account: GitHubAccount, + repo_name: str, + source_dir: Path, + branch: str = "main", + ): + # We execute all git operations in a separate temporary directory to avoid side effects. + with TemporaryDirectory(repo_name) as tmpdir_str: + tmpdir = Path(tmpdir_str) + self._overwrite_github_repository_with_tmpdir( + owner_account, repo_name, source_dir, tmpdir, branch + ) + + @staticmethod + def _overwrite_github_repository_with_tmpdir( + owner_account: GitHubAccount, + repo_name: str, + source_dir: Path, + tmpdir: Path, + branch: str = "main", + ): + """See `overwrite_github_repository`.""" + dest_dir = tmpdir / repo_name + shutil.copytree(source_dir, dest_dir) + + # Remove the .git directory (if it exists) + shutil.rmtree(dest_dir / ".git", ignore_errors=True) + dest_dir.joinpath(".git").unlink(missing_ok=True) # if it is a file + + # Initialize a new git repository and commit everything + subprocess.run( + ["git", "init", f"--initial-branch={branch}"], cwd=dest_dir, check=True + ) + subprocess.run(["git", "add", "--all"], cwd=dest_dir, check=True) + subprocess.run( + ["git", "commit", "-m", "Overwrite Repository Contents"], + cwd=dest_dir, + check=True, + ) + + # Push the new contents to the repository + push_token = get_github_token(owner_account) + run_command_hiding_token( + [ + "git", + "push", + f"https://{push_token}@github.com/{owner_account}/{repo_name}.git", + branch, + "--force", + ], + token=push_token, + cwd=dest_dir, + check=True, + ) + + LOGGER.info( + "Repository contents of %s have been overwritten successfully.", repo_name + ) + + def assert_version_pr_present( + self, + feedstock: str, + new_version: str, + new_hash: str, + old_version: str, + old_hash: str, + ): + gh = Github(get_github_token(GitHubAccount.CONDA_FORGE_ORG)) + + full_feedstock_name = feedstock + FEEDSTOCK_SUFFIX + repo = gh.get_organization(GitHubAccount.CONDA_FORGE_ORG).get_repo( + full_feedstock_name + ) + matching_prs = [ + pr for pr in repo.get_pulls(state="open") if f"v{new_version}" in pr.title + ] + + assert len(matching_prs) == 1, ( + f"Found {len(matching_prs)} matching version PRs, but exactly 1 must be present." + ) + + matching_pr = matching_prs[0] + + assert matching_pr.head.repo.owner.login == GitHubAccount.BOT_USER + assert matching_pr.head.repo.name == full_feedstock_name + + cli = GitCli() + + with tempfile.TemporaryDirectory() as tmpdir: + target_dir = Path(tmpdir) / full_feedstock_name + cli.clone_repo(matching_pr.head.repo.clone_url, target_dir) + cli.checkout_branch(target_dir, matching_pr.head.ref) + + with open(target_dir / "recipe" / "meta.yaml") as f: + meta = f.read() + + assert f'{{% set version = "{new_version}" %}}' in meta + assert f"sha256: {new_hash}" in meta + assert old_version not in meta + assert old_hash not in meta + + LOGGER.info( + "Version PR for %s v%s validated successfully.", + feedstock, + new_version, + ) diff --git a/tests_integration/lib/_run_test_cases.py b/tests_integration/lib/_run_test_cases.py new file mode 100644 index 000000000..12234ea47 --- /dev/null +++ b/tests_integration/lib/_run_test_cases.py @@ -0,0 +1,51 @@ +import logging +from importlib import resources + +from github import Github + +from conda_forge_tick.settings import settings + +from ._definitions import GitHubAccount, TestCase +from ._integration_test_helper import IntegrationTestHelper +from ._shared import FEEDSTOCK_SUFFIX, get_github_token + +LOGGER = logging.getLogger(__name__) +EMPTY_GRAPH_DIR = resources.files("tests_integration.resources").joinpath("empty-graph") + + +def close_all_open_pull_requests(): + github = Github(get_github_token(GitHubAccount.CONDA_FORGE_ORG)) + org = github.get_organization(GitHubAccount.CONDA_FORGE_ORG) + + for repo in org.get_repos(): + if not repo.name.endswith(FEEDSTOCK_SUFFIX): + continue + for pr in repo.get_pulls(state="open"): + pr.create_issue_comment( + "Closing this PR because it is a leftover from a previous test run." + ) + pr.edit(state="closed") + + +def reset_cf_graph(): + with resources.as_file(EMPTY_GRAPH_DIR) as empty_graph_dir: + IntegrationTestHelper().overwrite_github_repository( + GitHubAccount.REGRO_ORG, + "cf-graph-countyfair", + empty_graph_dir, + branch=settings().graph_repo_default_branch, + ) + + +def run_all_prepare_functions(scenario: dict[str, TestCase]): + test_helper = IntegrationTestHelper() + for feedstock_name, test_case in scenario.items(): + LOGGER.info("Preparing %s...", feedstock_name) + test_case.prepare(test_helper) + + +def run_all_validate_functions(scenario: dict[str, TestCase]): + test_helper = IntegrationTestHelper() + for feedstock_name, test_case in scenario.items(): + LOGGER.info("Validating %s...", feedstock_name) + test_case.validate(test_helper) diff --git a/tests_integration/lib/_setup_repositories.py b/tests_integration/lib/_setup_repositories.py new file mode 100644 index 000000000..b0627c9fa --- /dev/null +++ b/tests_integration/lib/_setup_repositories.py @@ -0,0 +1,193 @@ +""" +Module used by the integration tests to set up the GitHub repositories +that are needed for running the tests. + +We do not *create* any repositories within the bot's user account here. This is handled in the prepare function of the +test cases themselves because tests could purposefully rely on the actual bot itself to create repositories. + +However, we do delete unnecessary feedstocks from the bot's user account. +""" + +import logging +from collections.abc import Iterable +from dataclasses import dataclass +from typing import Protocol + +from github import Github +from github.Repository import Repository + +from ._definitions import TEST_CASE_MAPPING, GitHubAccount +from ._shared import ( + FEEDSTOCK_SUFFIX, + REGRO_ACCOUNT_REPOS, + get_github_token, + is_user_account, +) + +LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class GitHubAccountSetup: + """Information about the setup of a GitHub account for the integration tests.""" + + account: GitHubAccount + """ + The GitHub account for which the setup is done. + """ + + target_names: set[str] + """ + The names of the repositories that should exist after the preparation (excluding the suffix). + """ + + suffix: str | None = None + """ + If given, only repositories with the given suffix are considered for deletion and the target names + are extended with the suffix. + """ + + delete_only: bool = False + """ + If True, only delete unnecessary repositories and do not create any new ones. + """ + + +class RepositoryOwner(Protocol): + def create_repo(self, name: str) -> Repository: + pass + + def get_repo(self, name: str) -> Repository: + pass + + def get_repos(self) -> Iterable[Repository]: + pass + + +def get_test_feedstock_names() -> set[str]: + """Return the list of feedstock names that are needed for the integration tests. + + The names do not include the "-feedstock" suffix. + """ + return set(TEST_CASE_MAPPING.keys()) + + +def _or_empty_set(value: set[str]) -> set[str] | str: + """Return "{}" if the given set is empty, otherwise return the set itself.""" + return value or "{}" + + +def prepare_repositories( + owner: RepositoryOwner, + owner_name: str, + existing_repos: Iterable[Repository], + target_names: Iterable[str], + delete_only: bool, + suffix: str | None = None, +) -> None: + """Prepare the repositories of a certain owner for the integration tests. + Unnecessary repositories are deleted and missing repositories are created. + + Parameters + ---------- + owner + The owner of the repositories. + owner_name + The name of the owner (for logging). + existing_repos + The existing repositories of the owner. + target_names + The names of the repositories that should exist after the preparation (excluding the suffix). + suffix + If given, only repositories with the given suffix are considered for deletion and the target names + are extended with the suffix. + delete_only + If True, only delete unnecessary repositories and do not create any new ones. + """ + existing_names = {repo.name for repo in existing_repos} + target_names = set(target_names) + + if suffix: + existing_names = {name for name in existing_names if name.endswith(suffix)} + target_names = {name + suffix for name in target_names} + + to_delete = existing_names - target_names + to_create = target_names - existing_names + + LOGGER.info( + "Deleting the following repositories for %s: %s", + owner_name, + _or_empty_set(to_delete), + ) + for name in to_delete: + owner.get_repo(name).delete() + + if delete_only: + return + + LOGGER.info( + "Creating the following repositories for %s: %s", + owner_name, + _or_empty_set(to_create), + ) + for name in to_create: + owner.create_repo(name) + + +def prepare_accounts(setup_infos: Iterable[GitHubAccountSetup]): + """Prepare the repositories of all GitHub accounts for the integration tests. + + Raises + ------ + ValueError + If a token is not for associated user. + """ + for setup_info in setup_infos: + # for each account, we need to create a separate GitHub instance because different tokens are needed + github = Github(get_github_token(setup_info.account)) + + owner: RepositoryOwner + existing_repos: Iterable[Repository] + if is_user_account(setup_info.account): + current_user = github.get_user() + if current_user.login != setup_info.account: + raise ValueError("The token is not for the expected user") + owner = current_user + existing_repos = current_user.get_repos(type="owner") + else: + owner = github.get_organization(setup_info.account) + existing_repos = owner.get_repos() + + prepare_repositories( + owner=owner, + owner_name=setup_info.account, + existing_repos=existing_repos, + target_names=setup_info.target_names, + delete_only=setup_info.delete_only, + suffix=setup_info.suffix, + ) + + +def prepare_all_accounts(): + test_feedstock_names = get_test_feedstock_names() + LOGGER.info("Test feedstock names: %s", _or_empty_set(test_feedstock_names)) + + setup_infos: list[GitHubAccountSetup] = [ + GitHubAccountSetup( + GitHubAccount.CONDA_FORGE_ORG, + target_names=test_feedstock_names, + suffix=FEEDSTOCK_SUFFIX, + ), + GitHubAccountSetup( + GitHubAccount.BOT_USER, + target_names=set(), + suffix=FEEDSTOCK_SUFFIX, + delete_only=True, # see the top-level comment for the reason + ), + GitHubAccountSetup( + GitHubAccount.REGRO_ORG, + REGRO_ACCOUNT_REPOS, + ), + ] + + prepare_accounts(setup_infos) diff --git a/tests_integration/lib/_shared.py b/tests_integration/lib/_shared.py new file mode 100644 index 000000000..3f9fa7d73 --- /dev/null +++ b/tests_integration/lib/_shared.py @@ -0,0 +1,109 @@ +import logging +import os + +from fastapi import APIRouter + +from conda_forge_tick.settings import settings + +from ._definitions import GitHubAccount + +GITHUB_TOKEN_ENV_VARS: dict[GitHubAccount, str] = { + GitHubAccount.CONDA_FORGE_ORG: "TEST_SETUP_TOKEN", + GitHubAccount.BOT_USER: "TEST_SETUP_TOKEN", + GitHubAccount.REGRO_ORG: "TEST_SETUP_TOKEN", +} + +IS_USER_ACCOUNT: dict[GitHubAccount, bool] = { + GitHubAccount.CONDA_FORGE_ORG: False, + GitHubAccount.BOT_USER: True, + GitHubAccount.REGRO_ORG: False, +} + +REGRO_ACCOUNT_REPOS = {"cf-graph-countyfair"} + +ENV_GITHUB_RUN_ID = "GITHUB_RUN_ID" +""" +Used as a random seed for the integration tests. +""" +ENV_TEST_SCENARIO_ID = "SCENARIO_ID" + +FEEDSTOCK_SUFFIX = "-feedstock" + + +def setup_logging(default_level: int): + """ + Set up the Python logging module. + Uses the passed log level as the default level. + If running within GitHub Actions and the workflow runs in debug mode, the log level is never set above DEBUG. + """ + if settings().github_runner_debug and default_level > logging.DEBUG: + level = logging.DEBUG + else: + level = default_level + logging.basicConfig(level=level) + + +def get_github_token(account: GitHubAccount) -> str: + return os.environ[GITHUB_TOKEN_ENV_VARS[account]] + + +def is_user_account(account: GitHubAccount) -> bool: + return IS_USER_ACCOUNT[account] + + +def get_transparent_urls() -> set[str]: + """ + Get URLs which should be forwarded to the actual upstream URLs in the tests. + + Unix filename patterns (provided by fnmatch) are used to specify wildcards: + https://docs.python.org/3/library/fnmatch.html + """ + # this is not a constant because the graph_repo_default_branch setting is dynamic + graph_repo_default_branch = settings().graph_repo_default_branch + transparent_urls = { + f"https://raw.githubusercontent.com/regro/cf-graph-countyfair/{graph_repo_default_branch}/mappings/pypi/name_mapping.yaml", + f"https://raw.githubusercontent.com/regro/cf-graph-countyfair/{graph_repo_default_branch}/mappings/pypi/grayskull_pypi_mapping.json", + "https://raw.githubusercontent.com/regro/cf-scripts/refs/heads/main/conda_forge_tick/cf_tick_schema.json", + "https://raw.githubusercontent.com/conda-forge/conda-smithy/refs/heads/main/conda_smithy/data/conda-forge.json", + "https://api.github.com/*", + "https://github.com/regro-staging/*", + "https://github.com/conda-forge-bot-staging/*", + "https://github.com/regro-cf-autotick-bot-staging/*", + "https://raw.githubusercontent.com/regro-staging/*", + "https://raw.githubusercontent.com/conda-forge-bot-staging/*", + "https://raw.githubusercontent.com/regro-cf-autotick-bot-staging/*", + "https://pypi.io/packages/source/*", + "https://pypi.org/packages/source/*", + "https://files.pythonhosted.org/packages/*", + "https://api.anaconda.org/package/conda-forge/conda-forge-pinning", + "https://api.anaconda.org/download/conda-forge/conda-forge-pinning/*", + "https://binstar-cio-packages-prod.s3.amazonaws.com/*", + } + + # this is to protect against mistakes and typos, adjust if it ever becomes too strict + assert all(url.startswith("https://") for url in transparent_urls) + + # silence the PyCharm warning about using http instead of https + # noinspection HttpUrlsUsage + http_urls = {url.replace("https://", "http://", 1) for url in transparent_urls} + + return transparent_urls | http_urls + + +def get_global_router(): + """Return the global FastAPI router to be included in all test scenarios.""" + router = APIRouter() + + @router.get("/cran.r-project.org/src/contrib/") + def handle_cran_index(): + return "" + + @router.get("/cran.r-project.org/src/contrib/Archive/") + def handle_cran_index_archive(): + return "" + + return router + + +VIRTUAL_PROXY_HOSTNAME = "virtual.proxy" +VIRTUAL_PROXY_PORT = 80 diff --git a/tests_integration/mitmproxy_setup_wizard.sh b/tests_integration/mitmproxy_setup_wizard.sh new file mode 100755 index 000000000..5b6f2776f --- /dev/null +++ b/tests_integration/mitmproxy_setup_wizard.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "=== mitmproxy certificates setup wizard ===" +echo "Use this shell script to setup the mitmproxy certificates for the integration tests on your machine." + +# we could also add openssl to the conda environment, but this should be available on most systems +if ! command -v openssl &> /dev/null; then + echo "error: openssl is not installed. Please install it first." + exit 1 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +mitmproxy_dir="${script_dir}/.mitmproxy" +# the mitmproxy_dir should already exist +cd "${mitmproxy_dir}" + +# Headless Mode is used in GitHub Actions only +headless_mode="${MITMPROXY_WIZARD_HEADLESS:-false}" + +if [ "${headless_mode}" = "true" ]; then + echo "Running in headless mode." + echo "The mitmproxy certificates will be generated in the directory: ${mitmproxy_dir}" + + # path to a script that will be executed after the certificates have been generated + # the script should add the mitmproxy-ca.crt certificate to the system's trust store + # the first argument is the path to the mitmproxy-ca.crt certificate + headless_mode_trust_script="${MITMPROXY_WIZARD_HEADLESS_TRUST_SCRIPT}" +else + echo "The mitmproxy certificates will be generated in the directory: ${mitmproxy_dir}" + echo "Press enter to continue or Ctrl+C to cancel." + read -r +fi + +openssl genrsa -out mitmproxy-ca.key 4096 +openssl req -x509 -new -nodes -key mitmproxy-ca.key -sha256 -days 365 -out mitmproxy-ca.crt -addext keyUsage=critical,keyCertSign -subj "/C=US/ST=cf-scripts/L=cf-scripts/O=cf-scripts/OU=cf-scripts/CN=cf-scripts" +cat mitmproxy-ca.key mitmproxy-ca.crt > mitmproxy-ca.pem + +echo "The mitmproxy certificates have been generated successfully." +echo "The root certificate will be valid for 365 days." + +mitmproxy_ca_crt_file="${mitmproxy_dir}/mitmproxy-ca.crt" + +if [ "${headless_mode}" = "true" ]; then + echo "Executing the headless mode trust script..." + bash "${headless_mode_trust_script}" "${mitmproxy_ca_crt_file}" +else + echo "You now need to trust the mitmproxy-ca.crt certificate in your system's trust store." + echo "The exact process depends on your operating system." + if [[ -f "/etc/debian_version" ]]; then + echo "On Debian-based systems, you can use the following command to trust the certificate:" + echo "sudo cp ${mitmproxy_ca_crt_file} /usr/local/share/ca-certificates/mitmproxy-ca.crt" + echo "sudo update-ca-certificates" + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "On macOS, drag and drop the mitmproxy-ca.crt file into the Keychain Access app while having the 'Login' keychain selected." + echo "Then, double-click the certificate in the keychain and set ‘Always Trust‘ in the ‘Trust‘ section." + fi + echo "The certificate is located at: ${mitmproxy_ca_crt_file}" + echo "After you're done, press enter to continue." + read -r +fi + +echo "Generating the certificate bundle mitmproxy-cert-bundle.pem to pass to Python..." +cp "$(python -m certifi)" mitmproxy-cert-bundle.pem + +{ + echo "" + echo "# cf-scripts self-signed certificate" + cat mitmproxy-ca.crt +} >> mitmproxy-cert-bundle.pem + +echo "The certificate bundle has been generated successfully." +echo "The mitmproxy certificate setup wizard has been completed successfully." diff --git a/tests_integration/mock_proxy_start.sh b/tests_integration/mock_proxy_start.sh new file mode 100755 index 000000000..1e29979dd --- /dev/null +++ b/tests_integration/mock_proxy_start.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# If debugging locally, set PROXY_DEBUG_LOGGING to true to show full HTTP request/response details. +# We can't enable this in GitHub Actions because it will expose GitHub secrets. +PROXY_DEBUG_LOGGING=${PROXY_DEBUG_LOGGING:-false} + +if [[ "${PROXY_DEBUG_LOGGING}" == "true" ]]; then + flow_detail=4 +else + flow_detail=0 +fi + +if [[ -z "$MITMPROXY_CONFDIR" ]]; then + echo "Set $MITMPROXY_CONFDIR to a directory containing a mitmproxy-ca.pem CA certificate to intercept HTTPS traffic." + exit 1 +fi + +# You might need to set PYTHONPATH to the root of cf-scripts +mitmdump -s ./mock_server_addon.py \ + --flow-detail "$flow_detail" \ + --set confdir="$MITMPROXY_CONFDIR" \ + --set connection_strategy=lazy \ + --set upstream_cert=false 2>&1 | tee /tmp/mitmproxy.log diff --git a/tests_integration/mock_server_addon.py b/tests_integration/mock_server_addon.py new file mode 100644 index 000000000..b2425821b --- /dev/null +++ b/tests_integration/mock_server_addon.py @@ -0,0 +1,59 @@ +#!/usr/bin/env mitmdump -s +""" +Start this file with `mitmdump -s mock_server_addon.py`. + +This file expects an environment variable to be set to the ID of the test scenario to run. +The name of this variable is defined in `ENV_TEST_SCENARIO_ID`. + +Starting mitmdump from a Python script is not officially supported. +""" + +import fnmatch +import logging +import os + +from fastapi import FastAPI +from mitmproxy.addons import asgiapp +from mitmproxy.http import HTTPFlow + +from tests_integration.lib import ( + ENV_TEST_SCENARIO_ID, + VIRTUAL_PROXY_HOSTNAME, + VIRTUAL_PROXY_PORT, + get_global_router, + get_test_scenario, + get_transparent_urls, +) + +LOGGER = logging.getLogger(__name__) + + +def request(flow: HTTPFlow): + if any( + fnmatch.fnmatch(flow.request.url, pattern) for pattern in get_transparent_urls() + ): + return + flow.request.path = f"/{flow.request.host}{flow.request.path}" + flow.request.host = VIRTUAL_PROXY_HOSTNAME + flow.request.port = VIRTUAL_PROXY_PORT + flow.request.scheme = "http" + + +def _setup_fastapi(): + scenario_id = int(os.environ[ENV_TEST_SCENARIO_ID]) + scenario = get_test_scenario(scenario_id) + + app = FastAPI() + + app.include_router(get_global_router()) + + for feedstock_name, test_case in scenario.items(): + LOGGER.info("Setting up mocks for %s...", feedstock_name) + app.include_router(test_case.get_router()) + + return app + + +addons = [ + asgiapp.ASGIApp(_setup_fastapi(), VIRTUAL_PROXY_HOSTNAME, VIRTUAL_PROXY_PORT), +] diff --git a/tests_integration/resources/__init__.py b/tests_integration/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/resources/empty-graph/README.md b/tests_integration/resources/empty-graph/README.md new file mode 100644 index 000000000..917fd56dd --- /dev/null +++ b/tests_integration/resources/empty-graph/README.md @@ -0,0 +1,4 @@ +# cf-graph-countyfair (For Integration Tests) + +This repository is used in integration tests of [cf-scripts](https://github.com/regro/cf-scripts) to set up a clean initial state for the +`cf-graph-countyfair` repository. diff --git a/tests_integration/resources/empty-graph/graph.json b/tests_integration/resources/empty-graph/graph.json new file mode 100644 index 000000000..2f3b20c61 --- /dev/null +++ b/tests_integration/resources/empty-graph/graph.json @@ -0,0 +1,9 @@ +{ + "directed": true, + "graph": { + "outputs_lut": {} + }, + "links": [], + "multigraph": false, + "nodes": [] +} diff --git a/tests_integration/test_integration.py b/tests_integration/test_integration.py new file mode 100644 index 000000000..6901f1655 --- /dev/null +++ b/tests_integration/test_integration.py @@ -0,0 +1,321 @@ +""" +Pytest entry point for the integration tests. +Please refer to the README.md in the tests_integration (i.e., parent) directory +for more information. +""" + +import contextlib +import logging +import os +import socket +import subprocess +import tempfile +from pathlib import Path + +import pytest +from xprocess import ProcessStarter, XProcess + +from conda_forge_tick.settings import settings, use_settings +from tests_integration.lib import ( + TestCase, + close_all_open_pull_requests, + get_all_test_scenario_ids, + get_test_scenario, + prepare_all_accounts, + reset_cf_graph, + run_all_prepare_functions, + run_all_validate_functions, + setup_logging, +) + +TESTS_INTEGRATION_DIR = Path(__file__).parent +CF_SCRIPTS_ROOT_DIR = TESTS_INTEGRATION_DIR.parent +MITMPROXY_CONFDIR = TESTS_INTEGRATION_DIR / ".mitmproxy" +MITMPROXY_CERT_BUNDLE_FILE = MITMPROXY_CONFDIR / "mitmproxy-cert-bundle.pem" + + +@pytest.fixture(scope="module", autouse=True) +def global_environment_setup(): + """Set up the global environment variables for the tests.""" + # Make sure to also set BOT_TOKEN, we cannot validate this here! + assert os.environ.get("TEST_SETUP_TOKEN"), "TEST_SETUP_TOKEN must be set." + + # In Python 3.13, this might break. https://stackoverflow.com/a/79124282 + os.environ["MITMPROXY_CONFDIR"] = str(MITMPROXY_CONFDIR.resolve()) + os.environ["SSL_CERT_FILE"] = str(MITMPROXY_CERT_BUNDLE_FILE.resolve()) + os.environ["REQUESTS_CA_BUNDLE"] = str(MITMPROXY_CERT_BUNDLE_FILE.resolve()) + os.environ["GIT_SSL_CAINFO"] = str(MITMPROXY_CERT_BUNDLE_FILE.resolve()) + + github_run_id = os.environ.get("GITHUB_RUN_ID", "GITHUB_RUN_ID_NOT_SET") + os.environ["RUN_URL"] = ( + f"https://github.com/regro/cf-scripts/actions/runs/{github_run_id}" + ) + + # by default, we enable container mode because it is the default in the bot + os.environ["CF_FEEDSTOCK_OPS_IN_CONTAINER"] = "false" + + # set if not set + os.environ.setdefault("CF_FEEDSTOCK_OPS_CONTAINER_NAME", "conda-forge-tick") + os.environ.setdefault("CF_FEEDSTOCK_OPS_CONTAINER_TAG", "test") + + new_settings = settings() + + new_settings.frac_make_graph = 1.0 # do not skip nodes due to randomness + new_settings.frac_update_upstream_versions = 1.0 + new_settings.graph_github_backend_repo = "regro-staging/cf-graph-countyfair" + new_settings.conda_forge_org = "conda-forge-bot-staging" + + with use_settings(new_settings): + setup_logging(logging.INFO) + yield + + +@pytest.fixture +def disable_container_mode(monkeypatch): + """Disable container mode for the test.""" + monkeypatch.setenv("CF_FEEDSTOCK_OPS_IN_CONTAINER", "true") + + +@pytest.fixture(scope="module") +def repositories_setup(): + """Set up the repositories for the tests.""" + prepare_all_accounts() + + +@pytest.fixture(params=get_all_test_scenario_ids()) +def scenario(request) -> tuple[int, dict[str, TestCase]]: + scenario_id: int = request.param + close_all_open_pull_requests() + reset_cf_graph() + + scenario = get_test_scenario(scenario_id) + scenario_pretty_print = { + feedstock_name: test_case.__class__.__name__ + for feedstock_name, test_case in scenario.items() + } + + print(f"Preparing test scenario {scenario_id}...") + print(f"Scenario: {scenario_pretty_print}") + + run_all_prepare_functions(scenario) + + return scenario_id, scenario + + +def is_proxy_running(port: int, timeout: float = 2.0) -> bool: + """Return if the proxy is running on localhost:port. + + Parameters + ---------- + port + The port to check. + timeout + The timeout in seconds. + + Returns + ------- + bool + True if the proxy is running, False otherwise. + """ + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + return sock.connect_ex(("localhost", port)) == 0 + + +@pytest.fixture +def mitmproxy(xprocess: XProcess, scenario: tuple[int, dict[str, TestCase]]): + scenario_id, _ = scenario + + class MitmproxyStarter(ProcessStarter): + args = ["./mock_proxy_start.sh"] + timeout = 60 + popen_kwargs = {"cwd": TESTS_INTEGRATION_DIR} + env = os.environ | { + "SCENARIO_ID": str(scenario_id), + "PYTHONPATH": str(CF_SCRIPTS_ROOT_DIR.resolve()), + } + + def startup_check(self): + return is_proxy_running(port=8080) + + xprocess.ensure("mitmproxy", MitmproxyStarter) + + yield + + xprocess.getinfo("mitmproxy").terminate() + + +@contextlib.contextmanager +def in_fresh_cf_graph(): + """ + Context manager to execute code within the context with a new clone + of cf-graph in a temporary directory. + """ + old_working_dir = os.getcwd() + + with tempfile.TemporaryDirectory() as tmpdir_s: + tmpdir = Path(tmpdir_s) + + cf_graph_repo = settings().graph_github_backend_repo + + # --depth=5 is the same value as used in prod (see autotick-bot/install_bot_code.sh) + subprocess.run( + [ + "git", + "clone", + "--depth=5", + f"https://github.com/{cf_graph_repo}.git", + "cf-graph", + ], + check=True, + cwd=tmpdir, + ) + + cf_graph_dir = tmpdir / "cf-graph" + + subprocess.run( + [ + "git", + "config", + "user.name", + "regro-cf-autotick-bot-staging", + ], + check=True, + cwd=cf_graph_dir, + ) + + subprocess.run( + [ + "git", + "config", + "user.email", + "regro-cf-autotick-bot-staging@users.noreply.github.com", + ], + check=True, + cwd=cf_graph_dir, + ) + + subprocess.run( + [ + "git", + "config", + "pull.rebase", + "false", + ], + check=True, + cwd=cf_graph_dir, + ) + + subprocess.run( + ["git", "config", 'http."https://github.com".proxy', '""'], + check=True, + cwd=cf_graph_dir, + ) + + os.chdir(cf_graph_dir) + yield + + os.chdir(old_working_dir) + + +@contextlib.contextmanager +def mitmproxy_env(): + """Set environment variables for bot steps that should be piped through mitmproxy.""" + old_env = os.environ.copy() + + os.environ["http_proxy"] = "http://127.0.0.1:8080" + os.environ["https_proxy"] = "http://127.0.0.1:8080" + os.environ["CF_FEEDSTOCK_OPS_CONTAINER_PROXY_MODE"] = "true" + + yield + + os.environ.clear() + os.environ.update(old_env) + + +def invoke_bot_command(args: list[str]): + """Invoke the bot command with the given arguments.""" + from conda_forge_tick import cli + + cli.main(args, standalone_mode=False) + + +@pytest.mark.parametrize("use_containers", [False, True]) +def test_scenario( + use_containers: bool, + scenario: tuple[int, dict[str, TestCase]], + repositories_setup, + mitmproxy, + request: pytest.FixtureRequest, +): + """ + Execute the test scenario given by the scenario fixture (note that the fixture is + parameterized, and therefore we run this for all scenarios). + All steps of the bot are executed in sequence to test its end-to-end functionality. + + A test scenario assigns one test case to each feedstock. For details on + the testing setup, please refer to the README.md in the tests_integration + (i.e., parent) directory. + + Parameters + ---------- + use_containers + Whether container mode is enabled or not. + scenario + The test scenario to run. This is a tuple of (scenario_id, scenario), + where scenario is a dictionary with the feedstock name as key and the test + case name as value. + repositories_setup + The fixture that sets up the repositories. + mitmproxy + The fixture that sets up the mitmproxy. + request + The pytest fixture request object. + """ + _, scenario = scenario + + if not use_containers: + request.getfixturevalue("disable_container_mode") + + with in_fresh_cf_graph(): + invoke_bot_command(["--debug", "gather-all-feedstocks"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + invoke_bot_command(["--debug", "make-graph", "--update-nodes-and-edges"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + invoke_bot_command(["--debug", "make-graph"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + with mitmproxy_env(): + invoke_bot_command(["--debug", "update-upstream-versions"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + with mitmproxy_env(): + invoke_bot_command(["--debug", "make-migrators"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + with mitmproxy_env(): + invoke_bot_command(["--debug", "auto-tick"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + # because of an implementation detail in the bot, we need to run make-migrators twice + # for changes to be picked up + with mitmproxy_env(): + invoke_bot_command(["--debug", "make-migrators"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + with in_fresh_cf_graph(): + # due to a similar implementation detail, we need to run auto-tick twice + # for changes to be picked up + with mitmproxy_env(): + invoke_bot_command(["--debug", "auto-tick"]) + invoke_bot_command(["--debug", "deploy-to-github"]) + + run_all_validate_functions(scenario)