From ba3ce178c223aec68667c5860f1244f7cc0c91a3 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Tue, 5 Aug 2025 11:12:42 +0200 Subject: [PATCH 01/32] feat: release_schedule workflow The release_schedule workflow should create the artifacts on this repository so other repositories can access it, and make sure we have a paper trail --- .github/workflows/release_schedule.yaml | 66 +++++++++++++++++++++++++ .gitignore | 3 -- 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/release_schedule.yaml diff --git a/.github/workflows/release_schedule.yaml b/.github/workflows/release_schedule.yaml new file mode 100644 index 0000000..e575c4a --- /dev/null +++ b/.github/workflows/release_schedule.yaml @@ -0,0 +1,66 @@ +name: Generate release schedule artifacts +on: + schedule: + # At 00:00 on day-of-month 1 in every 3rd month. (i.e. every quarter) + - cron: "0 0 1 */3 *" + # on demand + workflow_dispatch: + +jobs: + create-artifacts: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # We're going to make a tag that we can we release so we'll need the full history for that + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Install dependencies + run: | + pip install -r requirements.txt + - name: Run spec_zero_versions.py + run: | + python spec_zero_versions.py + - name: setup git + run : | + # git will complain if we don't do this first + git config user.name "GitHub Actions Bot" + git config user.email "<>" + + - name: determine tag name + id: tag_name + run: | + echo "TAG_NAME=$(date '+%Y-Q%q')" >> "$GITHUB_OUTPUT" + + + - name: create tag + env: + TAG_NAME: ${{ steps.tag_name.outputs.TAG_NAME }} + run: | + git add schedule.md chart.md schedule.json + git commit -m "Update SPEC 0 schedule artifacts" + git tag "$TAG_NAME" + git push origin "$TAG_NAME" + + - name: Publish github release + uses: softprops/action-gh-release@v2 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + generate_release_notes: true + tag_name: ${{ steps.tag_name.outputs.TAG_NAME }} + make_latest: true + files: | + schedule.md + chart.md + schedule.json + + + + diff --git a/.gitignore b/.gitignore index d7a22f5..8f5fd87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ .history -chart.md -schedule.md -schedule.json From 30e7fa7c677a42c4a808987270810c1ef4740abc Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Tue, 5 Aug 2025 16:48:59 +0200 Subject: [PATCH 02/32] feat: have action.yml update pixi environments Action should create a PR with updated dependencies. Target branch and tool to be used are configurable. Currently only pixi is supported but it's good to make it possible to add other tools down the line --- action.yaml | 70 ++++++++++++++++++++++++++++++++++++++++---------- update_pixi.sh | 13 ++++++++++ 2 files changed, 69 insertions(+), 14 deletions(-) create mode 100755 update_pixi.sh diff --git a/action.yaml b/action.yaml index ab54c11..742e21d 100644 --- a/action.yaml +++ b/action.yaml @@ -1,26 +1,68 @@ name: "Generate SPEC-0000 Data" description: "Based on the current SPEC 0 schedule, generate a tarball with the latest versions of all packages." +author: Sam Vente +inputs: + target_branch: + description: 'Target branch for the pull request' + required: true + default: 'main' + tool: # for now only pixi, but good to make others possible from the start + description: 'Which tool you use for managing your environment.' + required: true + default: 'pixi' + github_token: + description: 'GitHub token with repo permissions to create pull requests' + required: true + + + runs: using: "composite" steps: - - name: Set up Python - uses: actions/setup-python@v5 + - name: Validate tool input + shell: bash + run: | + if [[ "${{ inputs.tool }}" != "pixi" ]]; then + echo "❌ Invalid tool: '${{ inputs.tool }}'" + echo "Accepted values are: 'pixi'" + exit 1 + fi + - name: Checkout code + uses: actions/checkout@v4 with: - python-version: "3.13" - - name: Install dependencies + fetch-depth: 0 + + - name: Set up Git shell: bash run: | - pip install -r requirements.txt - - name: Run spec_zero_versions.py + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Download schedule artifact + shell: bash + run: gh release download -R "savente93/spec-zero-tools" --pattern schedule.json --clobber + + # make usre pixi is available + - uses: prefix-dev/setup-pixi@v0.8.14 + if: ${{ inputs.tool == 'pixi' }} + name: Setup pixi + with: + pixi-version: v0.49.0 + + - name: Update Pixi dependencies + if: ${{ inputs.tool == 'pixi' }} shell: bash run: | - python spec_zero_versions.py - - name: Upload files as an artifact - uses: actions/upload-artifact@v4 + "${{ github.action_path }}/update_pixi.sh" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 with: - name: spec-zero-versions - path: | - schedule.json - schedule.md - chart.md + token: ${{ inputs.github_token }} + commit-message: "chore: Drop support for unsupported packages conform SPEC 0" + title: "Drop support for unsupported packages conform SPEC 0" + body: "This PR was created automatically" + base: ${{ inputs.target_branch }} + branch: update-spec-0-dependencies-${{ github.run_id }} + diff --git a/update_pixi.sh b/update_pixi.sh new file mode 100755 index 0000000..e7fbe95 --- /dev/null +++ b/update_pixi.sh @@ -0,0 +1,13 @@ +#!/bin/bash + + +for line in $(jq 'map(select(.start_date |fromdateiso8601 |tonumber < now))| sort_by("start_date") | reverse | .[0].packages | to_entries | map(.key + ":" + .value)[]' --raw-output schedule.json); do + + package=$(echo "$line" | cut -d ':' -f 1) + spec_0_version=$(echo "$line" | cut -d ':' -f 2) + + if pixi list -x "^$package" 2> /dev/null | grep "No packages" -q -v; then + echo "Updating $package" + pixi add "$package>=$spec_0_version" + fi +done From 0cdba7a5df28ef6038bd444dbc124238e243b436 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Tue, 5 Aug 2025 19:26:56 +0200 Subject: [PATCH 03/32] docs: Update README --- action.yaml | 4 +-- readme.md | 75 ++++++++++++++++++++++++----------------------------- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/action.yaml b/action.yaml index 742e21d..eb39ff7 100644 --- a/action.yaml +++ b/action.yaml @@ -10,7 +10,7 @@ inputs: description: 'Which tool you use for managing your environment.' required: true default: 'pixi' - github_token: + token: description: 'GitHub token with repo permissions to create pull requests' required: true @@ -59,7 +59,7 @@ runs: - name: Create Pull Request uses: peter-evans/create-pull-request@v6 with: - token: ${{ inputs.github_token }} + token: ${{ inputs.token }} commit-message: "chore: Drop support for unsupported packages conform SPEC 0" title: "Drop support for unsupported packages conform SPEC 0" body: "This PR was created automatically" diff --git a/readme.md b/readme.md index 3918a9e..c8f010a 100644 --- a/readme.md +++ b/readme.md @@ -1,63 +1,56 @@ # SPEC-0 Versions Action -This repository contains a GitHub Action to generate the files required for the SPEC-0 documentation. +This repository contains a Github Action to update python dependencies conform the SPEC 0 support schedule. +It also contains released versions of the schedule in various formats that that action can use to open PRs in your repository. ## Using the action ```yaml -name: Generate spec-zero data +name: Update SPEC 0 dependencies on: - push: - branches: - - main + workflow_dispatch: + schedule: + # At 00:00 on day-of-month 2 in every 3rd month. (i.e. every quarter) + # Releases should happen on the first day of the month in scientific-python/spec-zero-tools + # so allow one day as a buffer to avoid timing issues + - cron: "0 0 2 */3 *" + +permissions: + contents: write + pull-requests: write jobs: - devstats-query: + update: runs-on: ubuntu-latest steps: - uses: scientific-python/spec-zero-tools@main + with: + token: ${{ secrets.GH_PAT }} + target_branch: main + tool: pixi + ``` -The above would produce an artifact named `spec-zero-versions`, the following files: `schedule.yaml`,`schedule.md` and `chart.md`. +Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound. For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes. Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). To help projects stay compliant with SPEC-0, we provide a `schedule.json` file that can be used by CI systems to determine new version boundaries. -The structure of the file is as follows: - -```json -[ - { - "start_date": "iso8601_timestamp", - "packages": { - "package_name": "version" - } - } -] -``` -All information in the json file is in a string format that should be easy to use. -The date is the first timestamp of the relevant quarter. -Thus a workflow for using this file could be: +Currently the action can take the following inputs: -1. Fetch `schedule.json` -2. Determine maximum date that is smaller than current date -3. Update packages listed with new minimum versions +| Name | Description | Required | +|---------------|-------------------------------------------------------------------------------------------------------------|----------| +| `token` | The token that the action will use to create and update the pull request. See [token](https://github.com/marketplace/actions/create-pull-request#token). | Yes | +| `tool` | Which tool to use for managing your dependencies. Currently `pixi` is the only option. | No | +| `target_branch` | The branch to open a PR against with the updated versions. Defaults to `main`. | No | -You can obtain the new versions you should set by using this `jq` expression: -```sh -jq 'map(select(.start_date |fromdateiso8601 |tonumber < now))| sort_by("start_date") | reverse | .[0].packages ' schedule.json -``` +## Limitations + +This project is still in progress and thus it comes with some limitations we are working on. Hopefully this will be gone by the time you read this, but currently the limitations are: + +- Only `pixi` is supported +- if you have a higher bound than the one listed in SPEC 0 this is overwritten +- higher bounds are deleted instead of maintained. +- dependency groups are not yet supported -If you use a package manager like pixi you could update the dependencies with a bash script like this (untested): - -```sh -curl -Ls -o schedule.json https://raw.githubusercontent.com/scientific-python/specs/main/spec-0000/schedule.json -for line in $(jq 'map(select(.start_date |fromdateiso8601 |tonumber < now))| sort_by("start_date") | reverse | .[0].packages | to_entries | map(.key + ":" + .value)[]' --raw-output schedule.json); do - package=$(echo "$line" | cut -d ':' -f 1) - version=$(echo "$line" | cut -d ':' -f 2) - if pixi list -x "^$package" &>/dev/null| grep "No packages" -q; then - pixi add "$package>=$version"; - fi -done -``` From 2a2b9c04e5059ce668d412a60516f21a9b4c15ff Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Tue, 5 Aug 2025 19:49:04 +0200 Subject: [PATCH 04/32] fix: remove now defunct test_action.yaml --- .github/workflows/release_schedule.yaml | 21 ++++++++------------- action.yaml | 16 ++++++---------- readme.md | 19 ++++++++----------- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/.github/workflows/release_schedule.yaml b/.github/workflows/release_schedule.yaml index e575c4a..751bd25 100644 --- a/.github/workflows/release_schedule.yaml +++ b/.github/workflows/release_schedule.yaml @@ -10,13 +10,13 @@ jobs: create-artifacts: runs-on: ubuntu-latest permissions: - contents: write + contents: write steps: - name: Checkout uses: actions/checkout@v4 - with: - # We're going to make a tag that we can we release so we'll need the full history for that - fetch-depth: 0 + with: + # We're going to make a tag that we can we release so we'll need the full history for that + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: @@ -28,7 +28,7 @@ jobs: run: | python spec_zero_versions.py - name: setup git - run : | + run: | # git will complain if we don't do this first git config user.name "GitHub Actions Bot" git config user.email "<>" @@ -38,10 +38,9 @@ jobs: run: | echo "TAG_NAME=$(date '+%Y-Q%q')" >> "$GITHUB_OUTPUT" - - name: create tag env: - TAG_NAME: ${{ steps.tag_name.outputs.TAG_NAME }} + TAG_NAME: ${{ steps.tag_name.outputs.TAG_NAME }} run: | git add schedule.md chart.md schedule.json git commit -m "Update SPEC 0 schedule artifacts" @@ -51,16 +50,12 @@ jobs: - name: Publish github release uses: softprops/action-gh-release@v2 env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: generate_release_notes: true - tag_name: ${{ steps.tag_name.outputs.TAG_NAME }} + tag_name: ${{ steps.tag_name.outputs.TAG_NAME }} make_latest: true files: | schedule.md chart.md schedule.json - - - - diff --git a/action.yaml b/action.yaml index eb39ff7..de974b4 100644 --- a/action.yaml +++ b/action.yaml @@ -3,20 +3,17 @@ description: "Based on the current SPEC 0 schedule, generate a tarball with the author: Sam Vente inputs: target_branch: - description: 'Target branch for the pull request' + description: "Target branch for the pull request" required: true - default: 'main' + default: "main" tool: # for now only pixi, but good to make others possible from the start - description: 'Which tool you use for managing your environment.' + description: "Which tool you use for managing your environment." required: true - default: 'pixi' + default: "pixi" token: - description: 'GitHub token with repo permissions to create pull requests' + description: "GitHub token with repo permissions to create pull requests" required: true - - - runs: using: "composite" steps: @@ -43,7 +40,7 @@ runs: shell: bash run: gh release download -R "savente93/spec-zero-tools" --pattern schedule.json --clobber - # make usre pixi is available + # make user pixi is available - uses: prefix-dev/setup-pixi@v0.8.14 if: ${{ inputs.tool == 'pixi' }} name: Setup pixi @@ -65,4 +62,3 @@ runs: body: "This PR was created automatically" base: ${{ inputs.target_branch }} branch: update-spec-0-dependencies-${{ github.run_id }} - diff --git a/readme.md b/readme.md index c8f010a..4313421 100644 --- a/readme.md +++ b/readme.md @@ -25,11 +25,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: scientific-python/spec-zero-tools@main - with: + with: token: ${{ secrets.GH_PAT }} target_branch: main tool: pixi - ``` Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound. For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes. Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). @@ -38,19 +37,17 @@ To help projects stay compliant with SPEC-0, we provide a `schedule.json` file t Currently the action can take the following inputs: -| Name | Description | Required | -|---------------|-------------------------------------------------------------------------------------------------------------|----------| -| `token` | The token that the action will use to create and update the pull request. See [token](https://github.com/marketplace/actions/create-pull-request#token). | Yes | -| `tool` | Which tool to use for managing your dependencies. Currently `pixi` is the only option. | No | -| `target_branch` | The branch to open a PR against with the updated versions. Defaults to `main`. | No | - +| Name | Description | Required | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `token` | The token that the action will use to create and update the pull request. See [token](https://github.com/marketplace/actions/create-pull-request#token). | Yes | +| `tool` | Which tool to use for managing your dependencies. Currently `pixi` is the only option. | No | +| `target_branch` | The branch to open a PR against with the updated versions. Defaults to `main`. | No | ## Limitations -This project is still in progress and thus it comes with some limitations we are working on. Hopefully this will be gone by the time you read this, but currently the limitations are: +This project is still in progress and thus it comes with some limitations we are working on. Hopefully this will be gone by the time you read this, but currently the limitations are: - Only `pixi` is supported - if you have a higher bound than the one listed in SPEC 0 this is overwritten -- higher bounds are deleted instead of maintained. +- higher bounds are deleted instead of maintained. - dependency groups are not yet supported - From e6912aad2d32490a03bc3963c64e897d31a7ab48 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:23:29 +0200 Subject: [PATCH 05/32] feat: add python module with helper functions and tests --- .gitattributes | 2 + .gitignore | 4 + pixi.lock | 1030 ++++++++++++++++++++++++ pyproject.toml | 36 + requirements.txt | 3 - spec_zero_tools/__init__.py | 118 +++ spec_zero_tools/parsing.py | 79 ++ spec_zero_tools/versions.py | 28 + spec_zero_versions.py | 207 ----- tests/test_data/pyproject.toml | 29 + tests/test_data/pyproject_updated.toml | 29 + tests/test_data/test_schedule.json | 1 + tests/test_parsing.py | 55 ++ tests/test_update_pyproject_toml.py | 11 + tests/test_versions.py | 29 + 15 files changed, 1451 insertions(+), 210 deletions(-) create mode 100644 .gitattributes create mode 100644 pixi.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 spec_zero_tools/__init__.py create mode 100644 spec_zero_tools/parsing.py create mode 100644 spec_zero_tools/versions.py delete mode 100644 spec_zero_versions.py create mode 100644 tests/test_data/pyproject.toml create mode 100644 tests/test_data/pyproject_updated.toml create mode 100644 tests/test_data/test_schedule.json create mode 100644 tests/test_parsing.py create mode 100644 tests/test_update_pyproject_toml.py create mode 100644 tests/test_versions.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..887a2c1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/.gitignore b/.gitignore index 8f5fd87..435b4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ .history +# pixi environments +.pixi/* +!.pixi/config.toml +__pycache__ diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..495f564 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,1030 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + dev: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-33_h59b9bed_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-33_he106b2a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-33_h7ac8fdf_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py312h33ff503_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda + - pypi: ./ + schedule: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-33_h59b9bed_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-33_he106b2a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-33_h7ac8fdf_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py312h33ff503_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + update: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: ./ +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda + sha256: dc27c58dc717b456eee2d57d8bc71df3f562ee49368a2351103bc8f1b67da251 + md5: a32e0c069f6c3dcac635f7b0b0dac67e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - libbrotlicommon 1.1.0 hb9d3cd8_3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 351721 + timestamp: 1749230265727 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + md5: 62ee74e96c5ebb0af99386de58cf9553 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 252783 + timestamp: 1720974456583 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 + md5: 74784ee3d225fc3dca89edb635b4e5cc + depends: + - __unix + license: ISC + purls: [] + size: 154402 + timestamp: 1754210968730 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda + sha256: a1ad5b0a2a242f439608f22a538d2175cac4444b7b3f4e2b8c090ac337aaea40 + md5: 11f59985f49df4620890f3e746ed7102 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 158692 + timestamp: 1754231530168 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda + sha256: cba6ea83c4b0b4f5b5dc59cb19830519b28f95d7ebef7c9c5cf1c14843621457 + md5: a861504bbea4161a9170b85d4d2be840 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.4,<4.0a0 + - libgcc >=13 + - pycparser + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 294403 + timestamp: 1725560714366 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + sha256: 535ae5dcda8022e31c6dc063eb344c80804c537a5a04afba43a845fa6fa130f5 + md5: 40fe4284b8b5835a9073a645139f35af + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=hash-mapping + size: 50481 + timestamp: 1746214981991 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + sha256: ce61f4f99401a4bd455b89909153b40b9c823276aefcbb06f2044618696009ca + md5: 72e42d28960d875c7654614f8b50939a + depends: + - python >=3.9 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21284 + timestamp: 1746947398083 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + sha256: 0aa1cdc67a9fe75ea95b5644b734a756200d6ec9d0dff66530aec3d1c1e9df75 + md5: b4754fb1bdcb70c8fd54f918301582c6 + depends: + - hpack >=4.1,<5 + - hyperframe >=6.1,<7 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 53888 + timestamp: 1738578623567 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 + md5: 39a4f67be3286c86d696df570b1201b7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping + size: 49765 + timestamp: 1733211921194 +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + sha256: 0ec8f4d02053cd03b0f3e63168316530949484f80e16f5e2fb199a1d117a89ca + md5: 6837f3eff7dcea42ecd714ce1ac2b108 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=hash-mapping + size: 11474 + timestamp: 1733223232820 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 + md5: 0be7c6e070c19105f966d3758448d018 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - binutils_impl_linux-64 2.44 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 676044 + timestamp: 1752032747103 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-33_h59b9bed_openblas.conda + build_number: 33 + sha256: 18b165b45f3cea3892fee25a91b231abf29f521df76c8fcc0c92f6cf5071a911 + md5: b43d5de8fe73c2a5fb2b43f45301285b + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - mkl <2025 + - liblapack 3.9.0 33*_openblas + - blas 2.133 openblas + - liblapacke 3.9.0 33*_openblas + - libcblas 3.9.0 33*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18778 + timestamp: 1754412356514 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-33_he106b2a_openblas.conda + build_number: 33 + sha256: b34271bb9c3b2377ac23c3fb1ecf45c08f8d09675ff8aad860f4e3c547b126a7 + md5: 28052b5e6ea5bd283ac343c5c064b950 + depends: + - libblas 3.9.0 33_h59b9bed_openblas + constrains: + - liblapack 3.9.0 33*_openblas + - liblapacke 3.9.0 33*_openblas + - blas 2.133 openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18748 + timestamp: 1754412369555 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 + md5: 4211416ecba1866fab0c6470986c22d6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 74811 + timestamp: 1752719572741 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab + md5: ede4673863426c0883c0063d853bbd85 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 57433 + timestamp: 1743434498161 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + sha256: 144e35c1c2840f2dc202f6915fc41879c19eddbb8fa524e3ca4aa0d14018b26f + md5: f406dcbb2e7bef90d793e50e79a2882b + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.1.0=*_4 + - libgomp 15.1.0 h767d61c_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 824153 + timestamp: 1753903866511 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + sha256: 76ceac93ed98f208363d6e9c75011b0ff7b97b20f003f06461a619557e726637 + md5: 28771437ffcd9f3417c66012dc49a3be + depends: + - libgcc 15.1.0 h767d61c_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29249 + timestamp: 1753903872571 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + sha256: 2fe41683928eb3c57066a60ec441e605a69ce703fc933d6d5167debfeba8a144 + md5: 53e876bc2d2648319e94c33c57b9ec74 + depends: + - libgfortran5 15.1.0 hcea5267_4 + constrains: + - libgfortran-ng ==15.1.0=*_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29246 + timestamp: 1753903898593 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + sha256: 3070e5e2681f7f2fb7af0a81b92213f9ab430838900da8b4f9b8cf998ddbdd84 + md5: 8a4ab7ff06e4db0be22485332666da0f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.1.0 + constrains: + - libgfortran 15.1.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1564595 + timestamp: 1753903882088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + sha256: e0487a8fec78802ac04da0ac1139c3510992bc58a58cde66619dde3b363c2933 + md5: 3baf8976c96134738bba224e9ef6b1e5 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 447289 + timestamp: 1753903801049 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-33_h7ac8fdf_openblas.conda + build_number: 33 + sha256: 60c2ccdfa181bc304b5162c73cdecb5b4c3972da71758472c71fefb33965cde3 + md5: e598bb54c4a4b45c3d83c72984f79dbb + depends: + - libblas 3.9.0 33_h59b9bed_openblas + constrains: + - liblapacke 3.9.0 33*_openblas + - blas 2.133 openblas + - libcblas 3.9.0 33*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18785 + timestamp: 1754412383434 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee + md5: c7e925f37e3b40d893459e625f6a53f1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 91183 + timestamp: 1748393666725 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + sha256: 3f3fc30fe340bc7f8f46fea6a896da52663b4d95caed1f144e8ea114b4bb6b61 + md5: 7e2ba4ca7e6ffebb7f7fc2da2744df61 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5918161 + timestamp: 1753405234435 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da + md5: 0b367fad34931cb79e0d6b7e5c06bb1c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 932581 + timestamp: 1753948484112 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + sha256: b5b239e5fca53ff90669af1686c86282c970dd8204ebf477cf679872eb6d48ac + md5: 3c376af8888c386b9d3d1c2701e2f3ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.1.0 h767d61c_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3903453 + timestamp: 1753903894186 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + depends: + - libgcc-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 33601 + timestamp: 1680112270483 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py312h33ff503_0.conda + sha256: d54e52df67e0be7e5faa9e6f0efccea3d72f635a3159cc151c4668e5159f6ef3 + md5: 3f6efbc40eb13f019c856c410fa921d2 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - libblas >=3.9.0,<4.0a0 + - python_abi 3.12.* *_cp312 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=hash-mapping + size: 8785045 + timestamp: 1753401550884 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e + md5: ffffb341206dd0dab0c36053c048d621 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3128847 + timestamp: 1754465526100 +- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + name: packaging + version: '25.0' + sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + sha256: 289861ed0c13a15d7bbb408796af4de72c2fe67e2bcb0de98f4c3fce259d7991 + md5: 58335b26c38bf4a20f399384c33cbcf9 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=hash-mapping + size: 62477 + timestamp: 1745345660407 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda + sha256: 6ec86b1da8432059707114270b9a45d767dac97c4910ba82b1f4fa6f74e077c8 + md5: 7c73e62e62e5864b8418440e2a2cc246 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.12,<3.13.0a0 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.12.* *_cp312 + - pytz >=2020.1 + constrains: + - html5lib >=1.1 + - fastparquet >=2022.12.0 + - xarray >=2022.12.0 + - pyqt5 >=5.15.9 + - pyxlsb >=1.0.10 + - matplotlib >=3.6.3 + - numba >=0.56.4 + - odfpy >=1.4.1 + - bottleneck >=1.3.6 + - tabulate >=0.9.0 + - scipy >=1.10.0 + - pyreadstat >=1.2.0 + - pandas-gbq >=0.19.0 + - openpyxl >=3.1.0 + - xlrd >=2.0.1 + - pyarrow >=10.0.1 + - xlsxwriter >=3.0.5 + - python-calamine >=0.1.7 + - gcsfs >=2022.11.0 + - zstandard >=0.19.0 + - fsspec >=2022.11.0 + - lxml >=4.9.2 + - s3fs >=2022.11.0 + - numexpr >=2.8.4 + - psycopg2 >=2.9.6 + - qtpy >=2.3.0 + - pytables >=3.8.0 + - tzdata >=2022.7 + - sqlalchemy >=2.0.0 + - beautifulsoup4 >=4.11.2 + - blosc >=1.21.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + size: 15092371 + timestamp: 1752082221274 +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + sha256: a8eb555eef5063bbb7ba06a379fa7ea714f57d9741fe0efdb9442dbbc2cccbcc + md5: 7da7ccd349dbf6487a7778579d2bb971 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pluggy?source=hash-mapping + size: 24246 + timestamp: 1747339794916 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=hash-mapping + size: 110100 + timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a + md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + size: 889287 + timestamp: 1750615908735 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + sha256: 93e267e4ec35353e81df707938a6527d5eb55c97bf54c3b87229b69523afb59d + md5: a49c2283f24696a7b30367b7346a0144 + depends: + - colorama >=0.4 + - exceptiongroup >=1 + - iniconfig >=1 + - packaging >=20 + - pluggy >=1.5,<2 + - pygments >=2.7.2 + - python >=3.9 + - tomli >=1 + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=hash-mapping + size: 276562 + timestamp: 1750239526127 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda + sha256: 6cca004806ceceea9585d4d655059e951152fc774a471593d4f5138e6a54c81d + md5: 94206474a5608243a10c92cefbe0908f + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - liblzma >=5.8.1,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.50.0,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 31445023 + timestamp: 1749050216615 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + build_number: 102 + sha256: c2cdcc98ea3cbf78240624e4077e164dc9d5588eefb044b4097c3df54d24d504 + md5: 89e07d92cf50743886f41638d58c4328 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.50.1,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.0,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33273132 + timestamp: 1750064035176 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + sha256: e8392a8044d56ad017c08fec2b0eb10ae3d1235ac967d0aab8bd7b41c4a5eaf0 + md5: 88476ae6ebd24f39261e0854ac244f33 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=hash-mapping + size: 144160 + timestamp: 1742745254292 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + build_number: 8 + sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 + md5: c3efd25ac4d74b1584d2f7a57195ddf1 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6958 + timestamp: 1752805918820 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0 + md5: bc8e3267d44011051f2eb14d22fb0960 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytz?source=hash-mapping + size: 189015 + timestamp: 1742920947249 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 282480 + timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + sha256: 9866aaf7a13c6cfbe665ec7b330647a0fb10a81e6f9b8fee33642232a1920e18 + md5: f6082eae112814f1447b56a5e1f6ed05 + depends: + - certifi >=2017.4.17 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - python >=3.9 + - urllib3 >=1.21.1,<3 + constrains: + - chardet >=3.0.2,<6 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=hash-mapping + size: 59407 + timestamp: 1749498221996 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + size: 18455 + timestamp: 1753199211006 +- pypi: ./ + name: spec-zero-tools + version: 0.1.0 + sha256: f055138b9e6c5fa0f9a4be072f21e62c26b0b07e159e5376092dbb35d86da202 + requires_dist: + - packaging>=25.0,<26 + requires_python: '>=3.11' + editable: true +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 + md5: a0116df4f4ed05c303811a837d5b39d8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3285204 + timestamp: 1748387766691 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + sha256: 040a5a05c487647c089ad5e05ad5aff5942830db2a4e656f1e300d73436436f1 + md5: 30a0a26c8abccf4b7991d590fe17c699 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=compressed-mapping + size: 21238 + timestamp: 1753796677376 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda + sha256: f8d3b49c084831a20923f66826f30ecfc55a4cd951e544b7213c692887343222 + md5: 146402bf0f11cbeb8f781fa4309a95d3 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomlkit?source=hash-mapping + size: 38777 + timestamp: 1749127286558 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + sha256: 4f52390e331ea8b9019b87effaebc4f80c6466d09f68453f52d5cdc2a3e1194f + md5: e523f4f1e980ed7a4240d7e27e9ec81f + depends: + - python >=3.9 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51065 + timestamp: 1751643513473 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a + license: LicenseRef-Public-Domain + purls: [] + size: 122968 + timestamp: 1742727099393 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 + md5: 436c165519e140cb08d246a4472a9d6a + depends: + - brotli-python >=1.0.9 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.9 + - zstandard >=0.18.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + size: 101735 + timestamp: 1750271478254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda + sha256: ff62d2e1ed98a3ec18de7e5cf26c0634fd338cb87304cf03ad8cbafe6fe674ba + md5: 630db208bc7bbb96725ce9832c7423bb + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.11 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + size: 732224 + timestamp: 1745869780524 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e0568ed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] +name = "spec-zero-tools" +requires-python = ">= 3.11" +version = "0.1.0" +dependencies = ["packaging>=25.0,<26"] + + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.feature.update.pypi-dependencies] +spec_zero_tools = { path = ".", editable = true } + +[tool.pixi.feature.test.tasks] +test = { cmd = ["pytest", "-vvv"] } + +[tool.pixi.feature.test.dependencies] +pytest = "*" + +[tool.pixi.feature.update.dependencies] +tomlkit = ">=0.13.3,<0.14" + +[tool.pixi.feature.schedule.dependencies] +pandas = "*" +requests = "*" + +[tool.pixi.environments] +dev = ["test", "schedule", "update"] +schedule = ["schedule"] +update = ["update"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ccaba93..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -packaging -pandas -requests diff --git a/spec_zero_tools/__init__.py b/spec_zero_tools/__init__.py new file mode 100644 index 0000000..2f6e018 --- /dev/null +++ b/spec_zero_tools/__init__.py @@ -0,0 +1,118 @@ +from packaging.specifiers import SpecifierSet +from typing import Sequence, cast +import datetime + +from spec_zero_tools.versions import repr_spec_set, tighten_lower_bound +from spec_zero_tools.parsing import ( + SupportSchedule, + Url, + is_url_spec, + parse_pep_dependency, + parse_version_spec, + read_schedule, + read_toml, + write_toml +) +from packaging.version import Version + + +__all__ = ["read_schedule", "read_toml", "write_toml", "update_pyproject_toml"] + +def update_pyproject_dependencies(dependencies: dict, schedule: SupportSchedule): + # iterate by idx because we want to update it inplace + for i in range(len(dependencies)): + dep_str = dependencies[i] + pkg, spec = parse_pep_dependency(dep_str) + + if isinstance(spec, Url) or pkg not in schedule["packages"]: + continue + + new_lower_bound = Version(schedule["packages"][pkg]) + try: + spec = tighten_lower_bound(spec or SpecifierSet(), new_lower_bound) + # will raise a value error if bound is already tigheter, in this case we just do nothing and continue + + + except ValueError: + continue + + new_dep_str = f"{pkg}{repr_spec_set(spec)}" + + dependencies[i] = new_dep_str + + +def update_dependency_table(dep_table: dict, new_versions: dict): + for pkg, pkg_data in dep_table.items(): + # don't do anything for pkgs that aren't in our schedule + if pkg not in new_versions: + continue + + # like pkg = ">x.y.z,= datetime.datetime.fromisoformat(s["start_date"]), + schedule_data, + ) + ), + ) + except StopIteration: + raise RuntimeError( + "Could not find schedule that applies to current time, perhaps your schedule is oudated." + ) + + if "python" in new_version["packages"]: + pyproject_data["project"]["requires-python"] = repr_spec_set( + parse_version_spec(new_version["packages"]["python"]) + ) + + update_pyproject_dependencies( + pyproject_data["project"]["dependencies"], new_version + ) + + if "tool" in pyproject_data and "pixi" in pyproject_data['tool']: + pixi_data = pyproject_data["tool"]["pixi"] + update_pixi_dependencies(pixi_data, new_version) diff --git a/spec_zero_tools/parsing.py b/spec_zero_tools/parsing.py new file mode 100644 index 0000000..b9858be --- /dev/null +++ b/spec_zero_tools/parsing.py @@ -0,0 +1,79 @@ +from typing import TypeAlias +from urllib.parse import ParseResult, urlparse +from tomlkit import dumps, loads +import json +from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.version import InvalidVersion, Version +from typing import Dict, Sequence, Tuple, TypedDict +from pathlib import Path +from re import compile + +# we won't actually do anything with URLs we just need to detect them +Url: TypeAlias = ParseResult + +# slightly modified version of https://packaging.python.org/en/latest/specifications/dependency-specifiers/#names +PEP_PACKAGE_IDENT_RE = compile(r"(?im)^([A-Z0-9][A-Z0-9._-]*[A-Z0-9])(.*)$") + + +class SupportSchedule(TypedDict): + start_date: str + packages: Dict[str, str] + + +def parse_version_spec(s: str) -> SpecifierSet: + if s.strip() == "*": + # python version numeric components must be non-negative so this is ookay + # see https://packaging.python.org/en/latest/specifications/version-specifiers/ + return SpecifierSet(">=0") + try: + # if we can simply parse it return it + return SpecifierSet(s) + except InvalidSpecifier: + try: + ver = Version(s) + except InvalidVersion: + raise ValueError(f"{s} is not a version or specifyer") + + return SpecifierSet(f">={ver}") + + +def write_toml(path: Path | str, data: dict): + with open(path, "w") as file: + contents = dumps(data) + file.write(contents) + + +def read_toml(path: Path | str) -> dict: + with open(path, "r") as file: + contents = file.read() + return loads(contents) + + +def read_schedule(path: Path | str) -> Sequence[SupportSchedule]: + with open(path, "r") as file: + return json.load(file) + + +def parse_pep_dependency(dep_str: str) -> Tuple[str, SpecifierSet | Url | None]: + match = PEP_PACKAGE_IDENT_RE.match(dep_str) + if match is None: + raise ValueError("Could not find any valid python package identifier") + + match_groups = match.groups() + + pkg = match_groups[0] + # capture group could be empty + if len(match_groups) > 1 and match_groups[1]: + spec_str = match_groups[1] + if is_url_spec(spec_str): + spec = urlparse(spec_str.split("@")[1]) + else: + spec = SpecifierSet(spec_str) + else: + spec = None + + return (pkg, spec) + + +def is_url_spec(str_spec: str) -> bool: + return str_spec.strip().startswith("@") diff --git a/spec_zero_tools/versions.py b/spec_zero_tools/versions.py new file mode 100644 index 0000000..f88748e --- /dev/null +++ b/spec_zero_tools/versions.py @@ -0,0 +1,28 @@ +from packaging.version import Version +from packaging.specifiers import Specifier, SpecifierSet + + +def tighten_lower_bound( + spec_set: SpecifierSet, new_lower_bound: Version +) -> SpecifierSet: + out = [] + contains_lower_bound = False + + for spec in spec_set: + if spec.operator not in [">", ">="]: + out.append(spec) + continue + if new_lower_bound in spec: + out.append(Specifier(f">={new_lower_bound}")) + contains_lower_bound = True + else: + raise ValueError(f"{spec} is already stricter than {new_lower_bound}") + + if not contains_lower_bound: + out.append(Specifier(f">={new_lower_bound}")) + + return SpecifierSet(out) + + +def repr_spec_set(spec: SpecifierSet) -> str: + return ",".join(sorted(map(str, spec), reverse=True)).replace(" ", "") diff --git a/spec_zero_versions.py b/spec_zero_versions.py deleted file mode 100644 index 8786f45..0000000 --- a/spec_zero_versions.py +++ /dev/null @@ -1,207 +0,0 @@ -import requests -import json -import collections -from datetime import datetime, timedelta - -import pandas as pd -from packaging.version import Version - - -PY_RELEASES = { - "3.8": "Oct 14, 2019", - "3.9": "Oct 5, 2020", - "3.10": "Oct 4, 2021", - "3.11": "Oct 24, 2022", - "3.12": "Oct 2, 2023", - "3.13": "Oct 7, 2024", -} -CORE_PACKAGES = [ - "ipython", - "matplotlib", - "networkx", - "numpy", - "pandas", - "scikit-image", - "scikit-learn", - "scipy", - "xarray", - "zarr", -] -PLUS_36_MONTHS = timedelta(days=int(365 * 3)) -PLUS_24_MONTHS = timedelta(days=int(365 * 2)) - -# Release data -# We put the cutoff at 3 quarters ago - we do not use "just" -9 months -# to avoid the content of the quarter to change depending on when we -# generate this file during the current quarter. -CURRENT_DATE = pd.Timestamp.now() -CURRENT_QUARTER_START = pd.Timestamp( - CURRENT_DATE.year, (CURRENT_DATE.quarter - 1) * 3 + 1, 1 -) -CUTOFF = CURRENT_QUARTER_START - pd.DateOffset(months=9) - - -def get_release_dates(package, support_time=PLUS_24_MONTHS): - releases = {} - print(f"Querying pypi.org for {package} versions...", end="", flush=True) - response = requests.get( - f"https://pypi.org/simple/{package}", - headers={"Accept": "application/vnd.pypi.simple.v1+json"}, - ).json() - print("OK") - file_date = collections.defaultdict(list) - for f in response["files"]: - if f["filename"].endswith(".tar.gz") or f["filename"].endswith(".zip"): - continue - ver = f["filename"].split("-")[1] - try: - version = Version(ver) - except Exception: - continue - if version.is_prerelease or version.micro != 0: - continue - release_date = pd.Timestamp(f["upload-time"]).tz_localize(None) - if not release_date: - continue - file_date[version].append(release_date) - release_date = {v: min(file_date[v]) for v in file_date} - for ver, release_date in sorted(release_date.items()): - drop_date = release_date + support_time - if drop_date >= CUTOFF: - releases[ver] = { - "release_date": release_date, - "drop_date": drop_date, - } - return releases - - -package_releases = { - "python": { - version: { - "release_date": datetime.strptime(release_date, "%b %d, %Y"), - "drop_date": datetime.strptime(release_date, "%b %d, %Y") + PLUS_36_MONTHS, - } - for version, release_date in PY_RELEASES.items() - } -} -package_releases |= {package: get_release_dates(package) for package in CORE_PACKAGES} -# Filter all items whose drop_date are in the past -package_releases = { - package: { - version: dates - for version, dates in releases.items() - if dates["drop_date"] > CUTOFF - } - for package, releases in package_releases.items() -} - -# Save Gantt chart -print("Saving Mermaid chart to chart.md") -with open("chart.md", "w") as fh: - fh.write( - """gantt -dateFormat YYYY-MM-DD -axisFormat %m / %Y -title Support Window""" - ) - for name, releases in package_releases.items(): - fh.write(f"\n\nsection {name}") - for version, dates in releases.items(): - fh.write( - f"\n{version} : {dates['release_date'].strftime('%Y-%m-%d')},{dates['drop_date'].strftime('%Y-%m-%d')}" - ) - fh.write("\n") - -# Print drop schedule -data = [] -for k, versions in package_releases.items(): - for v, dates in versions.items(): - data.append( - ( - k, - v, - pd.to_datetime(dates["release_date"]), - pd.to_datetime(dates["drop_date"]), - ) - ) -df = pd.DataFrame(data, columns=["package", "version", "release", "drop"]) -df["quarter"] = df["drop"].dt.to_period("Q") -df["new_min_version"] = ( - df[["package", "version", "quarter"]].groupby("package").shift(-1)["version"] -) -dq = df.set_index(["quarter", "package"]).sort_index().dropna() -new_min_versions = ( - dq.groupby(["quarter", "package"]).agg({"new_min_version": "max"}).reset_index() -) - -# We want to build a dict with the structure [{start_date: timestamp, packages: {package: lower_bound}}] -new_min_versions_list = [] -for q, packages in new_min_versions.groupby("quarter"): - package_lower_bounds = { - p: str(v) for p, v in packages.drop("quarter", axis=1).itertuples(index=False) - } - # jq is really insistent the Z should be there - quarter_start_time_str = str(q.start_time.isoformat()) + "Z" - new_min_versions_list.append( - {"start_date": quarter_start_time_str, "packages": package_lower_bounds} - ) -print("Saving drop schedule to schedule.json") -with open("schedule.json", "w") as f: - f.write(json.dumps(new_min_versions_list, sort_keys=True)) - - -def pad_table(table): - rows = [[el.strip() for el in row.split("|")] for row in table] - col_widths = [max(map(len, column)) for column in zip(*rows)] - rows[1] = [ - el if el != "----" else "-" * col_widths[i] for i, el in enumerate(rows[1]) - ] - padded_table = [] - for row in rows: - line = "" - for entry, width in zip(row, col_widths): - if not width: - continue - line += f"| {str.ljust(entry, width)} " - line += "|" - padded_table.append(line) - return padded_table - - -def make_table(sub): - table = [] - table.append("| | | |") - table.append("|----|----|----|") - for package in sorted(set(sub.index.get_level_values(0))): - vers = sub.loc[[package]]["version"] - minv, maxv = min(vers), max(vers) - rels = sub.loc[[package]]["release"] - rel_min, rel_max = min(rels), max(rels) - version_range = str(minv) if minv == maxv else f"{minv} to {maxv}" - rel_range = ( - str(rel_min.strftime("%b %Y")) - if rel_min == rel_max - else f"{rel_min.strftime('%b %Y')} and {rel_max.strftime('%b %Y')}" - ) - table.append(f"|{package:<15}|{version_range:<19}|released {rel_range}|") - return pad_table(table) - - -def make_quarter(quarter, dq): - table = ["#### " + str(quarter).replace("Q", " - Quarter ") + ":\n"] - table.append("###### Recommend drop support for:\n") - sub = dq.loc[quarter] - table.extend(make_table(sub)) - return "\n".join(table) - - -print("Saving drop schedule to schedule.md") -with open("schedule.md", "w") as fh: - # We collect packages 6 month in the past, and drop the first quarter - # as we might have filtered some of the packages out depending on - # when we ran the script. - tb = [] - for quarter in list(sorted(set(dq.index.get_level_values(0))))[1:]: - tb.append(make_quarter(quarter, dq)) - fh.write("\n\n".join(tb)) - fh.write("\n") diff --git a/tests/test_data/pyproject.toml b/tests/test_data/pyproject.toml new file mode 100644 index 0000000..ff34941 --- /dev/null +++ b/tests/test_data/pyproject.toml @@ -0,0 +1,29 @@ +[project] +authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] +name = "tests" +requires-python = ">=3.10" +version = "0.1.0" +dependencies = ["ipython>=8.7.0,<4"] + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.pypi-dependencies] +tests = { path = ".", editable = true } +scikit-learn = ">=1.2.0" + +[tool.pixi.tasks] + +[tool.pixi.feature.foo.dependencies] +xarray = "*" + +[tool.pixi.environments] +bar = ["foo"] + +[tool.pixi.dependencies] +numpy = ">=1.10.0,<2" diff --git a/tests/test_data/pyproject_updated.toml b/tests/test_data/pyproject_updated.toml new file mode 100644 index 0000000..bdd191f --- /dev/null +++ b/tests/test_data/pyproject_updated.toml @@ -0,0 +1,29 @@ +[project] +authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] +name = "tests" +requires-python = ">=3.11" +version = "0.1.0" +dependencies = ["ipython>=8.8.0,<4"] + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.pypi-dependencies] +tests = { path = ".", editable = true } +scikit-learn = ">=1.3.0" + +[tool.pixi.tasks] + +[tool.pixi.feature.foo.dependencies] +xarray = ">=2023.1.0" + +[tool.pixi.environments] +bar = ["foo"] + +[tool.pixi.dependencies] +numpy = ">=1.25.0,<2" diff --git a/tests/test_data/test_schedule.json b/tests/test_data/test_schedule.json new file mode 100644 index 0000000..ec6c35e --- /dev/null +++ b/tests/test_data/test_schedule.json @@ -0,0 +1 @@ +[{"packages": {"ipython": "8.8.0", "numpy": "1.25.0", "python": "3.11", "scikit-learn": "1.3.0", "xarray": "2023.1.0"}, "start_date": "2024-10-01T00:00:00Z"}, {"packages": {"ipython": "8.13.0", "matplotlib": "3.8.0", "networkx": "3.1", "scikit-image": "0.21.0", "scipy": "1.11.0", "xarray": "2023.4.0", "zarr": "2.15.0"}, "start_date": "2025-01-01T00:00:00Z"}, {"packages": {"ipython": "8.15.0", "networkx": "3.2", "numpy": "1.26.0", "pandas": "2.1.0", "scikit-image": "0.22.0", "scikit-learn": "1.4.0", "scipy": "1.12.0", "xarray": "2023.7.0", "zarr": "2.16.0"}, "start_date": "2025-04-01T00:00:00Z"}, {"packages": {"ipython": "8.17.0", "matplotlib": "3.9.0", "numpy": "2.0.0", "pandas": "2.2.0", "xarray": "2023.10.0", "zarr": "2.17.0"}, "start_date": "2025-07-01T00:00:00Z"}, {"packages": {"ipython": "8.20.0", "networkx": "3.3", "python": "3.12", "scikit-image": "0.23.0", "xarray": "2024.1.0"}, "start_date": "2025-10-01T00:00:00Z"}, {"packages": {"ipython": "8.24.0", "pandas": "2.3.0", "scikit-learn": "1.5.0", "scipy": "1.13.0", "xarray": "2024.5.0", "zarr": "2.18.0"}, "start_date": "2026-01-01T00:00:00Z"}, {"packages": {"ipython": "8.27.0", "matplotlib": "3.10.0", "networkx": "3.4", "numpy": "2.1.0", "scikit-image": "0.25.0", "scikit-learn": "1.6.0", "scipy": "1.15.0", "xarray": "2024.7.0", "zarr": "3.0.0"}, "start_date": "2026-04-01T00:00:00Z"}, {"packages": {"ipython": "8.28.0", "numpy": "2.2.0", "xarray": "2024.10.0"}, "start_date": "2026-07-01T00:00:00Z"}, {"packages": {"ipython": "8.32.0", "networkx": "3.5", "numpy": "2.3.0", "python": "3.13", "scikit-learn": "1.7.0", "xarray": "2025.1.0"}, "start_date": "2026-10-01T00:00:00Z"}, {"packages": {"ipython": "9.1.0", "scipy": "1.16.0", "xarray": "2025.4.0", "zarr": "3.1.0"}, "start_date": "2027-01-01T00:00:00Z"}, {"packages": {"ipython": "9.4.0", "xarray": "2025.7.0"}, "start_date": "2027-04-01T00:00:00Z"}] diff --git a/tests/test_parsing.py b/tests/test_parsing.py new file mode 100644 index 0000000..3a0af4d --- /dev/null +++ b/tests/test_parsing.py @@ -0,0 +1,55 @@ +from spec_zero_tools.parsing import parse_version_spec, parse_pep_dependency +from packaging.specifiers import SpecifierSet +import pytest +from urllib.parse import urlparse + + +def test_parsing_correct(): + assert SpecifierSet(">=0") == parse_version_spec("*") + assert SpecifierSet(">4,<9") == parse_version_spec(">4, <9") + assert SpecifierSet(">=4") == parse_version_spec(">=4") + assert SpecifierSet(">=2025.7") == parse_version_spec(">=2025.7") + + +def test_parsing_incorrect(): + with pytest.raises(ValueError): + parse_version_spec("-18") + + with pytest.raises(ValueError): + parse_version_spec("asdf") + + +def test_pep_dependency_parsing(): + matplotlib_str = "matplotlib" + pkg, spec = parse_pep_dependency(matplotlib_str) + + assert pkg == "matplotlib", pkg + + assert spec is None, spec + + +def test_pep_dependency_parsing_with_spec(): + matplotlib_str = "matplotlib>=3.7.0,<4" + pkg, spec = parse_pep_dependency(matplotlib_str) + + assert pkg == "matplotlib", pkg + assert spec == SpecifierSet(">=3.7.0,<4"), spec + + +def test_pep_dependency_parsing_with_url_spec(): + dep_str = "matplotlib @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686" + pkg, spec = parse_pep_dependency(dep_str) + + assert pkg == "matplotlib", pkg + assert spec == urlparse( + " https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686" + ), spec + + +def test_pep_dependency_parsing_extra_restrictions(): + matplotlib_str = "matplotlib>=3.7.0,<4,!=3.8.14" + pkg, spec = parse_pep_dependency(matplotlib_str) + + assert pkg == "matplotlib", pkg + + assert spec == SpecifierSet("!=3.8.14,<4,>=3.7.0"), spec diff --git a/tests/test_update_pyproject_toml.py b/tests/test_update_pyproject_toml.py new file mode 100644 index 0000000..094462a --- /dev/null +++ b/tests/test_update_pyproject_toml.py @@ -0,0 +1,11 @@ +from spec_zero_tools.parsing import read_schedule, read_toml +from spec_zero_tools import update_pyproject_toml + + +def test_update_pyproject_toml(): + expected = read_toml("tests/test_data/pyproject_updated.toml") + pyproject_data = read_toml("tests/test_data/pyproject.toml") + test_schedule = read_schedule("tests/test_data/test_schedule.json") + update_pyproject_toml(pyproject_data, test_schedule) + + assert pyproject_data == expected diff --git a/tests/test_versions.py b/tests/test_versions.py new file mode 100644 index 0000000..460dc8f --- /dev/null +++ b/tests/test_versions.py @@ -0,0 +1,29 @@ +from packaging.version import Version +from spec_zero_tools.versions import repr_spec_set, tighten_lower_bound +from packaging.specifiers import SpecifierSet + + +def test_repr_specset(): + spec = SpecifierSet("<7,!=3.8.0,>4,~=3.14") + assert repr_spec_set(spec) == "~=3.14,>4,<7,!=3.8.0" + + +def test_tighter_lower_bound_any(): + spec = SpecifierSet(">=0") + lower_bound = Version("3.8.0") + tightened = tighten_lower_bound(spec, lower_bound) + assert tightened == SpecifierSet(">=3.8.0") + + +def test_tighter_lower_bound_leaves_other_restrictions(): + spec = SpecifierSet("~= 0.9,>=1.0,!= 1.3.4.*,< 2.0") + lower_bound = Version("3.8.0") + tightened = tighten_lower_bound(spec, lower_bound) + assert tightened == SpecifierSet("~= 0.9,>=3.8.0,!=1.3.4.*,<2.0") + + +def test_tighter_lower_bound_adds_lower_bound_if_not_present(): + spec = SpecifierSet("~=0.9,!=1.3.4.*,<2.0") + lower_bound = Version("3.8.0") + tightened = tighten_lower_bound(spec, lower_bound) + assert tightened == SpecifierSet("~= 0.9, != 1.3.4.*, < 2.0, >=3.8.0") From 7fee19f44b42f35b5231c77f49d6ec9cfeebda09 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:24:06 +0200 Subject: [PATCH 06/32] feat: add main update script --- generate_schedule.py | 207 +++++++++++++++++++++++++++++++++++++++++++ update.py | 26 ++++++ update_pixi.sh | 13 --- 3 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 generate_schedule.py create mode 100644 update.py delete mode 100755 update_pixi.sh diff --git a/generate_schedule.py b/generate_schedule.py new file mode 100644 index 0000000..8786f45 --- /dev/null +++ b/generate_schedule.py @@ -0,0 +1,207 @@ +import requests +import json +import collections +from datetime import datetime, timedelta + +import pandas as pd +from packaging.version import Version + + +PY_RELEASES = { + "3.8": "Oct 14, 2019", + "3.9": "Oct 5, 2020", + "3.10": "Oct 4, 2021", + "3.11": "Oct 24, 2022", + "3.12": "Oct 2, 2023", + "3.13": "Oct 7, 2024", +} +CORE_PACKAGES = [ + "ipython", + "matplotlib", + "networkx", + "numpy", + "pandas", + "scikit-image", + "scikit-learn", + "scipy", + "xarray", + "zarr", +] +PLUS_36_MONTHS = timedelta(days=int(365 * 3)) +PLUS_24_MONTHS = timedelta(days=int(365 * 2)) + +# Release data +# We put the cutoff at 3 quarters ago - we do not use "just" -9 months +# to avoid the content of the quarter to change depending on when we +# generate this file during the current quarter. +CURRENT_DATE = pd.Timestamp.now() +CURRENT_QUARTER_START = pd.Timestamp( + CURRENT_DATE.year, (CURRENT_DATE.quarter - 1) * 3 + 1, 1 +) +CUTOFF = CURRENT_QUARTER_START - pd.DateOffset(months=9) + + +def get_release_dates(package, support_time=PLUS_24_MONTHS): + releases = {} + print(f"Querying pypi.org for {package} versions...", end="", flush=True) + response = requests.get( + f"https://pypi.org/simple/{package}", + headers={"Accept": "application/vnd.pypi.simple.v1+json"}, + ).json() + print("OK") + file_date = collections.defaultdict(list) + for f in response["files"]: + if f["filename"].endswith(".tar.gz") or f["filename"].endswith(".zip"): + continue + ver = f["filename"].split("-")[1] + try: + version = Version(ver) + except Exception: + continue + if version.is_prerelease or version.micro != 0: + continue + release_date = pd.Timestamp(f["upload-time"]).tz_localize(None) + if not release_date: + continue + file_date[version].append(release_date) + release_date = {v: min(file_date[v]) for v in file_date} + for ver, release_date in sorted(release_date.items()): + drop_date = release_date + support_time + if drop_date >= CUTOFF: + releases[ver] = { + "release_date": release_date, + "drop_date": drop_date, + } + return releases + + +package_releases = { + "python": { + version: { + "release_date": datetime.strptime(release_date, "%b %d, %Y"), + "drop_date": datetime.strptime(release_date, "%b %d, %Y") + PLUS_36_MONTHS, + } + for version, release_date in PY_RELEASES.items() + } +} +package_releases |= {package: get_release_dates(package) for package in CORE_PACKAGES} +# Filter all items whose drop_date are in the past +package_releases = { + package: { + version: dates + for version, dates in releases.items() + if dates["drop_date"] > CUTOFF + } + for package, releases in package_releases.items() +} + +# Save Gantt chart +print("Saving Mermaid chart to chart.md") +with open("chart.md", "w") as fh: + fh.write( + """gantt +dateFormat YYYY-MM-DD +axisFormat %m / %Y +title Support Window""" + ) + for name, releases in package_releases.items(): + fh.write(f"\n\nsection {name}") + for version, dates in releases.items(): + fh.write( + f"\n{version} : {dates['release_date'].strftime('%Y-%m-%d')},{dates['drop_date'].strftime('%Y-%m-%d')}" + ) + fh.write("\n") + +# Print drop schedule +data = [] +for k, versions in package_releases.items(): + for v, dates in versions.items(): + data.append( + ( + k, + v, + pd.to_datetime(dates["release_date"]), + pd.to_datetime(dates["drop_date"]), + ) + ) +df = pd.DataFrame(data, columns=["package", "version", "release", "drop"]) +df["quarter"] = df["drop"].dt.to_period("Q") +df["new_min_version"] = ( + df[["package", "version", "quarter"]].groupby("package").shift(-1)["version"] +) +dq = df.set_index(["quarter", "package"]).sort_index().dropna() +new_min_versions = ( + dq.groupby(["quarter", "package"]).agg({"new_min_version": "max"}).reset_index() +) + +# We want to build a dict with the structure [{start_date: timestamp, packages: {package: lower_bound}}] +new_min_versions_list = [] +for q, packages in new_min_versions.groupby("quarter"): + package_lower_bounds = { + p: str(v) for p, v in packages.drop("quarter", axis=1).itertuples(index=False) + } + # jq is really insistent the Z should be there + quarter_start_time_str = str(q.start_time.isoformat()) + "Z" + new_min_versions_list.append( + {"start_date": quarter_start_time_str, "packages": package_lower_bounds} + ) +print("Saving drop schedule to schedule.json") +with open("schedule.json", "w") as f: + f.write(json.dumps(new_min_versions_list, sort_keys=True)) + + +def pad_table(table): + rows = [[el.strip() for el in row.split("|")] for row in table] + col_widths = [max(map(len, column)) for column in zip(*rows)] + rows[1] = [ + el if el != "----" else "-" * col_widths[i] for i, el in enumerate(rows[1]) + ] + padded_table = [] + for row in rows: + line = "" + for entry, width in zip(row, col_widths): + if not width: + continue + line += f"| {str.ljust(entry, width)} " + line += "|" + padded_table.append(line) + return padded_table + + +def make_table(sub): + table = [] + table.append("| | | |") + table.append("|----|----|----|") + for package in sorted(set(sub.index.get_level_values(0))): + vers = sub.loc[[package]]["version"] + minv, maxv = min(vers), max(vers) + rels = sub.loc[[package]]["release"] + rel_min, rel_max = min(rels), max(rels) + version_range = str(minv) if minv == maxv else f"{minv} to {maxv}" + rel_range = ( + str(rel_min.strftime("%b %Y")) + if rel_min == rel_max + else f"{rel_min.strftime('%b %Y')} and {rel_max.strftime('%b %Y')}" + ) + table.append(f"|{package:<15}|{version_range:<19}|released {rel_range}|") + return pad_table(table) + + +def make_quarter(quarter, dq): + table = ["#### " + str(quarter).replace("Q", " - Quarter ") + ":\n"] + table.append("###### Recommend drop support for:\n") + sub = dq.loc[quarter] + table.extend(make_table(sub)) + return "\n".join(table) + + +print("Saving drop schedule to schedule.md") +with open("schedule.md", "w") as fh: + # We collect packages 6 month in the past, and drop the first quarter + # as we might have filtered some of the packages out depending on + # when we ran the script. + tb = [] + for quarter in list(sorted(set(dq.index.get_level_values(0))))[1:]: + tb.append(make_quarter(quarter, dq)) + fh.write("\n\n".join(tb)) + fh.write("\n") diff --git a/update.py b/update.py new file mode 100644 index 0000000..7c09faf --- /dev/null +++ b/update.py @@ -0,0 +1,26 @@ +from spec_zero_tools import update_pyproject_toml, read_toml, write_toml, read_schedule +from pathlib import Path +from argparse import ArgumentParser + + +if __name__ == '__main__': + + parser = ArgumentParser( + prog='spec_zero_update', + description='A script to update your project dependencies to be in line with the scientific python SPEC 0 support schedule', + ) + + parser.add_argument('toml_path', default="pyproject.toml", help="Path to the project file that lists the dependencies. defaults to 'pyproject.toml'.") + + args = parser.parse_args() + + toml_path = Path(args.toml_path) + + if not toml_path.exists(): + raise ValueError(f"{toml_path} was supplied as path to project file but it did not exist") + + project_data = read_toml(toml_path) + schedule_data = read_schedule("schedule.json") + update_pyproject_toml(project_data, schedule_data) + + write_toml(toml_path, project_data) diff --git a/update_pixi.sh b/update_pixi.sh deleted file mode 100755 index e7fbe95..0000000 --- a/update_pixi.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - - -for line in $(jq 'map(select(.start_date |fromdateiso8601 |tonumber < now))| sort_by("start_date") | reverse | .[0].packages | to_entries | map(.key + ":" + .value)[]' --raw-output schedule.json); do - - package=$(echo "$line" | cut -d ':' -f 1) - spec_0_version=$(echo "$line" | cut -d ':' -f 2) - - if pixi list -x "^$package" 2> /dev/null | grep "No packages" -q -v; then - echo "Updating $package" - pixi add "$package>=$spec_0_version" - fi -done From 62b17f2b6d116799ef91c1cc4672d068aa99d11f Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:24:17 +0200 Subject: [PATCH 07/32] feat: add workflow to run tests of python code --- .github/workflows/test.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..9285ada --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,24 @@ +name: Generate release schedule artifacts +on: + push: + branches: main + pull_request: + branches: main + # on demand + workflow_dispatch: + +jobs: + create-artifacts: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: prefix-dev/setup-pixi@v0.8.14 + with: + pixi-version: "v0.49.0" + environments: dev + - run: | + pixi run test From 7ae79dc68b87fa0bea1e82ab630936aa3774891a Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:24:35 +0200 Subject: [PATCH 08/32] fix: update release_schedule.yaml to use new env setup --- .github/workflows/release_schedule.yaml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release_schedule.yaml b/.github/workflows/release_schedule.yaml index 751bd25..fc3190e 100644 --- a/.github/workflows/release_schedule.yaml +++ b/.github/workflows/release_schedule.yaml @@ -20,13 +20,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.13" - - name: Install dependencies - run: | - pip install -r requirements.txt + # We're going to make a tag that we can we release so we'll need the full history for that + fetch-depth: 0 + - uses: prefix-dev/setup-pixi@v0.8.14 + with: + pixi-version: "v0.49.0" + environments: schedule - name: Run spec_zero_versions.py run: | - python spec_zero_versions.py + pixi run generate_schedule - name: setup git run: | # git will complain if we don't do this first @@ -38,15 +40,6 @@ jobs: run: | echo "TAG_NAME=$(date '+%Y-Q%q')" >> "$GITHUB_OUTPUT" - - name: create tag - env: - TAG_NAME: ${{ steps.tag_name.outputs.TAG_NAME }} - run: | - git add schedule.md chart.md schedule.json - git commit -m "Update SPEC 0 schedule artifacts" - git tag "$TAG_NAME" - git push origin "$TAG_NAME" - - name: Publish github release uses: softprops/action-gh-release@v2 env: From 9cc5d79f82d5dfc3a0cc0a98c3a2288e068dfae7 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:24:48 +0200 Subject: [PATCH 09/32] fix: update action.yaml to use new python code --- action.yaml | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/action.yaml b/action.yaml index de974b4..2020e33 100644 --- a/action.yaml +++ b/action.yaml @@ -6,10 +6,10 @@ inputs: description: "Target branch for the pull request" required: true default: "main" - tool: # for now only pixi, but good to make others possible from the start - description: "Which tool you use for managing your environment." + project_file_name: + description: "The filename to the project file that lists your dependencies, relative to the repository root. Defaults to 'pyproject.toml' Curretnly only pyproject.toml is supported but others may be added." required: true - default: "pixi" + default: "pyproject.toml" token: description: "GitHub token with repo permissions to create pull requests" required: true @@ -17,14 +17,6 @@ inputs: runs: using: "composite" steps: - - name: Validate tool input - shell: bash - run: | - if [[ "${{ inputs.tool }}" != "pixi" ]]; then - echo "❌ Invalid tool: '${{ inputs.tool }}'" - echo "Accepted values are: 'pixi'" - exit 1 - fi - name: Checkout code uses: actions/checkout@v4 with: @@ -42,16 +34,19 @@ runs: # make user pixi is available - uses: prefix-dev/setup-pixi@v0.8.14 - if: ${{ inputs.tool == 'pixi' }} name: Setup pixi with: + environments: >- + update + activate-environment: update pixi-version: v0.49.0 + manifest-path: ${{github.action_path}}/pyproject.toml - - name: Update Pixi dependencies - if: ${{ inputs.tool == 'pixi' }} + - name: Run Update script shell: bash run: | - "${{ github.action_path }}/update_pixi.sh" + python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_path}}" + - name: Create Pull Request uses: peter-evans/create-pull-request@v6 From 190e210b5fcae72e2a75384419fdc71cc49f78b4 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:24:57 +0200 Subject: [PATCH 10/32] doc: Update README.md --- readme.md | 40 +++++++++++++++++----------------------- schedule.json | 1 + 2 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 schedule.json diff --git a/readme.md b/readme.md index 4313421..a638bb0 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,11 @@ It also contains released versions of the schedule in various formats that that ## Using the action +To use the action you can copy the yaml below, and paste it into `.github/workflows/update-spec-0.yaml`. The arguments bewlow are filled with heir default value, in most cases you won't have to fill them. All except for `token` are optional. + +Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound. For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes. Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). + + ```yaml name: Update SPEC 0 dependencies @@ -17,37 +22,26 @@ on: - cron: "0 0 2 */3 *" permissions: - contents: write - pull-requests: write + contents: write + pull-requests: write jobs: - update: + update: runs-on: ubuntu-latest steps: - - uses: scientific-python/spec-zero-tools@main - with: - token: ${{ secrets.GH_PAT }} - target_branch: main - tool: pixi -``` - -Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound. For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes. Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). + - uses: scientific-python/spec-zero-tools@v1 + with: + token: ${{ secrets.GH_PAT }} + project_file_name: "pyproject.toml" + target_branch: 'main' -To help projects stay compliant with SPEC-0, we provide a `schedule.json` file that can be used by CI systems to determine new version boundaries. -Currently the action can take the following inputs: +``` -| Name | Description | Required | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `token` | The token that the action will use to create and update the pull request. See [token](https://github.com/marketplace/actions/create-pull-request#token). | Yes | -| `tool` | Which tool to use for managing your dependencies. Currently `pixi` is the only option. | No | -| `target_branch` | The branch to open a PR against with the updated versions. Defaults to `main`. | No | +It should update any of the packages listed in the `dependency`, or `tool.pixi.*` tables. ## Limitations -This project is still in progress and thus it comes with some limitations we are working on. Hopefully this will be gone by the time you read this, but currently the limitations are: +1. since this action simply parses the toml to do the upgrade and leaves any other bounds in tackt, it is possible that the environment of the PR becomes unsolvable. For example if you have a numpy dependency like so: `numpy = ">=1.25.0,<2"` this will get updated in the PR to `numpy = "2.0.0,<2"` which is infeasable. Keeping the resulting environment is outside the scope of this action, so they might have to be adjusted manually. +2. Currently on `pyproject.toml` is supported by this action, though other manifest files could be considered upon request. -- Only `pixi` is supported -- if you have a higher bound than the one listed in SPEC 0 this is overwritten -- higher bounds are deleted instead of maintained. -- dependency groups are not yet supported diff --git a/schedule.json b/schedule.json new file mode 100644 index 0000000..c848ede --- /dev/null +++ b/schedule.json @@ -0,0 +1 @@ +[{"packages": {"ipython": "8.8.0", "numpy": "1.25.0", "python": "3.11", "scikit-learn": "1.3.0", "xarray": "2023.1.0"}, "start_date": "2024-10-01T00:00:00Z"}, {"packages": {"ipython": "8.13.0", "matplotlib": "3.8.0", "networkx": "3.1", "scikit-image": "0.21.0", "scipy": "1.11.0", "xarray": "2023.4.0", "zarr": "2.15.0"}, "start_date": "2025-01-01T00:00:00Z"}, {"packages": {"ipython": "8.15.0", "networkx": "3.2", "numpy": "1.26.0", "pandas": "2.1.0", "scikit-image": "0.22.0", "scikit-learn": "1.4.0", "scipy": "1.12.0", "xarray": "2023.7.0", "zarr": "2.16.0"}, "start_date": "2025-04-01T00:00:00Z"}, {"packages": {"ipython": "8.17.0", "matplotlib": "3.9.0", "numpy": "2.0.0", "pandas": "2.2.0", "xarray": "2023.10.0", "zarr": "2.17.0"}, "start_date": "2025-07-01T00:00:00Z"}, {"packages": {"ipython": "8.20.0", "networkx": "3.3", "python": "3.12", "scikit-image": "0.23.0", "xarray": "2024.1.0"}, "start_date": "2025-10-01T00:00:00Z"}, {"packages": {"ipython": "8.24.0", "pandas": "2.3.0", "scikit-learn": "1.5.0", "scipy": "1.13.0", "xarray": "2024.5.0", "zarr": "2.18.0"}, "start_date": "2026-01-01T00:00:00Z"}, {"packages": {"ipython": "8.27.0", "matplotlib": "3.10.0", "networkx": "3.4", "numpy": "2.1.0", "scikit-image": "0.25.0", "scikit-learn": "1.6.0", "scipy": "1.15.0", "xarray": "2024.7.0", "zarr": "3.0.0"}, "start_date": "2026-04-01T00:00:00Z"}, {"packages": {"ipython": "8.28.0", "numpy": "2.2.0", "xarray": "2024.10.0"}, "start_date": "2026-07-01T00:00:00Z"}, {"packages": {"ipython": "8.32.0", "networkx": "3.5", "numpy": "2.3.0", "python": "3.13", "scikit-learn": "1.7.0", "xarray": "2025.1.0"}, "start_date": "2026-10-01T00:00:00Z"}, {"packages": {"ipython": "9.1.0", "scipy": "1.16.0", "xarray": "2025.4.0", "zarr": "3.1.0"}, "start_date": "2027-01-01T00:00:00Z"}, {"packages": {"ipython": "9.4.0", "xarray": "2025.7.0"}, "start_date": "2027-04-01T00:00:00Z"}] \ No newline at end of file From 8319244a45de9ccc834c0f592a34660795525938 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Mon, 11 Aug 2025 14:37:34 +0200 Subject: [PATCH 11/32] fix: update lockfile --- pixi.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixi.lock b/pixi.lock index 495f564..9f1fa9b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -939,7 +939,7 @@ packages: - pypi: ./ name: spec-zero-tools version: 0.1.0 - sha256: f055138b9e6c5fa0f9a4be072f21e62c26b0b07e159e5376092dbb35d86da202 + sha256: 0a4320fdf6bed675d037f620da4018ab654acc41326fc3f4a41ab8392505b1ae requires_dist: - packaging>=25.0,<26 requires_python: '>=3.11' From a5894a9f662a1b075a527bc26c1d0d5414a4cca2 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Thu, 14 Aug 2025 10:16:53 +0200 Subject: [PATCH 12/32] fix: allow dependency groups in pep dependencies --- spec_zero_tools/parsing.py | 2 +- tests/test_parsing.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/spec_zero_tools/parsing.py b/spec_zero_tools/parsing.py index b9858be..a58b7aa 100644 --- a/spec_zero_tools/parsing.py +++ b/spec_zero_tools/parsing.py @@ -12,7 +12,7 @@ Url: TypeAlias = ParseResult # slightly modified version of https://packaging.python.org/en/latest/specifications/dependency-specifiers/#names -PEP_PACKAGE_IDENT_RE = compile(r"(?im)^([A-Z0-9][A-Z0-9._-]*[A-Z0-9])(.*)$") +PEP_PACKAGE_IDENT_RE = compile(r"(?im)^([A-Z0-9][A-Z0-9._-]*(?:\[[A-Z0-9._,-]+\])?)(.*)$") class SupportSchedule(TypedDict): diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 3a0af4d..9cdc35b 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -28,6 +28,13 @@ def test_pep_dependency_parsing(): assert spec is None, spec +def test_pep_dependency_parsing_with_spec_and_optional_dep(): + matplotlib_str = "matplotlib[foo,bar]>=3.7.0,<4" + pkg, spec = parse_pep_dependency(matplotlib_str) + + assert pkg == "matplotlib[foo,bar]", pkg + assert spec == SpecifierSet(">=3.7.0,<4"), spec + def test_pep_dependency_parsing_with_spec(): matplotlib_str = "matplotlib>=3.7.0,<4" pkg, spec = parse_pep_dependency(matplotlib_str) From 221ece38f0a661de651b29e017f7147209af115f Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Thu, 14 Aug 2025 10:31:49 +0200 Subject: [PATCH 13/32] fix: use actual input variable --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 2020e33..edacba1 100644 --- a/action.yaml +++ b/action.yaml @@ -45,7 +45,7 @@ runs: - name: Run Update script shell: bash run: | - python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_path}}" + python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_name}}" - name: Create Pull Request From f027d820d5bb356480e22c4fa5a8b77062d15b3a Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Thu, 14 Aug 2025 10:33:37 +0200 Subject: [PATCH 14/32] fix: don't commit schedule.json --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index edacba1..905c979 100644 --- a/action.yaml +++ b/action.yaml @@ -46,6 +46,7 @@ runs: shell: bash run: | python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_name}}" + rm schedule.json - name: Create Pull Request From 2b3570f540f70c2e3ba4ec936853c1ae8e788e96 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Thu, 14 Aug 2025 10:38:30 +0200 Subject: [PATCH 15/32] fix: test pep dependency with extras --- spec_zero_tools/__init__.py | 7 +++++-- spec_zero_tools/parsing.py | 29 +++++++++++++------------- tests/test_data/pyproject.toml | 2 +- tests/test_data/pyproject_updated.toml | 2 +- tests/test_parsing.py | 19 ++++++++++------- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/spec_zero_tools/__init__.py b/spec_zero_tools/__init__.py index 2f6e018..26c8c93 100644 --- a/spec_zero_tools/__init__.py +++ b/spec_zero_tools/__init__.py @@ -22,7 +22,7 @@ def update_pyproject_dependencies(dependencies: dict, schedule: SupportSchedule) # iterate by idx because we want to update it inplace for i in range(len(dependencies)): dep_str = dependencies[i] - pkg, spec = parse_pep_dependency(dep_str) + pkg, extras, spec = parse_pep_dependency(dep_str) if isinstance(spec, Url) or pkg not in schedule["packages"]: continue @@ -36,7 +36,10 @@ def update_pyproject_dependencies(dependencies: dict, schedule: SupportSchedule) except ValueError: continue - new_dep_str = f"{pkg}{repr_spec_set(spec)}" + if not extras: + new_dep_str = f"{pkg}{repr_spec_set(spec)}" + else: + new_dep_str = f"{pkg}{extras}{repr_spec_set(spec)}" dependencies[i] = new_dep_str diff --git a/spec_zero_tools/parsing.py b/spec_zero_tools/parsing.py index a58b7aa..6df1045 100644 --- a/spec_zero_tools/parsing.py +++ b/spec_zero_tools/parsing.py @@ -12,7 +12,7 @@ Url: TypeAlias = ParseResult # slightly modified version of https://packaging.python.org/en/latest/specifications/dependency-specifiers/#names -PEP_PACKAGE_IDENT_RE = compile(r"(?im)^([A-Z0-9][A-Z0-9._-]*(?:\[[A-Z0-9._,-]+\])?)(.*)$") +PEP_PACKAGE_IDENT_RE = compile(r"(?im)^([A-Z0-9][A-Z0-9._-]*)(\[[A-Z0-9._,-]+\])?(.*)$") class SupportSchedule(TypedDict): @@ -54,26 +54,27 @@ def read_schedule(path: Path | str) -> Sequence[SupportSchedule]: return json.load(file) -def parse_pep_dependency(dep_str: str) -> Tuple[str, SpecifierSet | Url | None]: +def parse_pep_dependency(dep_str: str) -> Tuple[str, str | None, SpecifierSet | Url | None]: match = PEP_PACKAGE_IDENT_RE.match(dep_str) if match is None: raise ValueError("Could not find any valid python package identifier") - match_groups = match.groups() + pkg, extras, spec_str = match.groups() - pkg = match_groups[0] - # capture group could be empty - if len(match_groups) > 1 and match_groups[1]: - spec_str = match_groups[1] - if is_url_spec(spec_str): - spec = urlparse(spec_str.split("@")[1]) - else: - spec = SpecifierSet(spec_str) - else: + extras = extras or None + + if is_url_spec(spec_str): + spec = urlparse(spec_str.split("@")[1]) + elif not spec_str: spec = None + else: + spec = SpecifierSet(spec_str) + + return (pkg, extras, spec) - return (pkg, spec) +def is_url_spec(str_spec: str|None) -> bool: + if str_spec is None: + return False -def is_url_spec(str_spec: str) -> bool: return str_spec.strip().startswith("@") diff --git a/tests/test_data/pyproject.toml b/tests/test_data/pyproject.toml index ff34941..8bcee20 100644 --- a/tests/test_data/pyproject.toml +++ b/tests/test_data/pyproject.toml @@ -3,7 +3,7 @@ authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] name = "tests" requires-python = ">=3.10" version = "0.1.0" -dependencies = ["ipython>=8.7.0,<4"] +dependencies = ["ipython>=8.7.0,<4", "numpy[foo,bar]>=1.10.0,<2"] [build-system] build-backend = "hatchling.build" diff --git a/tests/test_data/pyproject_updated.toml b/tests/test_data/pyproject_updated.toml index bdd191f..1fee189 100644 --- a/tests/test_data/pyproject_updated.toml +++ b/tests/test_data/pyproject_updated.toml @@ -3,7 +3,7 @@ authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] name = "tests" requires-python = ">=3.11" version = "0.1.0" -dependencies = ["ipython>=8.8.0,<4"] +dependencies = ["ipython>=8.8.0,<4", "numpy[foo,bar]>=1.25.0,<2"] [build-system] build-backend = "hatchling.build" diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 9cdc35b..fb5566c 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -21,33 +21,36 @@ def test_parsing_incorrect(): def test_pep_dependency_parsing(): matplotlib_str = "matplotlib" - pkg, spec = parse_pep_dependency(matplotlib_str) + pkg, features, spec = parse_pep_dependency(matplotlib_str) assert pkg == "matplotlib", pkg - + assert features is None, features assert spec is None, spec def test_pep_dependency_parsing_with_spec_and_optional_dep(): matplotlib_str = "matplotlib[foo,bar]>=3.7.0,<4" - pkg, spec = parse_pep_dependency(matplotlib_str) + pkg, features, spec = parse_pep_dependency(matplotlib_str) - assert pkg == "matplotlib[foo,bar]", pkg + assert pkg == "matplotlib", pkg + assert features == "[foo,bar]", features assert spec == SpecifierSet(">=3.7.0,<4"), spec def test_pep_dependency_parsing_with_spec(): matplotlib_str = "matplotlib>=3.7.0,<4" - pkg, spec = parse_pep_dependency(matplotlib_str) + pkg, features, spec = parse_pep_dependency(matplotlib_str) assert pkg == "matplotlib", pkg + assert features is None, features assert spec == SpecifierSet(">=3.7.0,<4"), spec def test_pep_dependency_parsing_with_url_spec(): dep_str = "matplotlib @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686" - pkg, spec = parse_pep_dependency(dep_str) + pkg, features, spec = parse_pep_dependency(dep_str) assert pkg == "matplotlib", pkg + assert features is None, features assert spec == urlparse( " https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686" ), spec @@ -55,8 +58,8 @@ def test_pep_dependency_parsing_with_url_spec(): def test_pep_dependency_parsing_extra_restrictions(): matplotlib_str = "matplotlib>=3.7.0,<4,!=3.8.14" - pkg, spec = parse_pep_dependency(matplotlib_str) + pkg, features, spec = parse_pep_dependency(matplotlib_str) assert pkg == "matplotlib", pkg - + assert features is None, features assert spec == SpecifierSet("!=3.8.14,<4,>=3.7.0"), spec From 52acc1bae02a2e44c4138bc529207a5574c0f323 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 09:56:12 +0200 Subject: [PATCH 16/32] fix: rename to spec0-action --- .github/workflows/release_schedule.yaml | 8 +++----- .github/workflows/test.yaml | 4 +--- action.yaml | 2 +- pixi.lock | 4 ++-- pyproject.toml | 7 +++++-- {spec_zero_tools => spec0-action}/__init__.py | 0 {spec_zero_tools => spec0-action}/parsing.py | 0 {spec_zero_tools => spec0-action}/versions.py | 0 8 files changed, 12 insertions(+), 13 deletions(-) rename {spec_zero_tools => spec0-action}/__init__.py (100%) rename {spec_zero_tools => spec0-action}/parsing.py (100%) rename {spec_zero_tools => spec0-action}/versions.py (100%) diff --git a/.github/workflows/release_schedule.yaml b/.github/workflows/release_schedule.yaml index fc3190e..9dd947e 100644 --- a/.github/workflows/release_schedule.yaml +++ b/.github/workflows/release_schedule.yaml @@ -19,9 +19,7 @@ jobs: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 - with: - # We're going to make a tag that we can we release so we'll need the full history for that - fetch-depth: 0 + - uses: prefix-dev/setup-pixi@v0.8.14 with: pixi-version: "v0.49.0" @@ -32,8 +30,8 @@ jobs: - name: setup git run: | # git will complain if we don't do this first - git config user.name "GitHub Actions Bot" - git config user.email "<>" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" - name: determine tag name id: tag_name diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9285ada..aa45c4d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,4 +1,4 @@ -name: Generate release schedule artifacts +name: Run the update test suite on: push: branches: main @@ -10,8 +10,6 @@ on: jobs: create-artifacts: runs-on: ubuntu-latest - permissions: - contents: write steps: - name: Checkout uses: actions/checkout@v4 diff --git a/action.yaml b/action.yaml index 905c979..ddb4dbd 100644 --- a/action.yaml +++ b/action.yaml @@ -30,7 +30,7 @@ runs: - name: Download schedule artifact shell: bash - run: gh release download -R "savente93/spec-zero-tools" --pattern schedule.json --clobber + run: gh release download -R "scientific-python/spec0-action" --pattern schedule.json --clobber # make user pixi is available - uses: prefix-dev/setup-pixi@v0.8.14 diff --git a/pixi.lock b/pixi.lock index 9f1fa9b..fc5cf2c 100644 --- a/pixi.lock +++ b/pixi.lock @@ -937,9 +937,9 @@ packages: size: 18455 timestamp: 1753199211006 - pypi: ./ - name: spec-zero-tools + name: spec0-action version: 0.1.0 - sha256: 0a4320fdf6bed675d037f620da4018ab654acc41326fc3f4a41ab8392505b1ae + sha256: 149b80b08892df9d897253182a9c4b4b3f48f0b73e93325c58d42c2e2f0aac73 requires_dist: - packaging>=25.0,<26 requires_python: '>=3.11' diff --git a/pyproject.toml b/pyproject.toml index e0568ed..3850afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] -name = "spec-zero-tools" +name = "spec0-action" requires-python = ">= 3.11" version = "0.1.0" dependencies = ["packaging>=25.0,<26"] @@ -15,11 +15,14 @@ channels = ["conda-forge"] platforms = ["linux-64"] [tool.pixi.feature.update.pypi-dependencies] -spec_zero_tools = { path = ".", editable = true } +spec0-action = { path = ".", editable = true } [tool.pixi.feature.test.tasks] test = { cmd = ["pytest", "-vvv"] } +[tool.pixi.feature.update.tasks] +generate-schedule = { cmd = ["python", "generate_schedule.py"] } + [tool.pixi.feature.test.dependencies] pytest = "*" diff --git a/spec_zero_tools/__init__.py b/spec0-action/__init__.py similarity index 100% rename from spec_zero_tools/__init__.py rename to spec0-action/__init__.py diff --git a/spec_zero_tools/parsing.py b/spec0-action/parsing.py similarity index 100% rename from spec_zero_tools/parsing.py rename to spec0-action/parsing.py diff --git a/spec_zero_tools/versions.py b/spec0-action/versions.py similarity index 100% rename from spec_zero_tools/versions.py rename to spec0-action/versions.py From 4cf09ab12d12a99148609f0a371509289118f0d6 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:02:52 +0200 Subject: [PATCH 17/32] fix: fix pyproject.toml --- .github/workflows/release_schedule.yaml | 2 +- pixi.lock | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_schedule.yaml b/.github/workflows/release_schedule.yaml index 9dd947e..2e38afa 100644 --- a/.github/workflows/release_schedule.yaml +++ b/.github/workflows/release_schedule.yaml @@ -26,7 +26,7 @@ jobs: environments: schedule - name: Run spec_zero_versions.py run: | - pixi run generate_schedule + pixi run -e schedule generate_schedule - name: setup git run: | # git will complain if we don't do this first diff --git a/pixi.lock b/pixi.lock index fc5cf2c..2ab4654 100644 --- a/pixi.lock +++ b/pixi.lock @@ -939,7 +939,7 @@ packages: - pypi: ./ name: spec0-action version: 0.1.0 - sha256: 149b80b08892df9d897253182a9c4b4b3f48f0b73e93325c58d42c2e2f0aac73 + sha256: af5bbf8d5b2207994b1d182b88167dc8de25271e802b84c07d4a2358c454dcf3 requires_dist: - packaging>=25.0,<26 requires_python: '>=3.11' diff --git a/pyproject.toml b/pyproject.toml index 3850afd..484de54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ spec0-action = { path = ".", editable = true } [tool.pixi.feature.test.tasks] test = { cmd = ["pytest", "-vvv"] } -[tool.pixi.feature.update.tasks] +[tool.pixi.feature.schedule.tasks] generate-schedule = { cmd = ["python", "generate_schedule.py"] } [tool.pixi.feature.test.dependencies] From 890e1fa8ee24258cfb66933572b58fa89681d181 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:07:13 +0200 Subject: [PATCH 18/32] fix: readme typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a638bb0..7c26c51 100644 --- a/readme.md +++ b/readme.md @@ -42,6 +42,6 @@ It should update any of the packages listed in the `dependency`, or `tool.pixi.* ## Limitations -1. since this action simply parses the toml to do the upgrade and leaves any other bounds in tackt, it is possible that the environment of the PR becomes unsolvable. For example if you have a numpy dependency like so: `numpy = ">=1.25.0,<2"` this will get updated in the PR to `numpy = "2.0.0,<2"` which is infeasable. Keeping the resulting environment is outside the scope of this action, so they might have to be adjusted manually. +1. since this action simply parses the toml to do the upgrade and leaves any other bounds in tackt, it is possible that the environment of the PR becomes unsolvable. For example if you have a numpy dependency like so: `numpy = ">=1.25.0,<2"` this will get updated in the PR to `numpy = "2.0.0,<2"` which is infeasable. Keeping the resulting environment solvable is outside the scope of this action, so they might have to be adjusted manually. 2. Currently on `pyproject.toml` is supported by this action, though other manifest files could be considered upon request. From 0e0cad5be1ae90d0ecc994ff2451da50c137fe91 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:16:05 +0200 Subject: [PATCH 19/32] fix: rename imports --- {spec0-action => spec0_action}/__init__.py | 4 ++-- {spec0-action => spec0_action}/parsing.py | 0 {spec0-action => spec0_action}/versions.py | 0 tests/test_parsing.py | 2 +- tests/test_update_pyproject_toml.py | 4 ++-- tests/test_versions.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename {spec0-action => spec0_action}/__init__.py (97%) rename {spec0-action => spec0_action}/parsing.py (100%) rename {spec0-action => spec0_action}/versions.py (100%) diff --git a/spec0-action/__init__.py b/spec0_action/__init__.py similarity index 97% rename from spec0-action/__init__.py rename to spec0_action/__init__.py index 26c8c93..8a0836c 100644 --- a/spec0-action/__init__.py +++ b/spec0_action/__init__.py @@ -2,8 +2,8 @@ from typing import Sequence, cast import datetime -from spec_zero_tools.versions import repr_spec_set, tighten_lower_bound -from spec_zero_tools.parsing import ( +from spec0_action.versions import repr_spec_set, tighten_lower_bound +from spec0_action.parsing import ( SupportSchedule, Url, is_url_spec, diff --git a/spec0-action/parsing.py b/spec0_action/parsing.py similarity index 100% rename from spec0-action/parsing.py rename to spec0_action/parsing.py diff --git a/spec0-action/versions.py b/spec0_action/versions.py similarity index 100% rename from spec0-action/versions.py rename to spec0_action/versions.py diff --git a/tests/test_parsing.py b/tests/test_parsing.py index fb5566c..c5b23bf 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1,4 +1,4 @@ -from spec_zero_tools.parsing import parse_version_spec, parse_pep_dependency +from spec0_action.parsing import parse_version_spec, parse_pep_dependency from packaging.specifiers import SpecifierSet import pytest from urllib.parse import urlparse diff --git a/tests/test_update_pyproject_toml.py b/tests/test_update_pyproject_toml.py index 094462a..40397fb 100644 --- a/tests/test_update_pyproject_toml.py +++ b/tests/test_update_pyproject_toml.py @@ -1,5 +1,5 @@ -from spec_zero_tools.parsing import read_schedule, read_toml -from spec_zero_tools import update_pyproject_toml +from spec0_action.parsing import read_schedule, read_toml +from spec0_action import update_pyproject_toml def test_update_pyproject_toml(): diff --git a/tests/test_versions.py b/tests/test_versions.py index 460dc8f..e364f7b 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -1,5 +1,5 @@ from packaging.version import Version -from spec_zero_tools.versions import repr_spec_set, tighten_lower_bound +from spec0_action.versions import repr_spec_set, tighten_lower_bound from packaging.specifiers import SpecifierSet From 17d74fa81e0ca353059a097bc41d96ca0e7328d7 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:17:32 +0200 Subject: [PATCH 20/32] fix: name issue in workflow --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index aa45c4d..a62597d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,7 +8,7 @@ on: workflow_dispatch: jobs: - create-artifacts: + run-tests: runs-on: ubuntu-latest steps: - name: Checkout From 943e9a00a9815b07a870b18cc2b444e491eabbc7 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:29:49 +0200 Subject: [PATCH 21/32] feat: make action test itself --- .github/workflows/test_action.yaml | 26 +++++--------------------- action.yaml | 8 ++++++++ 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test_action.yaml b/.github/workflows/test_action.yaml index b0ba0bf..7ecfeb2 100644 --- a/.github/workflows/test_action.yaml +++ b/.github/workflows/test_action.yaml @@ -10,24 +10,8 @@ jobs: uses: actions/checkout@v4 - name: Generate version data using local action uses: ./ - - name: Check file contents - run: | - printf "Contents of chart.md:\n" - cat chart.md - printf "\n\n" - printf "Contents of schedule.json:\n" - cat schedule.json - printf "\n\n" - printf "Contents of schedule.md:\n" - cat schedule.md - printf "\n\n" - - name: Remove generated files - run: | - printf "Removing generated files...\n" - rm -f chart.md schedule.json schedule.md - ls -R - - uses: actions/download-artifact@v5 - with: - name: spec-zero-versions - - name: Display structure of downloaded files - run: ls -R + with: + project_file_name: tests/test_data/pyproject.toml + create_pr: false + schedule_path: tests/test_data/test_schedule.json + diff --git a/action.yaml b/action.yaml index ddb4dbd..0ca9b10 100644 --- a/action.yaml +++ b/action.yaml @@ -10,6 +10,13 @@ inputs: description: "The filename to the project file that lists your dependencies, relative to the repository root. Defaults to 'pyproject.toml' Curretnly only pyproject.toml is supported but others may be added." required: true default: "pyproject.toml" + create_pr: + description: "Whether the action should open a PR or not. If this is set to false, this action should have no effect. This is mainly useful for testing" + required: true + default: true + schedule_path: + description: "Path to the schedule.json file. If it does not exist yet, it will be downloaded from the spec0-action repository latest release" + default: schedule.json token: description: "GitHub token with repo permissions to create pull requests" required: true @@ -50,6 +57,7 @@ runs: - name: Create Pull Request + if: ${{inputs.create_pr }} uses: peter-evans/create-pull-request@v6 with: token: ${{ inputs.token }} From 8359b1f214ab485702b7d4781d4f870847835d7f Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:34:41 +0200 Subject: [PATCH 22/32] fix: make schedule_path configurable so we can test it --- action.yaml | 7 ++++++- update.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/action.yaml b/action.yaml index 0ca9b10..70d5b2e 100644 --- a/action.yaml +++ b/action.yaml @@ -37,7 +37,12 @@ runs: - name: Download schedule artifact shell: bash - run: gh release download -R "scientific-python/spec0-action" --pattern schedule.json --clobber + env: + SCHEDULE_FILE: ${{ inputs.schedule_path }} + run: | + if [ ! -f "$SCHEDULE_FILE" ]; then + gh release download -R "scientific-python/spec0-action" --pattern schedule.json -O "$SCHEDULE_FILE" + fi # make user pixi is available - uses: prefix-dev/setup-pixi@v0.8.14 diff --git a/update.py b/update.py index 7c09faf..208be38 100644 --- a/update.py +++ b/update.py @@ -11,16 +11,18 @@ ) parser.add_argument('toml_path', default="pyproject.toml", help="Path to the project file that lists the dependencies. defaults to 'pyproject.toml'.") + parser.add_argument('schedule_path', default="schedule.json", help="Path to the schedule json payload. defaults to 'schedule.json'") args = parser.parse_args() toml_path = Path(args.toml_path) + schedule_path = Path(args.schedule_path) if not toml_path.exists(): raise ValueError(f"{toml_path} was supplied as path to project file but it did not exist") project_data = read_toml(toml_path) - schedule_data = read_schedule("schedule.json") + schedule_data = read_schedule(schedule_path) update_pyproject_toml(project_data, schedule_data) write_toml(toml_path, project_data) From 533ecca75fbde7d5724610639e5e0407477a56ca Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:35:47 +0200 Subject: [PATCH 23/32] fix: import --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index 208be38..5f2fa4b 100644 --- a/update.py +++ b/update.py @@ -1,4 +1,4 @@ -from spec_zero_tools import update_pyproject_toml, read_toml, write_toml, read_schedule +from spec0_action import update_pyproject_toml, read_toml, write_toml, read_schedule from pathlib import Path from argparse import ArgumentParser From 4885cc351eb428135daed5ffa5d27a1e9b31a548 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:38:01 +0200 Subject: [PATCH 24/32] fix: pass schedule_path to script --- action.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/action.yaml b/action.yaml index 70d5b2e..c4f1610 100644 --- a/action.yaml +++ b/action.yaml @@ -57,8 +57,9 @@ runs: - name: Run Update script shell: bash run: | - python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_name}}" - rm schedule.json + python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_name}}" "${{inputs.schedule_path}}" + # let's cleanup after ourselves so it doesn't end up in the PR + rm "${{inputs.schedule_path}}" - name: Create Pull Request From a103c2feff3e31251bef19b2c8970832261192df Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 15 Aug 2025 10:39:01 +0200 Subject: [PATCH 25/32] fix: fix pr step conditional --- .github/workflows/test_action.yaml | 2 +- action.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_action.yaml b/.github/workflows/test_action.yaml index 7ecfeb2..29b1139 100644 --- a/.github/workflows/test_action.yaml +++ b/.github/workflows/test_action.yaml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: generate_data: runs-on: ubuntu-latest - name: Generate version data + name: Update test project file steps: - name: Checkout uses: actions/checkout@v4 diff --git a/action.yaml b/action.yaml index c4f1610..3d5c81c 100644 --- a/action.yaml +++ b/action.yaml @@ -63,7 +63,7 @@ runs: - name: Create Pull Request - if: ${{inputs.create_pr }} + if: ${{ inputs.create_pr == 'true' }} uses: peter-evans/create-pull-request@v6 with: token: ${{ inputs.token }} From 11f4297057ab9d3a83cbd8084064489147393239 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 08:37:47 +0200 Subject: [PATCH 26/32] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nabil Freij Apply suggestions from code review Co-authored-by: Nabil Freij Apply suggestions from code review Co-authored-by: Brigitta Sipőcz --- .github/workflows/release_schedule.yaml | 2 +- .github/workflows/test.yaml | 2 +- action.yaml | 2 -- pyproject.toml | 3 +-- readme.md | 18 ++++++++++++------ spec0_action/__init__.py | 16 +++++++--------- spec0_action/parsing.py | 8 ++++---- 7 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release_schedule.yaml b/.github/workflows/release_schedule.yaml index 2e38afa..0e79fe8 100644 --- a/.github/workflows/release_schedule.yaml +++ b/.github/workflows/release_schedule.yaml @@ -3,7 +3,7 @@ on: schedule: # At 00:00 on day-of-month 1 in every 3rd month. (i.e. every quarter) - cron: "0 0 1 */3 *" - # on demand + # On demand workflow_dispatch: jobs: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a62597d..e926d01 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,7 @@ on: branches: main pull_request: branches: main - # on demand + # On demand workflow_dispatch: jobs: diff --git a/action.yaml b/action.yaml index 3d5c81c..1ebab33 100644 --- a/action.yaml +++ b/action.yaml @@ -44,7 +44,6 @@ runs: gh release download -R "scientific-python/spec0-action" --pattern schedule.json -O "$SCHEDULE_FILE" fi - # make user pixi is available - uses: prefix-dev/setup-pixi@v0.8.14 name: Setup pixi with: @@ -61,7 +60,6 @@ runs: # let's cleanup after ourselves so it doesn't end up in the PR rm "${{inputs.schedule_path}}" - - name: Create Pull Request if: ${{ inputs.create_pr == 'true' }} uses: peter-evans/create-pull-request@v6 diff --git a/pyproject.toml b/pyproject.toml index 484de54..dda0585 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,10 @@ [project] -authors = [{ name = "Sam Vente", email = "savente93@proton.me" }] +authors = [{ name = "Scientific Python Developers" }] name = "spec0-action" requires-python = ">= 3.11" version = "0.1.0" dependencies = ["packaging>=25.0,<26"] - [build-system] build-backend = "hatchling.build" requires = ["hatchling"] diff --git a/readme.md b/readme.md index 1873f34..e88f013 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,17 @@ # SPEC-0 Versions Action -This repository contains a Github Action to update python dependencies conform the SPEC 0 support schedule. +This repository contains a Github Action to update Python dependencies such that they conform to the SPEC 0 support schedule. It also contains released versions of the schedule in various formats that that action can use to open PRs in your repository. ## Using the action -To use the action you can copy the yaml below, and paste it into `.github/workflows/update-spec-0.yaml`. The arguments bewlow are filled with heir default value, in most cases you won't have to fill them. All except for `token` are optional. +To use the action you can copy the yaml below, and paste it into `.github/workflows/update-spec-0.yaml`. +The arguments below are filled with their default value, in most cases you won't have to fill them. +All except for `token` are optional. -Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound. For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes. Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). +Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound. +For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes. +Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). ```yaml @@ -29,7 +33,7 @@ jobs: update: runs-on: ubuntu-latest steps: - - uses: scientific-python/spec0-action@v1 + - uses: scientific-python/spec0-action@v1 with: token: ${{ secrets.GH_PAT }} project_file_name: "pyproject.toml" @@ -40,6 +44,8 @@ It should update any of the packages listed in the `dependency`, or `tool.pixi.* ## Limitations -1. since this action simply parses the toml to do the upgrade and leaves any other bounds in tackt, it is possible that the environment of the PR becomes unsolvable. For example if you have a numpy dependency like so: `numpy = ">=1.25.0,<2"` this will get updated in the PR to `numpy = "2.0.0,<2"` which is infeasable. Keeping the resulting environment solvable is outside the scope of this action, so they might have to be adjusted manually. -2. Currently on `pyproject.toml` is supported by this action, though other manifest files could be considered upon request. +1. Since this action simply parses the toml to do the upgrade and leaves any other bounds intact, it is possible that the environment of the PR becomes unsolvable. + For example if you have a numpy dependency like so: `numpy = ">=1.25.0,<2"` this will get updated in the PR to `numpy = "2.0.0,<2"` which is infeasible. + Keeping the resulting environment solvable is outside the scope of this action, so you might have to be adjusted manually. +2. Currently only `pyproject.toml` is supported by this action, though other manifest files could be considered upon request. diff --git a/spec0_action/__init__.py b/spec0_action/__init__.py index 8a0836c..5e6eea0 100644 --- a/spec0_action/__init__.py +++ b/spec0_action/__init__.py @@ -19,7 +19,7 @@ __all__ = ["read_schedule", "read_toml", "write_toml", "update_pyproject_toml"] def update_pyproject_dependencies(dependencies: dict, schedule: SupportSchedule): - # iterate by idx because we want to update it inplace + # Iterate by idx because we want to update it inplace for i in range(len(dependencies)): dep_str = dependencies[i] pkg, extras, spec = parse_pep_dependency(dep_str) @@ -30,9 +30,7 @@ def update_pyproject_dependencies(dependencies: dict, schedule: SupportSchedule) new_lower_bound = Version(schedule["packages"][pkg]) try: spec = tighten_lower_bound(spec or SpecifierSet(), new_lower_bound) - # will raise a value error if bound is already tigheter, in this case we just do nothing and continue - - + # Will raise a value error if bound is already tighter, in this case we just do nothing and continue except ValueError: continue @@ -46,11 +44,11 @@ def update_pyproject_dependencies(dependencies: dict, schedule: SupportSchedule) def update_dependency_table(dep_table: dict, new_versions: dict): for pkg, pkg_data in dep_table.items(): - # don't do anything for pkgs that aren't in our schedule + # Don't do anything for pkgs that aren't in our schedule if pkg not in new_versions: continue - # like pkg = ">x.y.z, SpecifierSet: if s.strip() == "*": - # python version numeric components must be non-negative so this is ookay + # Python version numeric components must be non-negative so this is okay # see https://packaging.python.org/en/latest/specifications/version-specifiers/ return SpecifierSet(">=0") try: - # if we can simply parse it return it + # If we can simply parse it return it return SpecifierSet(s) except InvalidSpecifier: try: From 1edb4ec145ed559583ed3622f9614e61128e364b Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 08:45:46 +0200 Subject: [PATCH 27/32] fix: update authors --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 1ebab33..34e323f 100644 --- a/action.yaml +++ b/action.yaml @@ -1,6 +1,6 @@ name: "Generate SPEC-0000 Data" description: "Based on the current SPEC 0 schedule, generate a tarball with the latest versions of all packages." -author: Sam Vente +author: Scientific Python Developers inputs: target_branch: description: "Target branch for the pull request" From 30042b898378b77435dd2bc925c00fcbc6cd72a4 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 09:41:03 +0200 Subject: [PATCH 28/32] fix: release packaging upper pin --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dda0585..ac74c6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ authors = [{ name = "Scientific Python Developers" }] name = "spec0-action" requires-python = ">= 3.11" version = "0.1.0" -dependencies = ["packaging>=25.0,<26"] +dependencies = ["packaging>=25.0"] [build-system] build-backend = "hatchling.build" From fa319834080d5edd0fdb2407af4931ae92ae72f2 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 09:42:43 +0200 Subject: [PATCH 29/32] fix: use SP bot as git name --- action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yaml b/action.yaml index 34e323f..7e3d699 100644 --- a/action.yaml +++ b/action.yaml @@ -32,8 +32,8 @@ runs: - name: Set up Git shell: bash run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "Scientific Python [bot]" + git config user.email "scientific-python@users.noreply.github.com" - name: Download schedule artifact shell: bash From cad587263e28cd6812ecd6a3aba4bd11f9fd79ac Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 09:45:02 +0200 Subject: [PATCH 30/32] fix: remove pixi.lock --- .gitignore | 1 + pixi.lock | 1030 ---------------------------------------------------- 2 files changed, 1 insertion(+), 1030 deletions(-) delete mode 100644 pixi.lock diff --git a/.gitignore b/.gitignore index 435b4a3..e8f5b39 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ # pixi environments .pixi/* !.pixi/config.toml +pixi.lock __pycache__ diff --git a/pixi.lock b/pixi.lock deleted file mode 100644 index 2ab4654..0000000 --- a/pixi.lock +++ /dev/null @@ -1,1030 +0,0 @@ -version: 6 -environments: - default: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - dev: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-33_h59b9bed_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-33_he106b2a_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-33_h7ac8fdf_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py312h33ff503_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda - - pypi: ./ - schedule: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-33_h59b9bed_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-33_he106b2a_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-33_h7ac8fdf_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py312h33ff503_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - update: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - - pypi: ./ -packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge - - libgomp >=7.5.0 - constrains: - - openmp_impl 9999 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 23621 - timestamp: 1650670423406 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda - sha256: dc27c58dc717b456eee2d57d8bc71df3f562ee49368a2351103bc8f1b67da251 - md5: a32e0c069f6c3dcac635f7b0b0dac67e - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - constrains: - - libbrotlicommon 1.1.0 hb9d3cd8_3 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 351721 - timestamp: 1749230265727 -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d - md5: 62ee74e96c5ebb0af99386de58cf9553 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 252783 - timestamp: 1720974456583 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 - md5: 74784ee3d225fc3dca89edb635b4e5cc - depends: - - __unix - license: ISC - purls: [] - size: 154402 - timestamp: 1754210968730 -- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda - sha256: a1ad5b0a2a242f439608f22a538d2175cac4444b7b3f4e2b8c090ac337aaea40 - md5: 11f59985f49df4620890f3e746ed7102 - depends: - - python >=3.9 - license: ISC - purls: - - pkg:pypi/certifi?source=compressed-mapping - size: 158692 - timestamp: 1754231530168 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - sha256: cba6ea83c4b0b4f5b5dc59cb19830519b28f95d7ebef7c9c5cf1c14843621457 - md5: a861504bbea4161a9170b85d4d2be840 - depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 - - pycparser - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 294403 - timestamp: 1725560714366 -- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda - sha256: 535ae5dcda8022e31c6dc063eb344c80804c537a5a04afba43a845fa6fa130f5 - md5: 40fe4284b8b5835a9073a645139f35af - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/charset-normalizer?source=hash-mapping - size: 50481 - timestamp: 1746214981991 -- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 - md5: 962b9857ee8e7018c22f2776ffa0b2d7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/colorama?source=hash-mapping - size: 27011 - timestamp: 1733218222191 -- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - sha256: ce61f4f99401a4bd455b89909153b40b9c823276aefcbb06f2044618696009ca - md5: 72e42d28960d875c7654614f8b50939a - depends: - - python >=3.9 - - typing_extensions >=4.6.0 - license: MIT and PSF-2.0 - purls: - - pkg:pypi/exceptiongroup?source=hash-mapping - size: 21284 - timestamp: 1746947398083 -- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - sha256: 0aa1cdc67a9fe75ea95b5644b734a756200d6ec9d0dff66530aec3d1c1e9df75 - md5: b4754fb1bdcb70c8fd54f918301582c6 - depends: - - hpack >=4.1,<5 - - hyperframe >=6.1,<7 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/h2?source=hash-mapping - size: 53888 - timestamp: 1738578623567 -- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba - md5: 0a802cb9888dd14eeefc611f05c40b6e - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/hpack?source=hash-mapping - size: 30731 - timestamp: 1737618390337 -- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 - md5: 8e6923fc12f1fe8f8c4e5c9f343256ac - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/hyperframe?source=hash-mapping - size: 17397 - timestamp: 1737618427549 -- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 - md5: 39a4f67be3286c86d696df570b1201b7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/idna?source=hash-mapping - size: 49765 - timestamp: 1733211921194 -- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - sha256: 0ec8f4d02053cd03b0f3e63168316530949484f80e16f5e2fb199a1d117a89ca - md5: 6837f3eff7dcea42ecd714ce1ac2b108 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/iniconfig?source=hash-mapping - size: 11474 - timestamp: 1733223232820 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 - md5: 0be7c6e070c19105f966d3758448d018 - depends: - - __glibc >=2.17,<3.0.a0 - constrains: - - binutils_impl_linux-64 2.44 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 676044 - timestamp: 1752032747103 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-33_h59b9bed_openblas.conda - build_number: 33 - sha256: 18b165b45f3cea3892fee25a91b231abf29f521df76c8fcc0c92f6cf5071a911 - md5: b43d5de8fe73c2a5fb2b43f45301285b - depends: - - libopenblas >=0.3.30,<0.3.31.0a0 - - libopenblas >=0.3.30,<1.0a0 - constrains: - - mkl <2025 - - liblapack 3.9.0 33*_openblas - - blas 2.133 openblas - - liblapacke 3.9.0 33*_openblas - - libcblas 3.9.0 33*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 18778 - timestamp: 1754412356514 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-33_he106b2a_openblas.conda - build_number: 33 - sha256: b34271bb9c3b2377ac23c3fb1ecf45c08f8d09675ff8aad860f4e3c547b126a7 - md5: 28052b5e6ea5bd283ac343c5c064b950 - depends: - - libblas 3.9.0 33_h59b9bed_openblas - constrains: - - liblapack 3.9.0 33*_openblas - - liblapacke 3.9.0 33*_openblas - - blas 2.133 openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 18748 - timestamp: 1754412369555 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 - md5: 4211416ecba1866fab0c6470986c22d6 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - constrains: - - expat 2.7.1.* - license: MIT - license_family: MIT - purls: [] - size: 74811 - timestamp: 1752719572741 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab - md5: ede4673863426c0883c0063d853bbd85 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 57433 - timestamp: 1743434498161 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda - sha256: 144e35c1c2840f2dc202f6915fc41879c19eddbb8fa524e3ca4aa0d14018b26f - md5: f406dcbb2e7bef90d793e50e79a2882b - depends: - - __glibc >=2.17,<3.0.a0 - - _openmp_mutex >=4.5 - constrains: - - libgcc-ng ==15.1.0=*_4 - - libgomp 15.1.0 h767d61c_4 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 824153 - timestamp: 1753903866511 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda - sha256: 76ceac93ed98f208363d6e9c75011b0ff7b97b20f003f06461a619557e726637 - md5: 28771437ffcd9f3417c66012dc49a3be - depends: - - libgcc 15.1.0 h767d61c_4 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 29249 - timestamp: 1753903872571 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda - sha256: 2fe41683928eb3c57066a60ec441e605a69ce703fc933d6d5167debfeba8a144 - md5: 53e876bc2d2648319e94c33c57b9ec74 - depends: - - libgfortran5 15.1.0 hcea5267_4 - constrains: - - libgfortran-ng ==15.1.0=*_4 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 29246 - timestamp: 1753903898593 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda - sha256: 3070e5e2681f7f2fb7af0a81b92213f9ab430838900da8b4f9b8cf998ddbdd84 - md5: 8a4ab7ff06e4db0be22485332666da0f - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=15.1.0 - constrains: - - libgfortran 15.1.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 1564595 - timestamp: 1753903882088 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda - sha256: e0487a8fec78802ac04da0ac1139c3510992bc58a58cde66619dde3b363c2933 - md5: 3baf8976c96134738bba224e9ef6b1e5 - depends: - - __glibc >=2.17,<3.0.a0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 447289 - timestamp: 1753903801049 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-33_h7ac8fdf_openblas.conda - build_number: 33 - sha256: 60c2ccdfa181bc304b5162c73cdecb5b4c3972da71758472c71fefb33965cde3 - md5: e598bb54c4a4b45c3d83c72984f79dbb - depends: - - libblas 3.9.0 33_h59b9bed_openblas - constrains: - - liblapacke 3.9.0 33*_openblas - - blas 2.133 openblas - - libcblas 3.9.0 33*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 18785 - timestamp: 1754412383434 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 - md5: 1a580f7796c7bf6393fddb8bbbde58dc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - xz 5.8.1.* - license: 0BSD - purls: [] - size: 112894 - timestamp: 1749230047870 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee - md5: c7e925f37e3b40d893459e625f6a53f1 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 91183 - timestamp: 1748393666725 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 - md5: d864d34357c3b65a4b731f78c0801dc4 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-only - license_family: GPL - purls: [] - size: 33731 - timestamp: 1750274110928 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda - sha256: 3f3fc30fe340bc7f8f46fea6a896da52663b4d95caed1f144e8ea114b4bb6b61 - md5: 7e2ba4ca7e6ffebb7f7fc2da2744df61 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libgfortran - - libgfortran5 >=14.3.0 - constrains: - - openblas >=0.3.30,<0.3.31.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 5918161 - timestamp: 1753405234435 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da - md5: 0b367fad34931cb79e0d6b7e5c06bb1c - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - license: blessing - purls: [] - size: 932581 - timestamp: 1753948484112 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda - sha256: b5b239e5fca53ff90669af1686c86282c970dd8204ebf477cf679872eb6d48ac - md5: 3c376af8888c386b9d3d1c2701e2f3ab - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc 15.1.0 h767d61c_4 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 3903453 - timestamp: 1753903894186 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 - md5: 40b61aab5c7ba9ff276c41cfffe6b80b - depends: - - libgcc-ng >=12 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 33601 - timestamp: 1680112270483 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c - md5: 5aa797f8787fe7a17d1b0821485b5adc - depends: - - libgcc-ng >=12 - license: LGPL-2.1-or-later - purls: [] - size: 100393 - timestamp: 1702724383534 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other - purls: [] - size: 60963 - timestamp: 1727963148474 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 - md5: 47e340acb35de30501a76c7c799c41d7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: X11 AND BSD-3-Clause - purls: [] - size: 891641 - timestamp: 1738195959188 -- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py312h33ff503_0.conda - sha256: d54e52df67e0be7e5faa9e6f0efccea3d72f635a3159cc151c4668e5159f6ef3 - md5: 3f6efbc40eb13f019c856c410fa921d2 - depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - libstdcxx >=14 - - libgcc >=14 - - libblas >=3.9.0,<4.0a0 - - python_abi 3.12.* *_cp312 - - libcblas >=3.9.0,<4.0a0 - - liblapack >=3.9.0,<4.0a0 - constrains: - - numpy-base <0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/numpy?source=hash-mapping - size: 8785045 - timestamp: 1753401550884 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda - sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e - md5: ffffb341206dd0dab0c36053c048d621 - depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=14 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 3128847 - timestamp: 1754465526100 -- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - name: packaging - version: '25.0' - sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - sha256: 289861ed0c13a15d7bbb408796af4de72c2fe67e2bcb0de98f4c3fce259d7991 - md5: 58335b26c38bf4a20f399384c33cbcf9 - depends: - - python >=3.8 - - python - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/packaging?source=hash-mapping - size: 62477 - timestamp: 1745345660407 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda - sha256: 6ec86b1da8432059707114270b9a45d767dac97c4910ba82b1f4fa6f74e077c8 - md5: 7c73e62e62e5864b8418440e2a2cc246 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - - numpy >=1.22.4 - - numpy >=1.23,<3 - - python >=3.12,<3.13.0a0 - - python-dateutil >=2.8.2 - - python-tzdata >=2022.7 - - python_abi 3.12.* *_cp312 - - pytz >=2020.1 - constrains: - - html5lib >=1.1 - - fastparquet >=2022.12.0 - - xarray >=2022.12.0 - - pyqt5 >=5.15.9 - - pyxlsb >=1.0.10 - - matplotlib >=3.6.3 - - numba >=0.56.4 - - odfpy >=1.4.1 - - bottleneck >=1.3.6 - - tabulate >=0.9.0 - - scipy >=1.10.0 - - pyreadstat >=1.2.0 - - pandas-gbq >=0.19.0 - - openpyxl >=3.1.0 - - xlrd >=2.0.1 - - pyarrow >=10.0.1 - - xlsxwriter >=3.0.5 - - python-calamine >=0.1.7 - - gcsfs >=2022.11.0 - - zstandard >=0.19.0 - - fsspec >=2022.11.0 - - lxml >=4.9.2 - - s3fs >=2022.11.0 - - numexpr >=2.8.4 - - psycopg2 >=2.9.6 - - qtpy >=2.3.0 - - pytables >=3.8.0 - - tzdata >=2022.7 - - sqlalchemy >=2.0.0 - - beautifulsoup4 >=4.11.2 - - blosc >=1.21.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pandas?source=hash-mapping - size: 15092371 - timestamp: 1752082221274 -- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - sha256: a8eb555eef5063bbb7ba06a379fa7ea714f57d9741fe0efdb9442dbbc2cccbcc - md5: 7da7ccd349dbf6487a7778579d2bb971 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pluggy?source=hash-mapping - size: 24246 - timestamp: 1747339794916 -- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 - md5: 12c566707c80111f9799308d9e265aef - depends: - - python >=3.9 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pycparser?source=hash-mapping - size: 110100 - timestamp: 1733195786147 -- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a - md5: 6b6ece66ebcae2d5f326c77ef2c5a066 - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/pygments?source=hash-mapping - size: 889287 - timestamp: 1750615908735 -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 - md5: 461219d1a5bd61342293efa2c0c90eac - depends: - - __unix - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pysocks?source=hash-mapping - size: 21085 - timestamp: 1733217331982 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda - sha256: 93e267e4ec35353e81df707938a6527d5eb55c97bf54c3b87229b69523afb59d - md5: a49c2283f24696a7b30367b7346a0144 - depends: - - colorama >=0.4 - - exceptiongroup >=1 - - iniconfig >=1 - - packaging >=20 - - pluggy >=1.5,<2 - - pygments >=2.7.2 - - python >=3.9 - - tomli >=1 - constrains: - - pytest-faulthandler >=2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pytest?source=hash-mapping - size: 276562 - timestamp: 1750239526127 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda - sha256: 6cca004806ceceea9585d4d655059e951152fc774a471593d4f5138e6a54c81d - md5: 94206474a5608243a10c92cefbe0908f - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=13 - - liblzma >=5.8.1,<6.0a0 - - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.50.0,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libxcrypt >=4.4.36 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.0,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.12.* *_cp312 - license: Python-2.0 - purls: [] - size: 31445023 - timestamp: 1749050216615 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda - build_number: 102 - sha256: c2cdcc98ea3cbf78240624e4077e164dc9d5588eefb044b4097c3df54d24d504 - md5: 89e07d92cf50743886f41638d58c4328 - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=13 - - liblzma >=5.8.1,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.50.1,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.0,<4.0a0 - - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - license: Python-2.0 - purls: [] - size: 33273132 - timestamp: 1750064035176 - python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 - md5: 5b8d21249ff20967101ffa321cab24e8 - depends: - - python >=3.9 - - six >=1.5 - - python - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/python-dateutil?source=hash-mapping - size: 233310 - timestamp: 1751104122689 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda - sha256: e8392a8044d56ad017c08fec2b0eb10ae3d1235ac967d0aab8bd7b41c4a5eaf0 - md5: 88476ae6ebd24f39261e0854ac244f33 - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/tzdata?source=hash-mapping - size: 144160 - timestamp: 1742745254292 -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - build_number: 8 - sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 - md5: c3efd25ac4d74b1584d2f7a57195ddf1 - constrains: - - python 3.12.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6958 - timestamp: 1752805918820 -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - build_number: 8 - sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 - md5: 94305520c52a4aa3f6c2b1ff6008d9f8 - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 7002 - timestamp: 1752805902938 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0 - md5: bc8e3267d44011051f2eb14d22fb0960 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pytz?source=hash-mapping - size: 189015 - timestamp: 1742920947249 -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c - md5: 283b96675859b20a825f8fa30f311446 - depends: - - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 282480 - timestamp: 1740379431762 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda - sha256: 9866aaf7a13c6cfbe665ec7b330647a0fb10a81e6f9b8fee33642232a1920e18 - md5: f6082eae112814f1447b56a5e1f6ed05 - depends: - - certifi >=2017.4.17 - - charset-normalizer >=2,<4 - - idna >=2.5,<4 - - python >=3.9 - - urllib3 >=1.21.1,<3 - constrains: - - chardet >=3.0.2,<6 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/requests?source=hash-mapping - size: 59407 - timestamp: 1749498221996 -- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d - md5: 3339e3b65d58accf4ca4fb8748ab16b3 - depends: - - python >=3.9 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/six?source=hash-mapping - size: 18455 - timestamp: 1753199211006 -- pypi: ./ - name: spec0-action - version: 0.1.0 - sha256: af5bbf8d5b2207994b1d182b88167dc8de25271e802b84c07d4a2358c454dcf3 - requires_dist: - - packaging>=25.0,<26 - requires_python: '>=3.11' - editable: true -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 - md5: a0116df4f4ed05c303811a837d5b39d8 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3285204 - timestamp: 1748387766691 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - sha256: 040a5a05c487647c089ad5e05ad5aff5942830db2a4e656f1e300d73436436f1 - md5: 30a0a26c8abccf4b7991d590fe17c699 - depends: - - python >=3.9 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomli?source=compressed-mapping - size: 21238 - timestamp: 1753796677376 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.3-pyha770c72_0.conda - sha256: f8d3b49c084831a20923f66826f30ecfc55a4cd951e544b7213c692887343222 - md5: 146402bf0f11cbeb8f781fa4309a95d3 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomlkit?source=hash-mapping - size: 38777 - timestamp: 1749127286558 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda - sha256: 4f52390e331ea8b9019b87effaebc4f80c6466d09f68453f52d5cdc2a3e1194f - md5: e523f4f1e980ed7a4240d7e27e9ec81f - depends: - - python >=3.9 - - python - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/typing-extensions?source=hash-mapping - size: 51065 - timestamp: 1751643513473 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 - md5: 4222072737ccff51314b5ece9c7d6f5a - license: LicenseRef-Public-Domain - purls: [] - size: 122968 - timestamp: 1742727099393 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 - md5: 436c165519e140cb08d246a4472a9d6a - depends: - - brotli-python >=1.0.9 - - h2 >=4,<5 - - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.9 - - zstandard >=0.18.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/urllib3?source=hash-mapping - size: 101735 - timestamp: 1750271478254 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda - sha256: ff62d2e1ed98a3ec18de7e5cf26c0634fd338cb87304cf03ad8cbafe6fe674ba - md5: 630db208bc7bbb96725ce9832c7423bb - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.11 - - libgcc >=13 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 732224 - timestamp: 1745869780524 From 999a1f31a9dfd012e73cab8faefe2d5ffa0f9e7f Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 09:45:26 +0200 Subject: [PATCH 31/32] fix: rename udpate script --- action.yaml | 2 +- update.py => spec0-update.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename update.py => spec0-update.py (100%) diff --git a/action.yaml b/action.yaml index 7e3d699..1c558dd 100644 --- a/action.yaml +++ b/action.yaml @@ -56,7 +56,7 @@ runs: - name: Run Update script shell: bash run: | - python ${{github.action_path}}/update.py "${{github.workspace}}/${{inputs.project_file_name}}" "${{inputs.schedule_path}}" + python ${{github.action_path}}/spec0-update.py "${{github.workspace}}/${{inputs.project_file_name}}" "${{inputs.schedule_path}}" # let's cleanup after ourselves so it doesn't end up in the PR rm "${{inputs.schedule_path}}" diff --git a/update.py b/spec0-update.py similarity index 100% rename from update.py rename to spec0-update.py From d3f3bb68dd881c109c21fb23bd7f0d4c827e2f1c Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Wed, 20 Aug 2025 09:48:56 +0200 Subject: [PATCH 32/32] fix: revert rename of versions script --- pyproject.toml | 2 +- generate_schedule.py => spec0-versions.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename generate_schedule.py => spec0-versions.py (100%) diff --git a/pyproject.toml b/pyproject.toml index ac74c6c..fb1c998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ spec0-action = { path = ".", editable = true } test = { cmd = ["pytest", "-vvv"] } [tool.pixi.feature.schedule.tasks] -generate-schedule = { cmd = ["python", "generate_schedule.py"] } +generate-schedule = { cmd = ["python", "spec0-verions.py"] } [tool.pixi.feature.test.dependencies] pytest = "*" diff --git a/generate_schedule.py b/spec0-versions.py similarity index 100% rename from generate_schedule.py rename to spec0-versions.py