diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b5e850..7cc8015 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ on: workflow_dispatch: env: - python-version: "3.12" + python-version: "3.13" jobs: build: @@ -65,12 +65,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] dependencies: ["pinned"] include: - - python-version: "3.12" + - python-version: "3.13" dependencies: "latest" - - python-version: "3.10" + - python-version: "3.11" dependencies: "minimum" @@ -135,7 +135,7 @@ jobs: uses: actions/cache@v3 with: path: '.mypy_cache' - key: mypy-${{ runner.os }}-py${{ env.python-version }}-${{ hashFiles('requirements.txt') }} + key: mypy-${{ runner.os }}-py${{ env.python-version }}-${{ hashFiles(format('continuous-integration/requirements-{0}.txt', env.python-version)) }} - run: flake8 src/ tests/ - run: isort --diff --check-only src/ tests/ diff --git a/continuous-integration/requirements-3.11.txt b/continuous-integration/requirements-3.11.txt index 3a4718d..177b6de 100644 --- a/continuous-integration/requirements-3.11.txt +++ b/continuous-integration/requirements-3.11.txt @@ -2,75 +2,76 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=testing --output-file=./continuous-integration/requirements-3.11.txt setup.cfg +# pip-compile --extra=testing --output-file=./continuous-integration/requirements-3.11.txt --unsafe-package=emsarray pyproject.toml # -bokeh==3.4.2 +bokeh==3.6.2 # via dask -bottleneck==1.4.0 - # via emsarray (setup.cfg) -cartopy==0.23.0 - # via emsarray (setup.cfg) -certifi==2024.6.2 +bottleneck==1.4.2 + # via + # emsarray + # emsarray (pyproject.toml) +cartopy==0.24.1 + # via emsarray +certifi==2024.12.14 # via # netcdf4 # pyproj # requests -cftime==1.6.4 +cftime==1.6.4.post1 # via # cfunits # netcdf4 cfunits==3.3.7 - # via emsarray (setup.cfg) -charset-normalizer==3.3.2 + # via emsarray +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # dask # distributed -cloudpickle==3.0.0 +cloudpickle==3.1.1 # via # dask # distributed -contourpy==1.2.1 +contourpy==1.3.1 # via # bokeh # matplotlib -coverage[toml]==7.5.4 +coverage[toml]==7.6.10 # via pytest-cov cycler==0.12.1 # via matplotlib -dask[array,complete,dataframe,diagnostics,distributed]==2024.6.2 +dask[array,complete,dataframe,diagnostics,distributed]==2025.1.0 # via - # dask-expr # distributed # xarray -dask-expr==1.1.6 - # via dask -distributed==2024.6.2 +distributed==2025.1.0 # via dask -flake8==7.1.0 - # via emsarray (setup.cfg) -fonttools==4.53.0 +flake8==7.1.1 + # via emsarray (pyproject.toml) +fonttools==4.55.4 # via matplotlib -fsspec==2024.6.1 +fsspec==2024.12.0 # via dask -geojson==3.1.0 - # via emsarray (setup.cfg) -idna==3.7 +geojson==3.2.0 + # via + # emsarray + # emsarray (pyproject.toml) +idna==3.10 # via requests -importlib-metadata==8.0.0 +importlib-metadata==8.6.1 # via dask iniconfig==2.0.0 # via pytest isort==5.13.2 - # via emsarray (setup.cfg) -jinja2==3.1.4 + # via emsarray (pyproject.toml) +jinja2==3.1.5 # via # bokeh # dask # distributed # pytest-mpl -kiwisolver==1.4.5 +kiwisolver==1.4.8 # via matplotlib locket==1.0.0 # via @@ -78,24 +79,26 @@ locket==1.0.0 # partd lz4==4.3.3 # via dask -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -matplotlib==3.9.0 +matplotlib==3.10.0 # via # cartopy - # emsarray (setup.cfg) + # emsarray # pytest-mpl mccabe==0.7.0 # via flake8 -msgpack==1.0.8 +msgpack==1.1.0 # via distributed -mypy==1.10.1 - # via emsarray (setup.cfg) +mypy==1.14.1 + # via emsarray (pyproject.toml) mypy-extensions==1.0.0 # via mypy -netcdf4==1.7.1.post1 - # via emsarray (setup.cfg) -numpy==2.0.0 +netcdf4==1.7.2 + # via + # emsarray + # emsarray (pyproject.toml) +numpy==2.2.2 # via # bokeh # bottleneck @@ -104,129 +107,132 @@ numpy==2.0.0 # cfunits # contourpy # dask - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # netcdf4 # pandas # pandas-stubs - # pyarrow # pykdtree # shapely # xarray -packaging==24.1 +packaging==24.2 # via # bokeh # cartopy # cfunits # dask # distributed - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # pooch # pytest # pytest-mpl # xarray -pandas==2.2.2 +pandas==2.2.3 # via # bokeh # dask - # dask-expr # xarray -pandas-stubs==2.2.2.240603 - # via emsarray (setup.cfg) +pandas-stubs==2.2.3.241126 + # via emsarray (pyproject.toml) partd==1.4.2 # via dask -pillow==10.4.0 +pillow==11.1.0 # via # bokeh # matplotlib # pytest-mpl -platformdirs==4.2.2 +platformdirs==4.3.6 # via pooch pluggy==1.5.0 # via pytest pooch==1.8.2 - # via emsarray (setup.cfg) -psutil==6.0.0 + # via emsarray +psutil==6.1.1 # via distributed -pyarrow==16.1.0 - # via - # dask - # dask-expr -pyarrow-hotfix==0.6 +pyarrow==19.0.0 # via dask -pycodestyle==2.12.0 +pycodestyle==2.12.1 # via flake8 pyflakes==3.2.0 # via flake8 -pykdtree==1.3.12 - # via emsarray (setup.cfg) -pyparsing==3.1.2 +pykdtree==1.3.13 + # via emsarray +pyparsing==3.2.1 # via matplotlib -pyproj==3.6.1 +pyproj==3.7.0 # via cartopy pyshp==2.3.1 # via # cartopy - # emsarray (setup.cfg) -pytest==8.2.2 + # emsarray + # emsarray (pyproject.toml) +pytest==8.3.4 # via - # emsarray (setup.cfg) + # emsarray (pyproject.toml) # pytest-cov # pytest-mpl -pytest-cov==5.0.0 - # via emsarray (setup.cfg) +pytest-cov==6.0.0 + # via emsarray (pyproject.toml) pytest-mpl==0.17.0 - # via emsarray (setup.cfg) + # via emsarray (pyproject.toml) python-dateutil==2.9.0.post0 # via # matplotlib # pandas -pytz==2024.1 +pytz==2024.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via # bokeh # dask # distributed requests==2.32.3 # via pooch -shapely==2.0.4 +shapely==2.0.6 # via # cartopy - # emsarray (setup.cfg) -six==1.16.0 + # emsarray + # emsarray (pyproject.toml) +six==1.17.0 # via python-dateutil sortedcontainers==2.4.0 # via distributed tblib==3.0.0 # via distributed -toolz==0.12.1 +toolz==1.0.0 # via # dask # distributed # partd -tornado==6.4.1 +tornado==6.4.2 # via # bokeh # distributed -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20241221 # via - # emsarray (setup.cfg) + # emsarray (pyproject.toml) # pandas-stubs typing-extensions==4.12.2 # via mypy -tzdata==2024.1 +tzdata==2025.1 # via pandas -urllib3==2.2.2 +urllib3==2.3.0 # via # distributed # requests -xarray[parallel]==2024.6.0 - # via emsarray (setup.cfg) -xyzservices==2024.6.0 +xarray[parallel]==2025.1.1 + # via + # emsarray + # emsarray (pyproject.toml) +xyzservices==2025.1.0 # via bokeh zict==3.0.0 # via distributed -zipp==3.19.2 +zipp==3.21.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# emsarray diff --git a/continuous-integration/requirements-3.12.txt b/continuous-integration/requirements-3.12.txt index 86f5c78..a282b8c 100644 --- a/continuous-integration/requirements-3.12.txt +++ b/continuous-integration/requirements-3.12.txt @@ -2,73 +2,74 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --extra=testing --output-file=./continuous-integration/requirements-3.12.txt setup.cfg +# pip-compile --extra=testing --output-file=./continuous-integration/requirements-3.12.txt --unsafe-package=emsarray pyproject.toml # -bokeh==3.4.2 +bokeh==3.6.2 # via dask -bottleneck==1.4.0 - # via emsarray (setup.cfg) -cartopy==0.23.0 - # via emsarray (setup.cfg) -certifi==2024.6.2 +bottleneck==1.4.2 + # via + # emsarray + # emsarray (pyproject.toml) +cartopy==0.24.1 + # via emsarray +certifi==2024.12.14 # via # netcdf4 # pyproj # requests -cftime==1.6.4 +cftime==1.6.4.post1 # via # cfunits # netcdf4 cfunits==3.3.7 - # via emsarray (setup.cfg) -charset-normalizer==3.3.2 + # via emsarray +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # dask # distributed -cloudpickle==3.0.0 +cloudpickle==3.1.1 # via # dask # distributed -contourpy==1.2.1 +contourpy==1.3.1 # via # bokeh # matplotlib -coverage[toml]==7.5.4 +coverage[toml]==7.6.10 # via pytest-cov cycler==0.12.1 # via matplotlib -dask[array,complete,dataframe,diagnostics,distributed]==2024.6.2 +dask[array,complete,dataframe,diagnostics,distributed]==2025.1.0 # via - # dask-expr # distributed # xarray -dask-expr==1.1.6 - # via dask -distributed==2024.6.2 +distributed==2025.1.0 # via dask -flake8==7.1.0 - # via emsarray (setup.cfg) -fonttools==4.53.0 +flake8==7.1.1 + # via emsarray (pyproject.toml) +fonttools==4.55.4 # via matplotlib -fsspec==2024.6.1 +fsspec==2024.12.0 # via dask -geojson==3.1.0 - # via emsarray (setup.cfg) -idna==3.7 +geojson==3.2.0 + # via + # emsarray + # emsarray (pyproject.toml) +idna==3.10 # via requests iniconfig==2.0.0 # via pytest isort==5.13.2 - # via emsarray (setup.cfg) -jinja2==3.1.4 + # via emsarray (pyproject.toml) +jinja2==3.1.5 # via # bokeh # dask # distributed # pytest-mpl -kiwisolver==1.4.5 +kiwisolver==1.4.8 # via matplotlib locket==1.0.0 # via @@ -76,24 +77,26 @@ locket==1.0.0 # partd lz4==4.3.3 # via dask -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -matplotlib==3.9.0 +matplotlib==3.10.0 # via # cartopy - # emsarray (setup.cfg) + # emsarray # pytest-mpl mccabe==0.7.0 # via flake8 -msgpack==1.0.8 +msgpack==1.1.0 # via distributed -mypy==1.10.1 - # via emsarray (setup.cfg) +mypy==1.14.1 + # via emsarray (pyproject.toml) mypy-extensions==1.0.0 # via mypy -netcdf4==1.7.1.post1 - # via emsarray (setup.cfg) -numpy==2.0.0 +netcdf4==1.7.2 + # via + # emsarray + # emsarray (pyproject.toml) +numpy==2.2.2 # via # bokeh # bottleneck @@ -102,127 +105,130 @@ numpy==2.0.0 # cfunits # contourpy # dask - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # netcdf4 # pandas # pandas-stubs - # pyarrow # pykdtree # shapely # xarray -packaging==24.1 +packaging==24.2 # via # bokeh # cartopy # cfunits # dask # distributed - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # pooch # pytest # pytest-mpl # xarray -pandas==2.2.2 +pandas==2.2.3 # via # bokeh # dask - # dask-expr # xarray -pandas-stubs==2.2.2.240603 - # via emsarray (setup.cfg) +pandas-stubs==2.2.3.241126 + # via emsarray (pyproject.toml) partd==1.4.2 # via dask -pillow==10.4.0 +pillow==11.1.0 # via # bokeh # matplotlib # pytest-mpl -platformdirs==4.2.2 +platformdirs==4.3.6 # via pooch pluggy==1.5.0 # via pytest pooch==1.8.2 - # via emsarray (setup.cfg) -psutil==6.0.0 + # via emsarray +psutil==6.1.1 # via distributed -pyarrow==16.1.0 - # via - # dask - # dask-expr -pyarrow-hotfix==0.6 +pyarrow==19.0.0 # via dask -pycodestyle==2.12.0 +pycodestyle==2.12.1 # via flake8 pyflakes==3.2.0 # via flake8 -pykdtree==1.3.12 - # via emsarray (setup.cfg) -pyparsing==3.1.2 +pykdtree==1.3.13 + # via emsarray +pyparsing==3.2.1 # via matplotlib -pyproj==3.6.1 +pyproj==3.7.0 # via cartopy pyshp==2.3.1 # via # cartopy - # emsarray (setup.cfg) -pytest==8.2.2 + # emsarray + # emsarray (pyproject.toml) +pytest==8.3.4 # via - # emsarray (setup.cfg) + # emsarray (pyproject.toml) # pytest-cov # pytest-mpl -pytest-cov==5.0.0 - # via emsarray (setup.cfg) +pytest-cov==6.0.0 + # via emsarray (pyproject.toml) pytest-mpl==0.17.0 - # via emsarray (setup.cfg) + # via emsarray (pyproject.toml) python-dateutil==2.9.0.post0 # via # matplotlib # pandas -pytz==2024.1 +pytz==2024.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via # bokeh # dask # distributed requests==2.32.3 # via pooch -shapely==2.0.4 +shapely==2.0.6 # via # cartopy - # emsarray (setup.cfg) -six==1.16.0 + # emsarray + # emsarray (pyproject.toml) +six==1.17.0 # via python-dateutil sortedcontainers==2.4.0 # via distributed tblib==3.0.0 # via distributed -toolz==0.12.1 +toolz==1.0.0 # via # dask # distributed # partd -tornado==6.4.1 +tornado==6.4.2 # via # bokeh # distributed -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20241221 # via - # emsarray (setup.cfg) + # emsarray (pyproject.toml) # pandas-stubs typing-extensions==4.12.2 # via mypy -tzdata==2024.1 +tzdata==2025.1 # via pandas -urllib3==2.2.2 +urllib3==2.3.0 # via # distributed # requests -xarray[parallel]==2024.6.0 - # via emsarray (setup.cfg) -xyzservices==2024.6.0 +xarray[parallel]==2025.1.1 + # via + # emsarray + # emsarray (pyproject.toml) +xyzservices==2025.1.0 # via bokeh zict==3.0.0 # via distributed + +# The following packages are considered to be unsafe in a requirements file: +# emsarray diff --git a/continuous-integration/requirements-3.10.txt b/continuous-integration/requirements-3.13.txt similarity index 55% rename from continuous-integration/requirements-3.10.txt rename to continuous-integration/requirements-3.13.txt index 2aa1748..f89c8fc 100644 --- a/continuous-integration/requirements-3.10.txt +++ b/continuous-integration/requirements-3.13.txt @@ -1,78 +1,75 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile --extra=testing --output-file=./continuous-integration/requirements-3.10.txt setup.cfg +# pip-compile --extra=testing --output-file=./continuous-integration/requirements-3.13.txt --unsafe-package=emsarray pyproject.toml # -bokeh==3.4.2 +bokeh==3.6.2 # via dask -bottleneck==1.4.0 - # via emsarray (setup.cfg) -cartopy==0.23.0 - # via emsarray (setup.cfg) -certifi==2024.6.2 +bottleneck==1.4.2 + # via + # emsarray + # emsarray (pyproject.toml) +cartopy==0.24.1 + # via emsarray +certifi==2024.12.14 # via # netcdf4 # pyproj # requests -cftime==1.6.4 +cftime==1.6.4.post1 # via # cfunits # netcdf4 cfunits==3.3.7 - # via emsarray (setup.cfg) -charset-normalizer==3.3.2 + # via emsarray +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # dask # distributed -cloudpickle==3.0.0 +cloudpickle==3.1.1 # via # dask # distributed -contourpy==1.2.1 +contourpy==1.3.1 # via # bokeh # matplotlib -coverage[toml]==7.5.4 +coverage[toml]==7.6.10 # via pytest-cov cycler==0.12.1 # via matplotlib -dask[array,complete,dataframe,diagnostics,distributed]==2024.6.2 +dask[array,complete,dataframe,diagnostics,distributed]==2025.1.0 # via - # dask-expr # distributed # xarray -dask-expr==1.1.6 - # via dask -distributed==2024.6.2 +distributed==2025.1.0 # via dask -exceptiongroup==1.2.1 - # via pytest -flake8==7.1.0 - # via emsarray (setup.cfg) -fonttools==4.53.0 +flake8==7.1.1 + # via emsarray (pyproject.toml) +fonttools==4.55.4 # via matplotlib -fsspec==2024.6.1 +fsspec==2024.12.0 # via dask -geojson==3.1.0 - # via emsarray (setup.cfg) -idna==3.7 +geojson==3.2.0 + # via + # emsarray + # emsarray (pyproject.toml) +idna==3.10 # via requests -importlib-metadata==8.0.0 - # via dask iniconfig==2.0.0 # via pytest isort==5.13.2 - # via emsarray (setup.cfg) -jinja2==3.1.4 + # via emsarray (pyproject.toml) +jinja2==3.1.5 # via # bokeh # dask # distributed # pytest-mpl -kiwisolver==1.4.5 +kiwisolver==1.4.8 # via matplotlib locket==1.0.0 # via @@ -80,24 +77,26 @@ locket==1.0.0 # partd lz4==4.3.3 # via dask -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -matplotlib==3.9.0 +matplotlib==3.10.0 # via # cartopy - # emsarray (setup.cfg) + # emsarray # pytest-mpl mccabe==0.7.0 # via flake8 -msgpack==1.0.8 +msgpack==1.1.0 # via distributed -mypy==1.10.1 - # via emsarray (setup.cfg) +mypy==1.14.1 + # via emsarray (pyproject.toml) mypy-extensions==1.0.0 # via mypy -netcdf4==1.7.1.post1 - # via emsarray (setup.cfg) -numpy==2.0.0 +netcdf4==1.7.2 + # via + # emsarray + # emsarray (pyproject.toml) +numpy==2.2.2 # via # bokeh # bottleneck @@ -106,134 +105,130 @@ numpy==2.0.0 # cfunits # contourpy # dask - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # netcdf4 # pandas # pandas-stubs - # pyarrow # pykdtree # shapely # xarray -packaging==24.1 +packaging==24.2 # via # bokeh # cartopy # cfunits # dask # distributed - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # pooch # pytest # pytest-mpl # xarray -pandas==2.2.2 +pandas==2.2.3 # via # bokeh # dask - # dask-expr # xarray -pandas-stubs==2.2.2.240603 - # via emsarray (setup.cfg) +pandas-stubs==2.2.3.241126 + # via emsarray (pyproject.toml) partd==1.4.2 # via dask -pillow==10.4.0 +pillow==11.1.0 # via # bokeh # matplotlib # pytest-mpl -platformdirs==4.2.2 +platformdirs==4.3.6 # via pooch pluggy==1.5.0 # via pytest pooch==1.8.2 - # via emsarray (setup.cfg) -psutil==6.0.0 + # via emsarray +psutil==6.1.1 # via distributed -pyarrow==16.1.0 - # via - # dask - # dask-expr -pyarrow-hotfix==0.6 +pyarrow==19.0.0 # via dask -pycodestyle==2.12.0 +pycodestyle==2.12.1 # via flake8 pyflakes==3.2.0 # via flake8 -pykdtree==1.3.12 - # via emsarray (setup.cfg) -pyparsing==3.1.2 +pykdtree==1.3.13 + # via emsarray +pyparsing==3.2.1 # via matplotlib -pyproj==3.6.1 +pyproj==3.7.0 # via cartopy pyshp==2.3.1 # via # cartopy - # emsarray (setup.cfg) -pytest==8.2.2 + # emsarray + # emsarray (pyproject.toml) +pytest==8.3.4 # via - # emsarray (setup.cfg) + # emsarray (pyproject.toml) # pytest-cov # pytest-mpl -pytest-cov==5.0.0 - # via emsarray (setup.cfg) +pytest-cov==6.0.0 + # via emsarray (pyproject.toml) pytest-mpl==0.17.0 - # via emsarray (setup.cfg) + # via emsarray (pyproject.toml) python-dateutil==2.9.0.post0 # via # matplotlib # pandas -pytz==2024.1 +pytz==2024.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via # bokeh # dask # distributed requests==2.32.3 # via pooch -shapely==2.0.4 +shapely==2.0.6 # via # cartopy - # emsarray (setup.cfg) -six==1.16.0 + # emsarray + # emsarray (pyproject.toml) +six==1.17.0 # via python-dateutil sortedcontainers==2.4.0 # via distributed tblib==3.0.0 # via distributed -tomli==2.0.1 - # via - # coverage - # mypy - # pytest -toolz==0.12.1 +toolz==1.0.0 # via # dask # distributed # partd -tornado==6.4.1 +tornado==6.4.2 # via # bokeh # distributed -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20241221 # via - # emsarray (setup.cfg) + # emsarray (pyproject.toml) # pandas-stubs typing-extensions==4.12.2 # via mypy -tzdata==2024.1 +tzdata==2025.1 # via pandas -urllib3==2.2.2 +urllib3==2.3.0 # via # distributed # requests -xarray[parallel]==2024.6.0 - # via emsarray (setup.cfg) -xyzservices==2024.6.0 +xarray[parallel]==2025.1.1 + # via + # emsarray + # emsarray (pyproject.toml) +xyzservices==2025.1.0 # via bokeh zict==3.0.0 # via distributed -zipp==3.19.2 - # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# emsarray diff --git a/continuous-integration/requirements-minimum.txt b/continuous-integration/requirements-minimum.txt index b636764..e550cdf 100644 --- a/continuous-integration/requirements-minimum.txt +++ b/continuous-integration/requirements-minimum.txt @@ -1,65 +1,63 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=complete --output-file=continuous-integration/requirements-minimum.txt setup.cfg +# pip-compile --extra=complete --output-file=./continuous-integration/requirements-minimum.txt --strip-extras --unsafe-package=certifi --unsafe-package=emsarray --unsafe-package=pytz --unsafe-package=tzdata pyproject.toml # -bokeh~=3.3.0 +bokeh~=3.4.3 # via dask -bottleneck~=1.3.0 - # via emsarray (setup.cfg) -cartopy~=0.22.0 - # via emsarray (setup.cfg) -certifi +bottleneck~=1.3.8 # via - # netcdf4 - # pyproj - # requests -cftime~=1.6.4 + # emsarray + # emsarray (pyproject.toml) +cartopy~=0.22.0 + # via emsarray +cftime~=1.6.4.post1 # via # cfunits # netcdf4 cfunits~=3.3.7 - # via emsarray (setup.cfg) -charset-normalizer~=3.2.0 + # via emsarray +charset-normalizer~=3.3.2 # via requests -click~=8.1.7 +click~=8.1.8 # via # dask # distributed -cloudpickle~=2.2.0 +cloudpickle~=3.0.0 # via # dask # distributed -contourpy~=1.1.0 +contourpy~=1.2.1 # via # bokeh # matplotlib -cycler~=0.11.0 +cycler~=0.12.1 # via matplotlib -dask[array,complete,dataframe,diagnostics,distributed]~=2023.8.0 +dask~=2024.1.1 # via - # dask-expr # distributed # xarray -distributed~=2023.8.0 +distributed~=2024.1.1 # via dask -fonttools~=4.42.0 +fonttools~=4.47.2 # via matplotlib -fsspec~=2023.6.0 +fsspec~=2023.12.2 # via dask -geojson~=3.0.0 - # via emsarray (setup.cfg) -idna~=3.4.0 +geojson~=3.1.0 + # via + # emsarray + # emsarray (pyproject.toml) +idna~=3.6 # via requests -importlib-metadata~=6.8.0 +importlib-metadata~=7.0.2 # via dask -jinja2~=3.1.4 +jinja2~=3.1.5 # via # bokeh # dask # distributed -kiwisolver~=1.4.5 +kiwisolver~=1.4.8 # via matplotlib locket~=1.0.0 # via @@ -69,15 +67,17 @@ lz4~=4.3.3 # via dask markupsafe~=2.1.5 # via jinja2 -matplotlib~=3.8.0 +matplotlib~=3.8.4 # via # cartopy - # emsarray (setup.cfg) + # emsarray msgpack~=1.0.8 # via distributed -netcdf4~=1.6.0 - # via emsarray (setup.cfg) -numpy~=1.24.0 +netcdf4~=1.6.5 + # via + # emsarray + # emsarray (pyproject.toml) +numpy~=1.25.2 # via # bokeh # bottleneck @@ -86,7 +86,8 @@ numpy~=1.24.0 # cfunits # contourpy # dask - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # netcdf4 # pandas @@ -94,94 +95,101 @@ numpy~=1.24.0 # pykdtree # shapely # xarray -packaging~=23.1.0 +packaging~=23.2 # via # bokeh # cartopy # cfunits # dask # distributed - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) # matplotlib # pooch # xarray -pandas~=2.1.0 +pandas~=2.2.3 # via # bokeh # dask - # dask-expr # xarray partd~=1.4.2 # via dask -pillow~=10.0.0 +pillow~=10.2.0 # via # bokeh # matplotlib -platformdirs~=3.10.0 +platformdirs~=4.1.0 # via pooch -pooch~=1.7.0 - # via emsarray (setup.cfg) -psutil~=5.9.0 +pooch~=1.8.2 + # via emsarray +psutil~=5.9.8 # via distributed -pyarrow~=13.0.0 - # via - # dask - # dask-expr -pyarrow-hotfix~=0.6.0 +pyarrow~=15.0.2 + # via dask +pyarrow-hotfix~=0.6 # via dask -pykdtree~=1.3.12 - # via emsarray (setup.cfg) -pyparsing~=3.1.2 +pykdtree~=1.3.13 + # via emsarray +pyparsing~=3.1.4 # via matplotlib pyproj~=3.6.1 # via cartopy pyshp~=2.3.1 # via # cartopy - # emsarray (setup.cfg) -python-dateutil~=2.8.0 + # emsarray + # emsarray (pyproject.toml) +python-dateutil~=2.8.2 # via # matplotlib # pandas -pytz - # via pandas -pyyaml~=6.0.1 +pyyaml~=6.0.2 # via # bokeh # dask # distributed requests~=2.31.0 # via pooch -shapely~=2.0.4 +shapely~=2.0.6 # via # cartopy - # emsarray (setup.cfg) + # emsarray + # emsarray (pyproject.toml) six~=1.16.0 # via python-dateutil sortedcontainers~=2.4.0 # via distributed -tblib~=2.0.0 +tblib~=3.0.0 # via distributed toolz~=0.12.1 # via # dask # distributed # partd -tornado~=6.3.0 +tornado~=6.4.2 # via # bokeh # distributed -tzdata - # via pandas -urllib3~=2.0.0 +urllib3~=2.1.0 # via # distributed # requests -xarray[parallel]~=2023.8.0 - # via emsarray (setup.cfg) -xyzservices~=2023.7.0 +xarray~=2024.1.1 + # via + # emsarray + # emsarray (pyproject.toml) +xyzservices~=2023.10.1 # via bokeh zict~=3.0.0 # via distributed -zipp~=3.16.0 +zipp~=3.17.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# certifi +# emsarray +# pytz +# tzdata +pytz +certifi +tzdata diff --git a/docs/internal/ci-dependencies.rst b/docs/internal/ci-dependencies.rst new file mode 100644 index 0000000..d1a7bdc --- /dev/null +++ b/docs/internal/ci-dependencies.rst @@ -0,0 +1,59 @@ +============================================== +Dependency versions for continuous integration +============================================== + +In order to make CI reproducible we pin the installed Python dependencies. +These dependencies should be updated every few months in order to stay relevant. + +CI tests are also run against the latest of all dependencies in a separate job. +This job is allowed to fail without breaking the build. +It acts as a canary so we can see new breakages as they happen +without needing to fix issues from new package versions in unrelated pull requests. + +Supported Python versions and dependency versions follows `SPEC-0000 `_. + +Updating supported Python versions +================================== + +To add a new supported version of Python the following changes must be made: + +* Update the pinned dependencies: + add a new version to the list in ``scripts/update_pinned_dependencies.sh``, and + rebuild the pinned dependencies by running the script. +* Update ``.github/workflows/ci.yaml``: + set ``env.python-version`` to the new version, + add the new version to the ``test`` job ``python-version`` matrix variable, and + update the version associated with the ``dependencies: "latest"`` matrix job. +* Update ``tox.ini``: + add the new version to the ``envlist`` for pinned dependencies, and + update the version used in the latest dependencies. +* Add a release note. + +To remove support for an old version of Python the following changes must be made: + +* Update the pinned dependencies: + remove the old version from the list in ``scripts/update_pinned_dependencies.sh``, + remove the old ``continuous-integration/requirements-X.YY.txt`` file, and + rebuild the pinned dependencies by running the script. +* Update ``.github/workflows/ci.yaml``: + remove the version from the ``test`` job ``python-version`` matrix variable, and + update the version associated with the ``dependencies: "minimum"`` matrix job. +* Update ``tox.ini``: + remove the old version from the ``envlist`` for pinned dependencies, and + update the version used in the minimum dependencies. +* Update ``pyproject.toml``: + update the ``[project] requires-python`` field, and + update the ``[tool.mypy] python_version`` field. +* Add a release note. + +Updating pinned dependencies +============================ + +To update the list of pinned dependencies used for CI +run the ``scripts/update_pinned_dependencies.sh`` script. +This will create an isolated conda prefix for each of the supported Python versions, +install `pip-tools `_, +and create a fresh requirements document. +Commit the changes to the pinned requirements, +run the test suite against all supported Python versions, and +fix any new issues that appear. diff --git a/docs/internal/index.rst b/docs/internal/index.rst index 2fd0c85..553484a 100644 --- a/docs/internal/index.rst +++ b/docs/internal/index.rst @@ -8,3 +8,4 @@ Internal documentation :maxdepth: 1 releasing + ci-dependencies diff --git a/docs/releases/development.rst b/docs/releases/development.rst index b97a90e..3c0ecf7 100644 --- a/docs/releases/development.rst +++ b/docs/releases/development.rst @@ -6,3 +6,7 @@ Next release (in development) connectivity(:issue:`165`, :pr:`168`). * Fix datasets hash_key generation when geometry encoding is missing a dtype (:issue:`166`, :pr:`167`). +* Bumped minimum versions of Python and package dependencies in line with + `SPEC-0000 `_. + Support for Python 3.10 was dropped, and support for Python 3.13 was added + (:pr:`169`). diff --git a/pyproject.toml b/pyproject.toml index d1d65e7..356e0a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,16 +12,16 @@ authors = [ {name = "Coastal Environmental Modelling team, Oceans and Atmosphere, CSIRO", email = "coasts@csiro.au"}, ] license = {file = "LICENSE"} -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "bottleneck >=1.3", - "geojson >=3.0", + "geojson >=3.1", "netcdf4 >=1.6.4", - "numpy >=1.24", - "packaging >=23.1", + "numpy >=1.25", + "packaging >=23.2", "shapely >=2.0", "pyshp >=2.3", - "xarray[parallel] >=2023.8", + "xarray[parallel] >=2024.1", ] dynamic = ["readme"] @@ -40,7 +40,7 @@ plot = [ ] tutorial = [ - "pooch >=1.7", + "pooch >=1.8", ] complete = [ @@ -109,7 +109,7 @@ mpl-use-full-test-name = true # mpl-baseline-path = "tests/baseline_images" [tool.mypy] -python_version = "3.10" +python_version = "3.11" plugins = ["numpy.typing.mypy_plugin"] disallow_untyped_defs = true diff --git a/scripts/min_deps_check.py b/scripts/min_deps_check.py index 86a5a79..6c2a4d5 100755 --- a/scripts/min_deps_check.py +++ b/scripts/min_deps_check.py @@ -1,21 +1,28 @@ #!/usr/bin/env python """ -Fetch from conda database all available versions of the emsarray dependencies and their -publication date. Compare it against continuous-integration/min-deps.yaml to verify the -policy on obsolete dependencies is being followed. Print a pretty report :) +Fetch from PyPI all available versions of the emsarray dependencies and their +publication date. Compare it against continuous-integration/requirements-minimum.txt +to verify the policy on obsolete dependencies is being followed. +Update the pinned dependencies using `pip-compile`. Based heavily on `min_deps_check.py` from xarray but rewritten to pull requirements from a Python requirements.txt and available versions from PyPI. Needs the following extra deps installed: - $ pip3 install packaging requests python-dateutil + $ pip3 install packaging requests python-dateutil pip-tools + +This is automatically run as part of the `scripts/update_pinned_dependencies.sh` script. See also ======== https://github.com/pydata/xarray/blob/v2024.06.0/ci/min_deps_check.py """ +import dataclasses import datetime +import enum +import itertools +import subprocess import sys from collections.abc import Iterator @@ -24,6 +31,23 @@ import requests from dateutil.relativedelta import relativedelta + +class VersionStatus(enum.StrEnum): + older = '<' + current = '~=' + newer = '>' + + +@dataclasses.dataclass +class Dependency: + package_name: str + requirements_version: packaging.version.Version + requirements_date: datetime.date | None + policy_version: packaging.version.Version + policy_date: datetime.date | None + status: VersionStatus + + IGNORE_DEPS: set[str] = { 'certifi', 'pytz', @@ -49,13 +73,12 @@ def warning(msg: str) -> None: def parse_requirements( - fname: str + filename: str ) -> Iterator[tuple[str, packaging.specifiers.Specifier, packaging.version.Version]]: - """Load requirements/min-all-deps.yml - - Yield (package name, major version, minor version, patch version) """ - for line_number, line in enumerate(open(fname), start=1): + Parse a requirements file, yield (package name, specifier, version) for each requirement. + """ + for line_number, line in enumerate(open(filename), start=1): if '#' in line: line = line[:line.index('#')] line = line.strip() @@ -71,9 +94,6 @@ def parse_requirements( continue specifier = next(iter(requirement.specifier)) - if specifier.operator != '~=': - error(f"Specificity for dependency {requirement.name} should be '~='") - version = packaging.version.parse(specifier.version) if version.micro is None: warning( @@ -110,7 +130,7 @@ def process_pkg( pkg: str, specifier: packaging.specifiers.Specifier, version: packaging.version.Version, -) -> tuple[str, str, str, str, str, str]: +) -> Dependency: """Compare package version from requirements file to available versions in conda. Return row to build pandas dataframe: @@ -148,7 +168,7 @@ def process_pkg( policy_version = packaging.version.parse(policy_specifier.version) # Find the release date of the policy version - policy_release_date = min( + policy_date = min( ( release_date for release_version, release_date @@ -158,17 +178,17 @@ def process_pkg( ) if version in policy_specifier: - status = "~=" + status = VersionStatus.current else: if version < policy_version: - status = '<' + status = VersionStatus.older warning( f"Requirement {pkg} {version} was published on {req_published:%Y-%m-%d} " f"which is older than the required {policy_months} months of support. " f"Minimum policy supported version is {pkg}{policy_specifier}." ) elif version > policy_version: - status = '> (!)' + status = VersionStatus.newer if req_published is None: error( f"Package version is newer than policy version. " @@ -186,26 +206,96 @@ def process_pkg( f"Update requirement to {pkg}{policy_specifier}." ) - return ( - pkg, - str(version), - req_published.strftime("%Y-%m-%d") if req_published else "-", - str(policy_version), - policy_release_date.strftime("%Y-%m-%d") if policy_release_date else "-", - status, + return Dependency( + package_name=pkg, + requirements_version=version, + requirements_date=req_published, + policy_version=policy_version, + policy_date=policy_date, + status=status, ) def main() -> None: + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} continuous-integration/requirements-minimum.txt") + sys.exit(1) + requirements_file = sys.argv[1] - rows = [process_pkg(pkg, specifier, version) for pkg, specifier, version in parse_requirements(requirements_file)] + dependencies = [ + process_pkg(pkg, specifier, version) + for pkg, specifier, version in parse_requirements(requirements_file) + ] print() print("Package Required Status Policy ") print("-------------------- ----------------------- ------ -----------------------") fmt = "{0:20} {1:10} ({2:10}) {5:^6} {3:10} ({4:10})" - for row in rows: - print(fmt.format(*row)) + for d in dependencies: + requirements_date = ( + d.requirements_date.strftime("%Y-%m-%d") + if d.requirements_date is not None + else "-" + ) + policy_date = ( + d.policy_date.strftime("%Y-%m-%d") + if d.policy_date is not None + else "-" + ) + print( + f"{d.package_name:20} {d.requirements_version!s:10} ({requirements_date:10}) " + f"{d.status:^6} {d.policy_version!s:10} ({policy_date:10})" + ) + + # These packages need upgrading + upgrade_args = list(itertools.chain.from_iterable( + ['--upgrade-package', f'{d.package_name}~={d.policy_version}'] + for d in dependencies + if d.status is VersionStatus.older + )) + + # These packages should be kept where they are + maintain_args = list(itertools.chain.from_iterable( + ['--upgrade-package', f'{d.package_name}~={d.requirements_version}'] + for d in dependencies + if d.status in {VersionStatus.current, VersionStatus.newer} + )) + # These packages should be installed fresh every time, so ignore them for now. + ignored_args = list(itertools.chain.from_iterable( + ['--unsafe-package', ignored] + for ignored in IGNORE_DEPS + )) + cmd = [ + 'pip-compile', + '--quiet', + '--extra', 'complete', + '--strip-extras', + '--unsafe-package', 'emsarray', + '--no-allow-unsafe', + # '--no-header', + # '--no-annotate', + '--output-file', requirements_file, + ] + upgrade_args + maintain_args + ignored_args + [ + 'pyproject.toml', + ] + + # pip-compile always prints '==' specifiers, + # but we want the latest point release in the minimum policy version. + subprocess.check_call(cmd) + cmd = [ + 'sed', + '-i', + 's/==/~=/', + requirements_file, + ] + subprocess.check_call(cmd) + + # CI will install strictly the requirements present in this file and none more, + # so append the ignored deps at the end with no version specifiers. + with open(requirements_file, "a") as f: + for ignored in IGNORE_DEPS: + f.write(ignored) + f.write("\n") if errors: print("\nErrors:") diff --git a/scripts/update_pinned_dependencies.sh b/scripts/update_pinned_dependencies.sh index 39a2b77..23a5f40 100755 --- a/scripts/update_pinned_dependencies.sh +++ b/scripts/update_pinned_dependencies.sh @@ -2,7 +2,7 @@ set -e -PYTHON_VERSIONS=('3.10' '3.11' '3.12') +PYTHON_VERSIONS=('3.11' '3.12' '3.13') HERE="$( cd -- "$( realpath -- "$( dirname -- "$0" )" )" && pwd )" PROJECT_ROOT="$( dirname "$HERE" )" @@ -11,6 +11,27 @@ cd "$PROJECT_ROOT" conda_venv_root=$( mktemp -d emsarray-conda-environments.XXXXXXX ) echo "Working in ${conda_venv_root}" +version="${PYTHON_VERSIONS[0]}" +requirements_file="./continuous-integration/requirements-minimum.txt" +echo "Updating $requirements_file" +conda_prefix="${conda_venv_root}/py-min" +conda create \ + --yes --quiet \ + --prefix="${conda_prefix}" \ + --no-default-packages +conda install \ + --yes \ + --prefix="${conda_prefix}" \ + --channel conda-forge \ + "python=${version}" pip +conda run \ + --prefix="${conda_prefix}" \ + pip install pip-tools packaging requests python-dateutil pip-tools +conda run \ + --prefix="${conda_prefix}" \ + python3 ./scripts/min_deps_check.py "$requirements_file" +conda env remove --yes --prefix="${conda_prefix}" + for version in "${PYTHON_VERSIONS[@]}" ; do requirements_file="./continuous-integration/requirements-${version}.txt" echo "Updating $requirements_file" @@ -23,6 +44,7 @@ for version in "${PYTHON_VERSIONS[@]}" ; do conda install \ --yes \ --prefix="${conda_prefix}" \ + --channel conda-forge \ "python=${version}" \ pip-tools conda run \ @@ -31,8 +53,11 @@ for version in "${PYTHON_VERSIONS[@]}" ; do --upgrade \ --extra="testing" \ --output-file="${requirements_file}" \ - setup.cfg + --unsafe-package emsarray \ + --no-allow-unsafe \ + pyproject.toml conda env remove --yes --prefix="${conda_prefix}" done + echo rm -rf "$conda_venv_root" diff --git a/src/emsarray/conventions/ugrid.py b/src/emsarray/conventions/ugrid.py index 0c1bd52..7010b7f 100644 --- a/src/emsarray/conventions/ugrid.py +++ b/src/emsarray/conventions/ugrid.py @@ -1006,7 +1006,7 @@ def edge_count(self) -> int: return self.dataset.sizes[self.edge_dimension] # By computing the edge_node array we can determine how many edges exist - return self.edge_node_array.shape[0] + return cast(int, self.edge_node_array.shape[0]) @property def face_count(self) -> int: diff --git a/src/emsarray/operations/cache.py b/src/emsarray/operations/cache.py index 4e80cf5..06bc1f0 100644 --- a/src/emsarray/operations/cache.py +++ b/src/emsarray/operations/cache.py @@ -19,6 +19,7 @@ """ import hashlib import marshal +from typing import cast import numpy import xarray @@ -146,7 +147,7 @@ def make_cache_key(dataset: xarray.Dataset, hash: "hashlib._Hash | None" = None) and should not be relied upon. """ if hash is None: - hash = hashlib.blake2b(digest_size=32) + hash = cast("hashlib._Hash", hashlib.blake2b(digest_size=32)) dataset.ems.hash_geometry(hash) diff --git a/src/emsarray/operations/geometry.py b/src/emsarray/operations/geometry.py index 2ef472d..62229e8 100644 --- a/src/emsarray/operations/geometry.py +++ b/src/emsarray/operations/geometry.py @@ -7,7 +7,7 @@ import pathlib from collections.abc import Generator, Iterable, Iterator from contextlib import contextmanager -from typing import IO, Any, TypeVar +from typing import IO, Any, Generic, TypeVar import geojson import shapefile @@ -19,7 +19,7 @@ T = TypeVar('T') -class _dumpable_iterator(list): +class _dumpable_iterator(Generic[T], list): """ Wrap an iterator / generator so it can be used in `json.dumps()`. No guarantees that it works for anything else! diff --git a/src/emsarray/operations/triangulate.py b/src/emsarray/operations/triangulate.py index 8981814..b0a2bcc 100644 --- a/src/emsarray/operations/triangulate.py +++ b/src/emsarray/operations/triangulate.py @@ -158,7 +158,7 @@ def _add_triangles(face_index: int, vertex_triangles: numpy.ndarray) -> None: vertex_triangles = _triangulate_polygons_by_length(same_length_polygons) for face_index, triangles in zip(same_length_face_indices, vertex_triangles): - _add_triangles(face_index, triangles) + _add_triangles(int(face_index), triangles) # Triangulate each concave polygon using a slower manual method. # Anecdotally concave polygons are very rare, @@ -166,7 +166,7 @@ def _add_triangles(face_index: int, vertex_triangles: numpy.ndarray) -> None: for face_index in polygon_is_concave: polygon = polygons[face_index] triangles = _triangulate_concave_polygon(polygon) - _add_triangles(face_index, triangles) + _add_triangles(int(face_index), triangles) # Check that we have handled each triangle we expected. assert current_face == total_triangles diff --git a/src/emsarray/utils.py b/src/emsarray/utils.py index 6e54c02..642c022 100644 --- a/src/emsarray/utils.py +++ b/src/emsarray/utils.py @@ -228,7 +228,7 @@ def fix_time_units_for_ems( """ with netCDF4.Dataset(dataset_path, 'r+') as dataset: - variable = dataset.variables[variable_name] + variable = dataset.variables[str(variable_name)] units = cast(str, variable.getncattr('units')) calendar = cast(str, variable.getncattr('calendar') or DEFAULT_CALENDAR) @@ -668,7 +668,11 @@ def wind_dimension( return xarray.DataArray(data=new_data, dims=new_dims) -def datetime_from_np_time(np_time: numpy.datetime64) -> datetime.datetime: +def datetime_from_np_time( + np_time: numpy.datetime64, + *, + tz: datetime.tzinfo = datetime.timezone.utc, +) -> datetime.datetime: """ Convert a numpy :class:`~numpy.datetime64` to a python :class:`~datetime.datetime`. @@ -682,8 +686,24 @@ def datetime_from_np_time(np_time: numpy.datetime64) -> datetime.datetime: A conversion that truncates data is not reported as an error. If you're using numpy datetime64 with attosecond accuracy, the Python datetime formatting methods are insufficient for your needs anyway. + + Parameters + ========== + np_time : numpy.datetime64 + The numpy datetime64 to convert to a Python datetime. + tz : datetime.tzinfo + The timezone that the numpy datetime is in. + Defaults to UTC, as xarray will convert all time variables to UTC when + opening files. + + Returns + ======= + datetime.datetime + A timezone aware Python datetime.datetime instance. """ - return datetime.datetime.fromtimestamp(np_time.item() / 10**9) + epoc = numpy.datetime64('1970-01-01') + timestamp = (np_time - epoc).astype('timedelta64[ns]') + return datetime.datetime.fromtimestamp(timestamp.astype(float) / 1e9, tz=tz) class RequiresExtraException(Exception): diff --git a/tox.ini b/tox.ini index 8dba14e..a3832d3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,9 @@ isolated_build = true package = wheel wheel_build_env = .pkg envlist = - py{310,311,312}-pytest-pinned - py310-pytest-minimum - py312-pytest-latest + py{311,312,313}-pytest-pinned + py311-pytest-minimum + py313-pytest-latest lint,docs skip_missing_interpreters = true @@ -18,12 +18,12 @@ sitepackages = false passenv = UDUNITS2_XML_PATH -[testenv:py{310,311,312}-pytest-{pinned,latest,minimum}] +[testenv:py{311,312,313}-pytest-{pinned,latest,minimum}] description = "Run the pytest test suite against a specific Python version and dependencies" deps = - py310-pinned: -rcontinuous-integration/requirements-3.10.txt py311-pinned: -rcontinuous-integration/requirements-3.11.txt py312-pinned: -rcontinuous-integration/requirements-3.12.txt + py313-pinned: -rcontinuous-integration/requirements-3.13.txt minimum: -rcontinuous-integration/requirements-minimum.txt extras = latest: testing