diff --git a/spec-0000/SPEC0_versions.py b/spec-0000/SPEC0_versions.py index 094216e0..f2dfcfea 100644 --- a/spec-0000/SPEC0_versions.py +++ b/spec-0000/SPEC0_versions.py @@ -1,4 +1,5 @@ import requests +import json import collections from datetime import datetime, timedelta @@ -148,8 +149,32 @@ def get_release_dates(package, support_time=plus24): 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") -dq = df.set_index(["quarter", "package"]).sort_index() +with open("schedule.json", "w") as f: + f.write(json.dumps(new_min_versions_list, sort_keys=True)) print("Saving drop schedule to schedule.md") diff --git a/spec-0000/index.md b/spec-0000/index.md index 0bdf2f53..465acf5e 100644 --- a/spec-0000/index.md +++ b/spec-0000/index.md @@ -97,6 +97,89 @@ You may want to delay the removal of support of an older Python version until yo {{< include-md "schedule.md" >}} +### Automatically updating dependencies + +To help projects stay compliant with this spec, we additionally provide a `schedule.json` file that can be used by CI systems to deterime 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: + +1. fetch `schedule.json` +2. determine maximum date that is smaller than current date +3. update packages listed with new minimum versions + +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 + +``` + +If you use a package manager like pixi you could update the dependencies with a bash script like this: + +```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 + +``` + +You can create a GH action that runs every quarter like so + +```yaml +name: Update SPEC 0 dependencies + +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: + auto-update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: update + run: | + + 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) + + # don't add packages that aren't already added. + if pixi list -x "^$package" &>/dev/null| grep "No packages" -q; then + pixi add "$package>=$version"; + fi + done + - uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update/spec-0 + title: Drop support for packages according to SPEC 0 + commit-message: "Update dependencies" + body: Increase minimum version of . + author: "GitHub " +``` + ## Notes - This document builds on [NEP 29](https://numpy.org/neps/nep-0029-deprecation_policy.html), which describes several alternatives including ad hoc version support, all CPython supported versions, default version on Linux distribution, N minor versions of Python, and time window from the X.Y.1 Python release. diff --git a/spec-0000/schedule.json b/spec-0000/schedule.json new file mode 100644 index 00000000..c11e7ad5 --- /dev/null +++ b/spec-0000/schedule.json @@ -0,0 +1 @@ +[{"packages": {"ipython": "8.5.0", "networkx": "3.0", "numpy": "1.24.0", "scikit-learn": "1.2.0", "zarr": "2.13.0"}, "start_date": "2024-04-01T00:00:00Z"}, {"packages": {"ipython": "8.6.0", "matplotlib": "3.7.0", "pandas": "2.0.0", "scipy": "1.10.0", "xarray": "2022.10.0", "zarr": "2.14.0"}, "start_date": "2024-07-01T00:00:00Z"}, {"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", "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", "python": "3.13", "xarray": "2025.1.0"}, "start_date": "2026-10-01T00:00:00Z"}]