diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27b98a5..efab206 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,21 @@ jobs: - name: QC run: pixi run qc + test-unit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: prefix-dev/setup-pixi@v0.9.0 + with: + cache: true + + - name: Unit Tests + run: pixi run test-unit + test-fast: runs-on: ubuntu-latest + needs: test-unit steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index be6b618..4fe843b 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,5 @@ cython_debug/ # pixi environments .pixi *.egg-info + +.envrc diff --git a/pixi.lock b/pixi.lock index 3ac0ff7..7d781b3 100644 --- a/pixi.lock +++ b/pixi.lock @@ -10,81 +10,83 @@ environments: pypi-prerelease-mode: if-necessary-or-explicit 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/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45-h9d8b0ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.45-h4852527_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314hdfeb8a1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.1.3-hc85cc9f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45.1-default_hfdba357_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.45.1-default_h4852527_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.3-hc85cc9f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-hcacfade_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.2.0-h862fb80_13.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-he420e7e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.2.0-h862fb80_21.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.53.0-pl5321h6d3cee1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.46-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_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/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.19.0-hcf29cc6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-h73f6952_107.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_118.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.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/liblzma-5.8.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.2.0-hb13aed2_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.2.0-h90f66d4_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.2.0-hd446a21_118.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.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/noarch/m2r2-0.3.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-0.8.4-pyh1a96a4e_1006.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.6.0-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/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pkg-config-0.29.2-h4bc722e_1009.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgconfig-1.5.5-pyhd8ed1ab_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgconfig-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_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/linux-64/python-3.14.0-h32b2ec7_102_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.3-h32b2ec7_101_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ratelimit-2.2.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda @@ -94,21 +96,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_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.25.0-py314h0f05182_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/55/9a/16082d513509848e5b9165f9d578606419c87d965fcd2ce0c0ea6c0e53d5/mailbits-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/0e/758dc5f520eaafbb360973f60d3b7f74e17a2ff8e5de6998fb55b952b532/mailbits-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/ad/5ce88458a6d01147d600d72bb55f5086ff3ffa5f4085c6d3e37e91f4d591/pypi_simple-1.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/e9/ca6a7178677bb171c8fd7f9eab726c00182168d38a79d2a58e7678590227/sphinxawesome_theme-5.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl style: @@ -156,13 +158,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda 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-20_gnu.conda build_number: 20 sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 @@ -177,20 +172,6 @@ packages: purls: [] size: 28948 timestamp: 1770939786096 -- 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/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda sha256: 6c4456a138919dae9edd3ac1a74b6fbe5fd66c05675f54df2f8ab8c8d0cc6cea md5: 1fd9696649f65fd6611fcdb4ffec738a @@ -214,24 +195,36 @@ packages: version: 25.4.0 sha256: adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - sha256: 1c656a35800b7f57f7371605bc6507c8d3ad60fbaaec65876fce7f73df1fc8ac - md5: 0a01c169f0ab0f91b26e77a3301fbfe4 +- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 + md5: f1976ce927373500cc19d3c0b2c85177 depends: - - python >=3.9 + - python >=3.10 + - python + constrains: - pytz >=2015.7 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/babel?source=hash-mapping - size: 6938256 - timestamp: 1738490268466 -- pypi: https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl + - pkg:pypi/babel?source=compressed-mapping + size: 7684321 + timestamp: 1772555330347 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + noarch: generic + sha256: c31ab719d256bc6f89926131e88ecd0f0c5d003fe8481852c6424f4ec6c7eb29 + md5: a2ac7763a9ac75055b68f325d3255265 + depends: + - python >=3.14 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: [] + size: 7514 + timestamp: 1767044983590 +- pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl name: beautifulsoup4 - version: 4.14.2 - sha256: 5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 + version: 4.14.3 + sha256: 0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb requires_dist: - - soupsieve>1.2 + - soupsieve>=1.6.1 - typing-extensions>=4.0.0 - cchardet ; extra == 'cchardet' - chardet ; extra == 'chardet' @@ -239,31 +232,31 @@ packages: - html5lib ; extra == 'html5lib' - lxml ; extra == 'lxml' requires_python: '>=3.7.0' -- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45-h9d8b0ac_0.conda - sha256: 1733bd616f0e7afdc926e4eb80b00483ebdc51bc6aadf7c4b7242ed93044e25b - md5: 0f846eecce9004022f9706252b143b0f +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45.1-default_hfdba357_101.conda + sha256: 74341b26a2b9475dc14ba3cf12432fcd10a23af285101883e720216d81d44676 + md5: 83aa53cb3f5fc849851a84d777a60551 depends: - - ld_impl_linux-64 2.45 h1aa0949_0 + - ld_impl_linux-64 2.45.1 default_hbd61a6d_101 - sysroot_linux-64 - zstd >=1.5.7,<1.6.0a0 license: GPL-3.0-only license_family: GPL purls: [] - size: 3781434 - timestamp: 1763060453906 -- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.45-h4852527_0.conda - sha256: ffcb163c3f3b87d6aaa2608ff6027fe21bdcf9844f204112c7ef0f9eb65a848e - md5: 01c3e6f0d3266733368ca1b64f1415d8 + size: 3744895 + timestamp: 1770267152681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.45.1-default_h4852527_101.conda + sha256: 4826f97d33cbe54459970a1e84500dbe0cccf8326aaf370e707372ae20ec5a47 + md5: dec96579f9a7035a59492bf6ee613b53 depends: - - binutils_impl_linux-64 2.45 h9d8b0ac_0 + - binutils_impl_linux-64 2.45.1 default_hfdba357_101 license: GPL-3.0-only license_family: GPL purls: [] - size: 36086 - timestamp: 1763060480570 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314hdfeb8a1_0.conda - sha256: 9f6d339fb78b647be35e3564dac453d8d2f1b865ba72fb961eaac41061368699 - md5: 3ef9d2a701760467b9db2338b6cd926f + size: 36060 + timestamp: 1770267177798 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 + md5: 8910d2c46f7e7b519129f486e0fe927a depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 @@ -271,24 +264,13 @@ packages: - python >=3.14,<3.15.0a0 - python_abi 3.14.* *_cp314 constrains: - - libbrotlicommon 1.2.0 h09219d5_0 + - libbrotlicommon 1.2.0 hb03c661_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 368319 - timestamp: 1761592337171 -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 - md5: 51a19bba1b8ebfb60df25cde030b7ebc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 260341 - timestamp: 1757437258798 + size: 367376 + timestamp: 1764017265553 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 md5: d2ffd7602c02f2b316fd921d39876885 @@ -300,26 +282,17 @@ packages: purls: [] size: 260182 timestamp: 1771350215188 -- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - sha256: f8003bef369f57396593ccd03d08a8e21966157269426f71e943f96e4b579aeb - md5: f7f0d6cc2dc986d42ac2689ec88192be +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 206884 - timestamp: 1744127994291 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - sha256: b986ba796d42c9d3265602bc038f6f5264095702dd546c14bc684e60c385e773 - md5: f0991f0f84902f6b6009b4d2350a83aa - depends: - - __unix - license: ISC - purls: [] - size: 152432 - timestamp: 1762967197890 + size: 207882 + timestamp: 1765214722852 - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc md5: 4492fd26db29495f0ba23f146cd5638d @@ -329,53 +302,37 @@ packages: purls: [] size: 147413 timestamp: 1772006283803 -- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda - sha256: 083a2bdad892ccf02b352ecab38ee86c3e610ba9a4b11b073ea769d55a115d32 - md5: 96a02a5c1a65470a7e4eedb644c872fd +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_0.conda + sha256: a6b118fd1ed6099dc4fc03f9c492b88882a780fadaef4ed4f93dc70757713656 + md5: 765c4d97e877cdbbb88ff33152b86125 depends: - python >=3.10 license: ISC purls: - pkg:pypi/certifi?source=compressed-mapping - size: 157131 - timestamp: 1762976260320 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda - sha256: c6339858a0aaf5d939e00d345c98b99e4558f285942b27232ac098ad17ac7f8e - md5: cf45f4278afd6f4e6d03eda0f435d527 - depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - pycparser - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 300271 - timestamp: 1761203085220 -- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda - sha256: b32f8362e885f1b8417bac2b3da4db7323faa12d5db62b7fd6691c02d60d6f59 - md5: a22d1fd9bf98827e280a02875d9a007a + size: 151445 + timestamp: 1772001170301 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda + sha256: 05ea76a016c77839b64f9f8ec581775f6c8a259044bd5b45a177e46ab4e7feac + md5: beb628209b2b354b98203066f90b3287 depends: - python >=3.10 license: MIT license_family: MIT purls: - - pkg:pypi/charset-normalizer?source=hash-mapping - size: 50965 - timestamp: 1760437331772 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.1.3-hc85cc9f_0.conda - sha256: 5b47d55df169ef390ba21e2c20385473b51cade9a3248edd0d27c550aeb2dadc - md5: 21eca3ff0e576568afbce5409a92a0f8 + - pkg:pypi/charset-normalizer?source=compressed-mapping + size: 53210 + timestamp: 1772816516728 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.2.3-hc85cc9f_1.conda + sha256: 5ece78754577b8d9030ec1f09ce1cd481125f27d8d6fcdcfe2c1017661830c61 + md5: 51d37989c1758b5edfe98518088bf700 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.17.0,<9.0a0 - - libexpat >=2.7.1,<3.0a0 + - libcurl >=8.18.0,<9.0a0 + - libexpat >=2.7.4,<3.0a0 - libgcc >=14 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libstdcxx >=14 - libuv >=1.51.0,<2.0a0 - libzlib >=1.3.1,<2.0a0 @@ -385,8 +342,8 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] - size: 21260270 - timestamp: 1763512297910 + size: 22330508 + timestamp: 1771383666798 - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 md5: 962b9857ee8e7018c22f2776ffa0b2d7 @@ -408,6 +365,17 @@ packages: - pkg:pypi/docutils?source=hash-mapping size: 402700 timestamp: 1733217860944 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 - conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.3.0-hdab8a38_0.conda sha256: 55d3011ca72e1d97acc651b2af5d4d4d785988a8cfa9026205e9cf11f2d4ee67 md5: 1b8aaa7bb23496abb0e23369db7fb5b7 @@ -418,28 +386,28 @@ packages: - __glibc >=2.17 license: MIT license_family: MIT - purls: [] size: 1209421 timestamp: 1757336717570 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-hcacfade_7.conda - sha256: 6a19411e3fe4e4f55509f4b0c374663b3f8903ed5ae1cc94be1b88846c50c269 - md5: 3d75679d5e2bd547cb52b913d73f69ef +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-he420e7e_18.conda + sha256: a088cfd3ae6fa83815faa8703bc9d21cc915f17bd1b51aac9c16ddf678da21e4 + md5: cf56b6d74f580b91fd527e10d9a2e324 depends: - - binutils_impl_linux-64 >=2.40 + - binutils_impl_linux-64 >=2.45 - libgcc >=15.2.0 - - libgcc-devel_linux-64 15.2.0 h73f6952_107 + - libgcc-devel_linux-64 15.2.0 hcc6f6b0_118 - libgomp >=15.2.0 - - libsanitizer 15.2.0 hb13aed2_7 + - libsanitizer 15.2.0 h90f66d4_18 - libstdcxx >=15.2.0 + - libstdcxx-devel_linux-64 15.2.0 hd446a21_118 - sysroot_linux-64 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 77766660 - timestamp: 1759968214246 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.2.0-h862fb80_13.conda - sha256: 15f8e927985e820a2b236cdb26421a01b6fbaa101dc0d787e00317498a9d17d6 - md5: 8388a8efed23bccf6a97f0a3788ccb0d + size: 81814135 + timestamp: 1771378369317 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.2.0-h862fb80_21.conda + sha256: de02364d7eb088d8fa9c91ddafc572a359f49070388b1981541064ecf75ea0b8 + md5: be589fb742197599a6275c5e69df1c43 depends: - gcc_impl_linux-64 15.2.0.* - binutils_linux-64 @@ -447,8 +415,8 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] - size: 27972 - timestamp: 1763064850441 + size: 28948 + timestamp: 1770908206658 - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.53.0-pl5321h6d3cee1_0.conda sha256: 33b20cf09ff1c6ca960e6c5f7fad1f08ffd3112a87d79e42ed56f4e1b4cdefe3 md5: ad8d4260a6dae5f55960b26b237d576b @@ -502,7 +470,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/h2?source=compressed-mapping + - pkg:pypi/h2?source=hash-mapping size: 95967 timestamp: 1756364871835 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda @@ -527,18 +495,6 @@ packages: - pkg:pypi/hyperframe?source=hash-mapping size: 17397 timestamp: 1737618427549 -- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e - md5: 8b189310083baabfb622af68fd9d3ae3 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: MIT - license_family: MIT - purls: [] - size: 12129203 - timestamp: 1720853576813 - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda sha256: 142a722072fa96cf16ff98eaaf641f54ab84744af81754c292cb81e0881c0329 md5: 186a18e3ba246eccfc7cff00cd19a870 @@ -559,7 +515,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/idna?source=compressed-mapping + - pkg:pypi/idna?source=hash-mapping size: 50721 timestamp: 1760286526795 - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 @@ -582,32 +538,42 @@ packages: - python license: Apache-2.0 license_family: APACHE - purls: - - pkg:pypi/importlib-metadata?source=hash-mapping size: 34641 timestamp: 1747934053147 -- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af - md5: 446bd6c8cb26050d528881df495ce646 +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=compressed-mapping + size: 13387 + timestamp: 1760831448842 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d depends: - markupsafe >=2.0 - - python >=3.9 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jinja2?source=hash-mapping - size: 112714 - timestamp: 1741263433881 -- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda - sha256: 305c22a251db227679343fd73bfde121e555d466af86e537847f4c8b9436be0d - md5: ff007ab0f0fdc53d245972bba8a6d40c + - pkg:pypi/jinja2?source=compressed-mapping + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + sha256: 41557eeadf641de6aeae49486cef30d02a6912d8da98585d687894afd65b356a + md5: 86d9cba083cd041bfbf242a01a7a1999 constrains: - sysroot_linux-64 ==2.28 license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later license_family: GPL purls: [] - size: 1272697 - timestamp: 1752669126073 + size: 1278712 + timestamp: 1765578681495 - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 md5: b38117a3c920364aff79f870c984b4a3 @@ -634,19 +600,6 @@ packages: purls: [] size: 1386730 timestamp: 1769769569681 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda - sha256: 32321d38b8785ef8ddcfef652ee370acee8d944681014d47797a18637ff16854 - md5: 1450224b3e7d17dfeb985364b77a4d47 - depends: - - __glibc >=2.17,<3.0.a0 - - zstd >=1.5.7,<1.6.0a0 - constrains: - - binutils_impl_linux-64 2.45 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 753744 - timestamp: 1763060439129 - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda sha256: 565941ac1f8b0d2f2e8f02827cbca648f4d18cd461afc31f15604cd291b5c5f3 md5: 12bd9a3f089ee6c9266a37dab82afabd @@ -660,9 +613,9 @@ packages: purls: [] size: 725507 timestamp: 1770267139900 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda - sha256: c84e8dccb65ad5149c0121e4b54bdc47fa39303fd5f4979b8c44bb51b39a369b - md5: 1707cdd636af2ff697b53186572c9f77 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.19.0-hcf29cc6_0.conda + sha256: a0390fd0536ebcd2244e243f5f00ab8e76ab62ed9aa214cd54470fe7496620f4 + md5: d50608c443a30c341c24277d28290f76 depends: - __glibc >=2.17,<3.0.a0 - krb5 >=1.22.2,<1.23.0a0 @@ -675,8 +628,8 @@ packages: license: curl license_family: MIT purls: [] - size: 463621 - timestamp: 1770892808818 + size: 466704 + timestamp: 1773218522665 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 md5: c277e0a4d549b03ac1e9d6cbbe3d017b @@ -724,31 +677,6 @@ packages: purls: [] size: 58592 timestamp: 1769456073053 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - sha256: 25cbdfa65580cfab1b8d15ee90b4c9f1e0d72128f1661449c9a999d341377d54 - md5: 35f29eec58405aaf55e01cb470d8c26a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: MIT - license_family: MIT - purls: [] - size: 57821 - timestamp: 1760295480630 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda - sha256: 08f9b87578ab981c7713e4e6a7d935e40766e10691732bba376d4964562bcb45 - md5: c0374badb3a5d4b1372db28d19462c53 - depends: - - __glibc >=2.17,<3.0.a0 - - _openmp_mutex >=4.5 - constrains: - - libgomp 15.2.0 h767d61c_7 - - libgcc-ng ==15.2.0=*_7 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 822552 - timestamp: 1759968052178 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 md5: 0aa00f03f9e39fb9876085dee11a85d4 @@ -763,36 +691,26 @@ packages: purls: [] size: 1041788 timestamp: 1771378212382 -- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-h73f6952_107.conda - sha256: 67323768cddb87e744d0e593f92445cd10005e04259acd3e948c7ba3bcb03aed - md5: 85fce551e54a1e81b69f9ffb3ade6aee +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_118.conda + sha256: af69fc5852908d26e5b630b270982ac792506551dd6af1614bf0370dd5ab5746 + md5: 5d3a96d55f1be45fef88ee23155effd9 depends: - __unix license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 2728965 - timestamp: 1759967882886 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda - sha256: 2045066dd8e6e58aaf5ae2b722fb6dfdbb57c862b5f34ac7bfb58c40ef39b6ad - md5: 280ea6eee9e2ddefde25ff799c4f0363 + size: 3085932 + timestamp: 1771378098166 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + sha256: e318a711400f536c81123e753d4c797a821021fb38970cebfb3f454126016893 + md5: d5e96b1ed75ca01906b3d2469b4ce493 depends: - - libgcc 15.2.0 h767d61c_7 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 29313 - timestamp: 1759968065504 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda - sha256: e9fb1c258c8e66ee278397b5822692527c5f5786d372fe7a869b900853f3f5ca - md5: f7b4d76975aac7e5d9e6ad13845f92fe - depends: - - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 447919 - timestamp: 1759967942498 + size: 27526 + timestamp: 1771378224552 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 md5: 239c5e9546c38a1e884d69effcf4c882 @@ -813,18 +731,6 @@ packages: purls: [] size: 790176 timestamp: 1754908768807 -- 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/liblzma-5.8.2-hb03c661_0.conda sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb md5: c7c83eecbb72d88b940c249af56c8b17 @@ -848,17 +754,6 @@ packages: purls: [] size: 92400 timestamp: 1769482286018 -- 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/libnghttp2-1.67.0-had1ee68_0.conda sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 md5: b499ce4b026493a13774bcf0f4c33849 @@ -876,9 +771,9 @@ packages: purls: [] size: 666600 timestamp: 1756834976695 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.2.0-hb13aed2_7.conda - sha256: 4d15a66e136fba55bc0e83583de603f46e972f3486e2689628dfd9729a5c3d78 - md5: 4ea6053660330c1bbd4635b945f7626d +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.2.0-h90f66d4_18.conda + sha256: 0329e23d54a567c259adc962a62172eaa55e6ca33c105ef67b4f3cdb4ef70eaa + md5: ff754fbe790d4e70cf38aea3668c3cb3 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15.2.0 @@ -886,20 +781,8 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 5133768 - timestamp: 1759968130105 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda - sha256: 4c992dcd0e34b68f843e75406f7f303b1b97c248d18f3c7c330bdc0bc26ae0b3 - md5: 729a572a3ebb8c43933b30edcc628ceb - depends: - - __glibc >=2.17,<3.0.a0 - - icu >=75.1,<76.0a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - license: blessing - purls: [] - size: 945576 - timestamp: 1762299687230 + size: 8095113 + timestamp: 1771378289674 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda sha256: d716847b7deca293d2e49ed1c8ab9e4b9e04b9d780aea49a97c26925b28a7993 md5: fd893f6a3002a635b5e50ceb9dd2c0f4 @@ -925,19 +808,6 @@ packages: purls: [] size: 304790 timestamp: 1745608545575 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda - sha256: 1b981647d9775e1cdeb2fab0a4dd9cd75a6b0de2963f6c3953dbd712f78334b3 - md5: 5b767048b1b3ee9a954b06f4084f93dc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc 15.2.0 h767d61c_7 - constrains: - - libstdcxx-ng ==15.2.0=*_7 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 3898269 - timestamp: 1759968103436 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e md5: 1b08cd684f34175e4514474793d44bcb @@ -951,27 +821,16 @@ packages: purls: [] size: 5852330 timestamp: 1771378262446 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda - sha256: 024fd46ac3ea8032a5ec3ea7b91c4c235701a8bf0e6520fe5e6539992a6bd05f - md5: f627678cf829bd70bccf141a19c3ad3e +- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.2.0-hd446a21_118.conda + sha256: 138ee40ba770abf4556ee9981879da9e33299f406a450831b48c1c397d7d0833 + md5: a50630d1810916fc252b2152f1dc9d6d depends: - - libstdcxx 15.2.0 h8f9b012_7 + - __unix license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 29343 - timestamp: 1759968157195 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda - sha256: e5ec6d2ad7eef538ddcb9ea62ad4346fde70a4736342c4ad87bd713641eb9808 - md5: 80c07c68d2f6870250959dcc95b209d1 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 37135 - timestamp: 1758626800002 + size: 20669511 + timestamp: 1771378139786 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee md5: db409b7c1720428638e7c0d509d3e1b5 @@ -1029,14 +888,13 @@ packages: - pkg:pypi/m2r2?source=hash-mapping size: 17361 timestamp: 1746266852756 -- pypi: https://files.pythonhosted.org/packages/55/9a/16082d513509848e5b9165f9d578606419c87d965fcd2ce0c0ea6c0e53d5/mailbits-0.2.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/47/0e/758dc5f520eaafbb360973f60d3b7f74e17a2ff8e5de6998fb55b952b532/mailbits-0.2.3-py3-none-any.whl name: mailbits - version: 0.2.2 - sha256: 9ddbfc65d7d7fc0a09b82a123cb480f21aa38b3f7ae58bf71a81b4399b3217d5 + version: 0.2.3 + sha256: 75082106ed1b9a19fdd86f73aee268b3662a42df5aa024a06018b1d1bedac7dc requires_dist: - attrs>=18.1 - - typing-extensions ; python_full_version < '3.8' - requires_python: '>=3.8' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda sha256: 0fbacdfb31e55964152b24d5567e9a9996e1e7902fb08eb7d91b5fd6ce60803a md5: fee3164ac23dfca50cfcc8b85ddefb81 @@ -1045,25 +903,24 @@ packages: - python >=3.9 license: MIT license_family: MIT - purls: - - pkg:pypi/markdown-it-py?source=hash-mapping size: 64430 timestamp: 1733250550053 -- conda: https://conda.anaconda.org/conda-forge/noarch/markupsafe-3.0.3-pyh7db6752_0.conda - sha256: e0cbfea51a19b3055ca19428bd9233a25adca956c208abb9d00b21e7259c7e03 - md5: fab1be106a50e20f10fe5228fd1d1651 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf + md5: 9a17c4307d23318476d7fbf0fedc0cde depends: - - python >=3.10 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 constrains: - jinja2 >=3.0.0 - track_features: - - markupsafe_no_compile license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 15499 - timestamp: 1759055275624 + - pkg:pypi/markupsafe?source=compressed-mapping + size: 27424 + timestamp: 1772445227915 - conda: https://conda.anaconda.org/conda-forge/noarch/mdformat-0.7.22-pyhd8ed1ab_0.conda sha256: b27fc9ea628794500403f05bf245ba1ec355a9dba88b320bd8d0a54b0ea8e321 md5: a8901729d5dd45565b737ddfe83fa312 @@ -1074,8 +931,6 @@ packages: - tomli >=1.1.0 license: MIT license_family: MIT - purls: - - pkg:pypi/mdformat?source=hash-mapping size: 33289 timestamp: 1738305715383 - conda: https://conda.anaconda.org/conda-forge/noarch/mdformat-gfm-1.0.0-pyhd8ed1ab_0.conda @@ -1089,8 +944,6 @@ packages: - python >=3.10 license: MIT license_family: MIT - purls: - - pkg:pypi/mdformat-gfm?source=hash-mapping size: 16247 timestamp: 1760614559965 - conda: https://conda.anaconda.org/conda-forge/noarch/mdformat-tables-1.0.0-pyhd8ed1ab_1.conda @@ -1102,8 +955,6 @@ packages: - wcwidth >=0.2.13 license: MIT license_family: MIT - purls: - - pkg:pypi/mdformat-tables?source=hash-mapping size: 11047 timestamp: 1735313983357 - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda @@ -1114,8 +965,6 @@ packages: - python >=3.10 license: MIT license_family: MIT - purls: - - pkg:pypi/mdit-py-plugins?source=hash-mapping size: 43805 timestamp: 1754946862113 - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda @@ -1125,8 +974,6 @@ packages: - python >=3.9 license: MIT license_family: MIT - purls: - - pkg:pypi/mdurl?source=hash-mapping size: 14465 timestamp: 1733255681319 - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-0.8.4-pyh1a96a4e_1006.conda @@ -1150,18 +997,6 @@ packages: purls: [] size: 891641 timestamp: 1738195959188 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - sha256: a47271202f4518a484956968335b2521409c8173e123ab381e775c358c67fe6d - md5: 9ee58d5c534af06558933af3c845a780 - depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=14 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 3165399 - timestamp: 1762839186699 - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c md5: f61eb8cd60ff9057122a3d338b99c00f @@ -1174,18 +1009,18 @@ packages: purls: [] size: 3164551 timestamp: 1769555830639 -- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - sha256: 289861ed0c13a15d7bbb408796af4de72c2fe67e2bcb0de98f4c3fce259d7991 - md5: 58335b26c38bf4a20f399384c33cbcf9 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + sha256: c1fc0f953048f743385d31c468b4a678b3ad20caffdeaa94bed85ba63049fd58 + md5: b76541e68fea4d511b1ac46a28dcd2c6 depends: - python >=3.8 - python license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/packaging?source=hash-mapping - size: 62477 - timestamp: 1745345660407 + - pkg:pypi/packaging?source=compressed-mapping + size: 72010 + timestamp: 1769093650580 - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda sha256: 5e6f7d161356fefd981948bea5139c5aa0436767751a6930cb1ca801ebb113ff md5: 7a3bff861a6583f1889021facefc08b1 @@ -1221,34 +1056,34 @@ packages: purls: [] size: 115175 timestamp: 1720805894943 -- conda: https://conda.anaconda.org/conda-forge/noarch/pkgconfig-1.5.5-pyhd8ed1ab_5.conda - sha256: 0e57ac1dda7ddb6c2064f3ab4a0f8cdc1f285e1b09c61151ffdd698ff9e4469f - md5: 1168e78fa9d400015d9497a87ac653bf +- conda: https://conda.anaconda.org/conda-forge/noarch/pkgconfig-1.6.0-pyhd8ed1ab_0.conda + sha256: f98f3daf9c36330334cf323e2d012143d53fa3c9c4ec92745f69b1fb02383107 + md5: 8c76df85639f9c631a6556b591e0526a depends: - pkg-config - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - - pkg:pypi/pkgconfig?source=hash-mapping - size: 11812 - timestamp: 1733230819679 -- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 - md5: 12c566707c80111f9799308d9e265aef + - pkg:pypi/pkgconfig?source=compressed-mapping + size: 13066 + timestamp: 1772824753547 +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e depends: - python >=3.9 - python - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: - - pkg:pypi/pycparser?source=hash-mapping - size: 110100 - timestamp: 1733195786147 -- pypi: https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl + - pkg:pypi/pluggy?source=compressed-mapping + size: 25877 + timestamp: 1764896838868 +- pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl name: pydantic - version: 2.12.4 - sha256: 92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e + version: 2.12.5 + sha256: e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d requires_dist: - annotated-types>=0.6.0 - pydantic-core==2.41.5 @@ -1299,34 +1134,27 @@ packages: - pkg:pypi/pysocks?source=hash-mapping size: 21085 timestamp: 1733217331982 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.0-h32b2ec7_102_cp314.conda - build_number: 102 - sha256: 76d750045b94fded676323bfd01975a26a474023635735773d0e4d80aaa72518 - md5: 0a19d2cc6eb15881889b0c6fa7d6a78d +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhcf101f3_1.conda + sha256: 39f41a52eb6f927caf5cd42a2ff98a09bb850ce9758b432869374b6253826962 + md5: da0c42269086f5170e2b296878ec13a6 depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.1,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - liblzma >=5.8.1,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.50.4,<4.0a0 - - libuuid >=2.41.2,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.4,<4.0a0 - - python_abi 3.14.* *_cp314 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - zstd >=1.5.7,<1.6.0a0 - license: Python-2.0 - purls: [] - size: 36681389 - timestamp: 1761176838143 - python_site_packages_path: lib/python3.14/site-packages + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1 + - packaging >=20 + - pluggy >=1.5,<2 + - tomli >=1 + - colorama >=0.4 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=hash-mapping + size: 294852 + timestamp: 1762354779909 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.3-h32b2ec7_101_cp314.conda build_number: 101 sha256: cb0628c5f1732f889f53a877484da98f5a0e0f47326622671396fb4f2b0cd6bd @@ -1366,17 +1194,6 @@ packages: purls: [] size: 6989 timestamp: 1752805904792 -- 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/noarch/ratelimit-2.2.1-pyhd8ed1ab_0.tar.bz2 sha256: 101e2a376e7b8cdff4a8e84e922a47a2ed4c8469a43a20d4e19b3f4e9a29f36f md5: 3e8565b8c77205fc638ee40f5d2a2dba @@ -1388,17 +1205,6 @@ packages: - pkg:pypi/ratelimit?source=hash-mapping size: 9031 timestamp: 1611101670701 -- 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/linux-64/readline-8.3-h853b02a_0.conda sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 md5: d7d95fc8287ea7bf33e0e7116d2b95ec @@ -1440,16 +1246,27 @@ packages: purls: [] size: 193775 timestamp: 1748644872902 -- conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - sha256: 0116a9ca9bf3487e18979b58b2f280116dba55cb53475af7a6d835f7aa133db8 - md5: 5f0f24f8032c2c1bb33f59b75974f5fc +- conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + sha256: 30f3c04fcfb64c44d821d392a4a0b8915650dbd900c8befc20ade8fde8ec6aa2 + md5: 0dc48b4b570931adc8641e55c6c17fe4 depends: - - python >=3.9 + - python >=3.10 + license: 0BSD OR CC0-1.0 + purls: + - pkg:pypi/roman-numerals?source=hash-mapping + size: 13814 + timestamp: 1766003022813 +- conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-4.1.0-pyhd8ed1ab_0.conda + sha256: ce21b50a412b87b388db9e8dfbf8eb16fc436c23750b29bf612ee1a74dd0beb2 + md5: 28687768633154993d521aecfa4a56ac + depends: + - python >=3.10 + - roman-numerals 4.1.0 license: 0BSD OR CC0-1.0 purls: - pkg:pypi/roman-numerals-py?source=hash-mapping - size: 13348 - timestamp: 1740240332327 + size: 11074 + timestamp: 1766025162370 - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.12-h718f522_0.conda noarch: python sha256: 76861e67f7b57303ec61b2a1569e83a972b253bd5c094ce40dcc4761a70e223f @@ -1462,8 +1279,6 @@ packages: - __glibc >=2.17 license: MIT license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping size: 10836594 timestamp: 1757055819993 - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda @@ -1488,10 +1303,10 @@ packages: - pkg:pypi/snowballstemmer?source=hash-mapping size: 73009 timestamp: 1747749529809 -- pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl name: soupsieve - version: '2.8' - sha256: 0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c + version: 2.8.3 + sha256: ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95 requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda sha256: 995f58c662db0197d681fa345522fd9e7ac5f05330d3dff095ab2f102e260ab0 @@ -1602,18 +1417,18 @@ packages: - pkg:pypi/sphinxcontrib-serializinghtml?source=hash-mapping size: 28669 timestamp: 1733750596111 -- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - sha256: 0053c17ffbd9f8af1a7f864995d70121c292e317804120be4667f37c92805426 - md5: 1bad93f0aa428d618875ef3a588a889e +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + sha256: c47299fe37aebb0fcf674b3be588e67e4afb86225be4b0d452c7eb75c086b851 + md5: 13dc3adbc692664cd3beabd216434749 depends: - __glibc >=2.28 - - kernel-headers_linux-64 4.18.0 he073ed8_8 + - kernel-headers_linux-64 4.18.0 he073ed8_9 - tzdata license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later license_family: GPL purls: [] - size: 24210909 - timestamp: 1752669140965 + size: 24008591 + timestamp: 1765578833462 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac md5: cffd3bdd58090148f4cfcd831f4b26ab @@ -1628,20 +1443,6 @@ packages: purls: [] size: 3301196 timestamp: 1769460227866 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - sha256: 1544760538a40bcd8ace2b1d8ebe3eb5807ac268641f8acdc18c69c5ebfeaf64 - md5: 86bc20552bf46075e3d92b67f089172d - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - constrains: - - xorg-libx11 >=1.8.12,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3284905 - timestamp: 1763054914403 - conda: https://conda.anaconda.org/conda-forge/linux-64/tombi-0.9.4-hb17b654_0.conda sha256: 2a0fd77982d9191b1992da4d8c0aaec53b947dc0a3102fdd34248c25721947c5 md5: 8a14bfd771500accd3db17772c401ddb @@ -1685,13 +1486,6 @@ packages: - pkg:pypi/typing-extensions?source=hash-mapping size: 51692 timestamp: 1756220668932 -- 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/tzdata-2025c-hc9c84f9_1.conda sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c md5: ad659d0a2b3e47e38d829aa8cad2d610 @@ -1699,21 +1493,21 @@ packages: purls: [] size: 119135 timestamp: 1767016325805 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 - md5: 436c165519e140cb08d246a4472a9d6a +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + sha256: af641ca7ab0c64525a96fd9ad3081b0f5bcf5d1cbb091afb3f6ed5a9eee6111a + md5: 9272daa869e03efe68833e3dc7a02130 depends: - - brotli-python >=1.0.9 + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 - h2 >=4,<5 - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.9 - - zstandard >=0.18.0 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/urllib3?source=hash-mapping - size: 101735 - timestamp: 1750271478254 + size: 103172 + timestamp: 1767817860341 - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda sha256: e298b508b2473c4227206800dfb14c39e4b14fd79d4636132e9e1e4244cdf4aa md5: c3197f8c0d5b955c904616b716aca093 @@ -1721,8 +1515,6 @@ packages: - python >=3.10 license: MIT license_family: MIT - purls: - - pkg:pypi/wcwidth?source=compressed-mapping size: 71550 timestamp: 1770634638503 - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda @@ -1733,27 +1525,8 @@ packages: - python license: MIT license_family: MIT - purls: - - pkg:pypi/zipp?source=hash-mapping size: 24194 timestamp: 1764460141901 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py314h0f05182_1.conda - sha256: e589f694b44084f2e04928cabd5dda46f20544a512be2bdb0d067d498e4ac8d0 - md5: 2930a6e1c7b3bc5f66172e324a8f5fc3 - depends: - - python - - cffi >=1.11 - - zstd >=1.5.7,<1.5.8.0a0 - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - zstd >=1.5.7,<1.6.0a0 - - python_abi 3.14.* *_cp314 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 473605 - timestamp: 1762512687493 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 @@ -1765,16 +1538,3 @@ packages: purls: [] size: 601375 timestamp: 1764777111296 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb - md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 567578 - timestamp: 1742433379869 diff --git a/pixi.toml b/pixi.toml index 5a36ef9..22f3a63 100644 --- a/pixi.toml +++ b/pixi.toml @@ -6,6 +6,7 @@ platforms = ["linux-64"] version = "0.1.0" [tasks] +test-unit = "pytest source/test_collect_plugins.py -v" build = "sphinx-build source build" apply-qc = [{ task = "style", environment = "style" }] qc = [{ task = "lint", environment = "style" }] @@ -30,6 +31,7 @@ gcc_linux-64 = ">=15.1.0,<16" pkgconfig = ">=1.5.5,<2" gitpython = ">=3.1.46,<4" git = ">=2.53.0,<3" +pytest = ">=8.0.0,<9" [pypi-dependencies] sphinxawesome-theme = ">=5.3.2, <6" diff --git a/source/_static/custom.css b/source/_static/custom.css index fbe1170..fd78cd5 100644 --- a/source/_static/custom.css +++ b/source/_static/custom.css @@ -1,9 +1,11 @@ .image-reference img { display: inline; + margin-right: 4px; } .image-reference { border-bottom: none !important; + font-size: 0; } table code.literal { diff --git a/source/_templates/executor_plugin.rst.j2 b/source/_templates/executor_plugin.rst.j2 index 321bc4d..ee8ace1 100644 --- a/source/_templates/executor_plugin.rst.j2 +++ b/source/_templates/executor_plugin.rst.j2 @@ -3,7 +3,7 @@ {% block type %}executor{% endblock %} {% block usage %} -In order to use the plugin, run Snakemake (>=8.0) in the folder where your workflow code and config resides (containing either ``workflow/Snakefile`` or ``Snakefile``) with the corresponding value for the executor flag:: +In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=8.0" }}) in the folder where your workflow code and config resides (containing either ``workflow/Snakefile`` or ``Snakefile``) with the corresponding value for the executor flag:: snakemake --executor {{ plugin_name }} --default-resources --jobs N ... diff --git a/source/_templates/logger_plugin.rst.j2 b/source/_templates/logger_plugin.rst.j2 index 1ffbf57..402797f 100644 --- a/source/_templates/logger_plugin.rst.j2 +++ b/source/_templates/logger_plugin.rst.j2 @@ -3,9 +3,9 @@ {% block type %}logger{% endblock %} {% block usage %} -In order to use the plugin, run Snakemake (>=9.0) with the corresponding value for the logger flag:: +In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=9.0" }}) with the corresponding value for the logger flag:: snakemake --logger {{ plugin_name }} ... with ``...`` being any additional arguments you want to use. -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/source/_templates/plugin_base.rst.j2 b/source/_templates/plugin_base.rst.j2 index 23bc309..14bc9db 100644 --- a/source/_templates/plugin_base.rst.j2 +++ b/source/_templates/plugin_base.rst.j2 @@ -4,38 +4,44 @@ Snakemake {% block type %}{% endblock %} plugin: {{ plugin_name }} ########################################### -{% if repository is not none %} +{% if repository is not none -%} .. image:: https://img.shields.io/badge/repository-{{ repository_type }}-blue?color=%23022c22 :target: {{ repository }} -{% else %} +{% else -%} .. image:: https://img.shields.io/badge/repository-unknown-blue?color=%23022c22 :target: # -{% endif %} - -{% if repository_type == "github" and repository is not none %} -{% set repo = repository.replace("https://github.com/", "").replace(".git", "").rstrip("/") %} -.. image:: https://img.shields.io/github/last-commit/{{ repo }} +{% endif -%} +{% if repository_type == "github" and repository is not none -%} +.. image:: https://img.shields.io/github/last-commit/{{ repo_shortname }} :alt: GitHub - Last commit :target: {{ repository }} -{% elif repository_type == "gitlab" and repository is not none %} -{% set repo = repository.replace("https://gitlab.com/", "").replace(".git", "").rstrip("/") %} -.. image:: https://img.shields.io/gitlab/last-commit/{{ repo }} +{% elif repository_type == "gitlab" and repository is not none -%} +.. image:: https://img.shields.io/gitlab/last-commit/{{ repo_shortname }} :alt: GitLab - Last commit :target: {{ repository }} -{% endif %} - -{% for author in authors %} +{% elif commit_info is not none -%} +.. image:: https://img.shields.io/badge/last_commit-{{ commit_info["sha"] }}-green?color={{ commit_age_color }} + :alt: Last commit + :target: {{ commit_url }} +.. image:: https://img.shields.io/badge/last_updated-{{ commit_date_label }}-green?color={{ commit_age_color }} + :alt: Last updated + :target: {{ commit_url }} +{% endif -%} +{% for author in authors -%} .. image:: https://img.shields.io/badge/author-{{author|replace("-","--")|urlencode}}-purple?color=%23064e3b :target: https://pypi.org/project/{{ package_name }} -{% endfor %} - +{% endfor -%} .. image:: https://img.shields.io/pypi/v/{{ package_name }}?color=%23047857 :alt: PyPI - Version :target: https://pypi.org/project/{{ package_name }} - .. image:: https://img.shields.io/pypi/l/{{ package_name }}?color=%2310b981 :alt: PyPI - License :target: https://pypi.org/project/{{ package_name }} +{% if snakemake_version is not none %} +.. image:: https://img.shields.io/badge/snakemake-{{ snakemake_version|replace("-", "--") }}-blue?color=%230ea5e9 + :alt: Snakemake + :target: https://snakemake.readthedocs.io +{% endif %} {% if repository is not none and not repository.startswith("https://github.com/snakemake/") %} .. warning:: diff --git a/source/_templates/report_plugin.rst.j2 b/source/_templates/report_plugin.rst.j2 index 002a84f..aed538f 100644 --- a/source/_templates/report_plugin.rst.j2 +++ b/source/_templates/report_plugin.rst.j2 @@ -3,9 +3,9 @@ {% block type %}report{% endblock %} {% block usage %} -In order to use the plugin, run Snakemake (>=8.5) with the corresponding value for the reporter flag:: +In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=8.5" }}) with the corresponding value for the reporter flag:: snakemake --reporter {{ plugin_name }} ... with ``...`` being any additional arguments you want to use. -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/source/_templates/scheduler_plugin.rst.j2 b/source/_templates/scheduler_plugin.rst.j2 index 06e22ed..d056207 100644 --- a/source/_templates/scheduler_plugin.rst.j2 +++ b/source/_templates/scheduler_plugin.rst.j2 @@ -3,7 +3,7 @@ {% block type %}scheduler{% endblock %} {% block usage %} -In order to use the plugin, run Snakemake (>=9.0) with the corresponding value for the scheduler flag:: +In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=9.0" }}) with the corresponding value for the scheduler flag:: snakemake --scheduler {{ plugin_name }} ... diff --git a/source/collect_plugins.py b/source/collect_plugins.py index 5be302d..2b70494 100644 --- a/source/collect_plugins.py +++ b/source/collect_plugins.py @@ -1,16 +1,22 @@ from abc import ABC, abstractmethod from collections import defaultdict +from dataclasses import dataclass +from datetime import datetime, timezone +import git +import git.exc import json import os +import re from pathlib import Path import shutil import subprocess import sys import tempfile import textwrap -from typing import Any, Dict, List -import git +from typing import Any, Dict, List, Optional import uuid +from packaging.specifiers import SpecifierSet +from packaging.version import Version from pypi_simple import PyPISimple import requests @@ -166,7 +172,7 @@ def plugin_type(self) -> str: def aux_info(self, metadata_collector) -> Dict[str, Any]: return {} - def collect_plugins(self, plugins, packages, templates): + def collect_plugins(self, plugins, packages, templates, snakemake_compat_index): """ Collect plugins of the type of the corresponding plugin type collector class. Plugins are selected from the set of ALL pypi packages by matching names to the @@ -231,6 +237,10 @@ def get_setting_meta(setting, key, default="", verb=False): repository = project_urls.get("Repository") or project_urls.get( "repository" ) + # Clean up repository URL early - remove .git suffix and trailing slashes + if repository: + repository = repository.replace(".git", "").rstrip("/") + repository_type = None if repository is None: docs_warning = ( @@ -244,16 +254,48 @@ def get_setting_meta(setting, key, default="", verb=False): repository_type = "github" elif repository.startswith("https://gitlab.com"): repository_type = "gitlab" - docs_intro = get_docs(repository, section="intro") - docs_further = get_docs(repository, section="further") - if repository is not None and docs_intro is None and docs_further is None: - docs_warning = ( - f"No documentation found in repository {repository}. The plugin should " - "provide a docs/intro.md with some introductory sentences and " - "optionally a docs/further.md file with details beyond the " - "auto-generated usage instructions presented in this catalog." + + commit_info = None + commit_url = repository + docs_intro = None + docs_further = None + + # Fetch git info (commit + docs) in a single clone operation + if repository and (git_info := _get_plugin_git_info(repository)): + if git_info.commit: + commit_info = { + "sha": git_info.commit.sha, + "date": git_info.commit.date, + } + commit_url = _commit_url( + repository, repository_type, git_info.commit.sha + ) + + # Convert docs from markdown to RST + docs_intro = _convert_markdown_to_rst(git_info.docs.intro, "intro") + docs_further = _convert_markdown_to_rst( + git_info.docs.further, "further" ) + if docs_intro is None and docs_further is None: + docs_warning = ( + f"No documentation found in repository {repository}. The plugin should " + "provide a docs/intro.md with some introductory sentences and " + "optionally a docs/further.md file with details beyond the " + "auto-generated usage instructions presented in this catalog." + ) + + commit_date_label = ( + _commit_date_label(commit_info["date"]) if commit_info else None + ) + commit_age_color = ( + _commit_age_color(commit_info["date"]) if commit_info else None + ) + + snakemake_version = _plugin_min_snakemake( + meta["info"].get("requires_dist"), snakemake_compat_index + ) + settings = {} aux_info = {} @@ -272,12 +314,21 @@ def get_setting_meta(setting, key, default="", verb=False): else: error += "\n\nPlease contact the plugin authors." + # Get repository shortname for shields.io badges + repo_shortname = get_repo_shortname(repository) if repository else None + rendered = templates.get_template(f"{plugin_type}_plugin.rst.j2").render( plugin_name=plugin_name, package_name=package, authors=authors, repository=repository, + repo_shortname=repo_shortname, repository_type=repository_type, + commit_info=commit_info, + commit_url=commit_url, + commit_age_color=commit_age_color, + commit_date_label=commit_date_label, + snakemake_version=snakemake_version, meta=meta, desc=desc, docs_intro=docs_intro, @@ -331,6 +382,201 @@ def plugin_type(self): return "scheduler" +def _commit_date_label(date_str: str) -> str: + """Return a shields.io-safe date label like 'March_2026' from an ISO date string.""" + return datetime.fromisoformat(date_str.replace("Z", "+00:00")).strftime("%B_%Y") + + +def _commit_url( + repository: str, repository_type: Optional[str], commit_sha: str +) -> str: + """Construct commit URL based on repository type. + + Args: + repository: Base repository URL + repository_type: Type of repository ('github', 'gitlab', or None) + commit_sha: Commit SHA hash + + Returns: + Full URL to the specific commit + """ + if repository_type == "github": + return f"{repository}/commit/{commit_sha}" + elif repository_type == "gitlab": + return f"{repository}/-/commit/{commit_sha}" + else: + return repository + + +def get_repo_shortname(repository: Optional[str]) -> str: + """Extract the shortname from repository URL for shields.io badges. + + Removes protocol and known forge prefix (https://github.com/ or https://gitlab.com/), + returning just the user/repo path. + + Args: + repository: Full repository URL (e.g., https://github.com/user/repo) + + Returns: + Repository shortname (e.g., user/repo) + """ + if not repository: + return "" + + # Remove protocol and domain prefixes + cleaned = ( + repository.replace("https://github.com/", "") + .replace("https://gitlab.com/", "") + .replace("http://github.com/", "") + .replace("http://gitlab.com/", "") + ) + + return cleaned + + +def _commit_age_color(date_str: str) -> str: + """Return a shields.io color hex based on how old the commit date is.""" + dt = datetime.fromisoformat(date_str.replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + months = (now.year - dt.year) * 12 + (now.month - dt.month) + if months < 6: + return "%2316a34a" # green + elif months < 24: + return "%23ea580c" # orange + else: + return "%23dc2626" # red + + +_INTERFACE_PKG_RE = re.compile( + r"(snakemake-interface-(?:executor|storage|report|logger|scheduler)-plugins)" + r"\s*\(?([^)]+)" +) + + +def _build_snakemake_compat_index() -> list[tuple]: + """Build compatibility index mapping Snakemake versions to interface requirements. + + Returns a sorted list of (snakemake_version, interface_pkg, lower, upper) tuples + where lower/upper are Version objects (or None) representing the interface version range + that each Snakemake version requires. + + Example: (Version("8.0.0"), "snakemake-interface-executor-plugins", Version("1.0"), Version("2.0")) + means Snakemake 8.0 requires executor interface >=1.0,<2.0 + """ + print("Building Snakemake compatibility index...", file=sys.stderr) + meta = pypi_api("https://pypi.org/pypi/snakemake/json") + + # Get all non-prerelease versions >= 8.0.0 + all_versions = sorted( + [ + v + for v in meta["releases"] + if not Version(v).is_prerelease and Version(v) >= Version("8.0.0") + ], + key=Version, + ) + + # Build index by checking versions and deduplicating when requirements don't change + # This handles cases where interface requirements change mid-minor version + # (e.g., 8.10.3 has different requirements than 8.10.0) + entries = [] + prev_requirements = {} # iface_pkg -> (lower, upper) + + for snakemake_ver in all_versions: + try: + ver_meta = pypi_api(f"https://pypi.org/pypi/snakemake/{snakemake_ver}/json") + except MetadataError: + continue + + current_requirements = {} + for dep in ver_meta["info"].get("requires_dist") or []: + match = _INTERFACE_PKG_RE.search(dep) + if not match: + continue + + iface_pkg = match.group(1) + spec = SpecifierSet(match.group(2).strip()) + + # Extract lower and upper bounds from specifier set + lower = upper = None + for s in spec: + v = Version(s.version) + if s.operator in (">=", ">"): + lower = v if lower is None else max(lower, v) + elif s.operator in ("<", "<="): + upper = v if upper is None else min(upper, v) + + current_requirements[iface_pkg] = (lower, upper) + + # Only add entry if requirements changed from previous version + if current_requirements != prev_requirements: + for iface_pkg, (lower, upper) in current_requirements.items(): + entries.append((Version(snakemake_ver), iface_pkg, lower, upper)) + prev_requirements = current_requirements + + return sorted(entries, key=lambda e: e[0]) + + +def _plugin_min_snakemake( + requires_dist: list[str] | None, + compat_index: list[tuple], +) -> str | None: + """Return the minimum Snakemake version compatible with a plugin. + + A plugin is compatible with a Snakemake version if their interface version requirements overlap. + + Example: + Plugin requires: interface >=2.5 + Snakemake 8.1 requires: interface >=2.0,<3.0 + Overlap: [2.5, 3.0) -> Compatible! + + Plugin requires: interface >=1.5 + Snakemake 8.1 requires: interface >=2.0,<3.0 + No overlap: plugin allows 1.5-1.9 which Snakemake doesn't support -> Incompatible + + Returns: + Minimum Snakemake version string like ">=8.1" or None if incompatible + """ + if not requires_dist: + return None + + # Extract plugin's interface requirement + plugin_iface = None + plugin_lower = None + for dep in requires_dist: + match = _INTERFACE_PKG_RE.search(dep) + if match: + plugin_iface = match.group(1) + spec = SpecifierSet(match.group(2).strip()) + for s in spec: + if s.operator in (">=", ">"): + v = Version(s.version) + plugin_lower = v if plugin_lower is None else max(plugin_lower, v) + break + + if plugin_lower is None or plugin_iface is None: + return None + + # Find first Snakemake version with overlapping interface requirement + for snakemake_ver, iface_pkg, snakemake_lower, snakemake_upper in compat_index: + if iface_pkg != plugin_iface: + continue + + # Check if [plugin_lower, ∞) overlaps with [snakemake_lower, snakemake_upper) + # No overlap if plugin needs older version than Snakemake provides + if snakemake_lower is not None and plugin_lower < snakemake_lower: + continue + + # No overlap if plugin needs newer version than Snakemake supports + if snakemake_upper is not None and plugin_lower >= snakemake_upper: + continue + + # Found compatible version + return f">={snakemake_ver.major}.{snakemake_ver.minor}" + + return None + + def collect_plugins(): templates = Environment( loader=FileSystemLoader("_templates"), @@ -340,6 +586,7 @@ def collect_plugins(): ) plugins = defaultdict(list) + snakemake_compat_index = _build_snakemake_compat_index() with PyPISimple() as pypi_client: packages = pypi_client.get_index_page().projects @@ -351,7 +598,9 @@ def collect_plugins(): LoggerPluginCollector, SchedulerPluginCollector, ): - collector().collect_plugins(plugins, packages, templates) + collector().collect_plugins( + plugins, packages, templates, snakemake_compat_index + ) with open("index.rst", "w") as f: f.write(templates.get_template("index.rst.j2").render(plugins=plugins)) @@ -360,36 +609,97 @@ def collect_plugins(): SECTION_MARK_ORDER = '#*=-^"~:`_+<' -def retrieve_plugin_markdown_files(repo_url: str, branches: [str], section: str): - """ - fetch the intro.md and further.md doc files provided by plugins - """ - docs_path = f"docs/{section}.md" +@dataclass +class CommitInfo: + """Metadata about the latest commit.""" + + sha: str + date: str + + +@dataclass +class PluginDocs: + """Documentation files from the plugin repository.""" + + intro: Optional[str] + further: Optional[str] + + +@dataclass +class PluginGitInfo: + """Git information for a plugin.""" + + commit: Optional[CommitInfo] + docs: PluginDocs + + +def _get_plugin_git_info( + repo_url: str, branches: Optional[List[str]] = None +) -> PluginGitInfo: + """Clone the plugin repo once (bare) and return docs + commit info.""" + + if branches is None: + branches = ["main", "master"] + for branch in branches: try: with tempfile.TemporaryDirectory() as tmpdir: repo = git.Repo.clone_from(repo_url, to_path=tmpdir, bare=True) - docs_content = repo.git.show(f"{branch}:{docs_path}") - return docs_content - except git.GitCommandError as e: - print(f"Failed to get cached git source file {branch}:{docs_path}: {e}. ") + # commit metadata + commit = repo.commit(branch) + commit_info = CommitInfo( + sha=commit.hexsha[:7], + date=commit.committed_datetime.isoformat(), + ) -def get_docs(repository: str | None, section: str, branches=["main", "master"]): - if repository is None: - return None - retrieved = retrieve_plugin_markdown_files(repository, branches, section) - - if retrieved is not None: - renderer = m2r2.RestRenderer() - renderer.hmarks = { - i + 1: mark - for i, mark in enumerate( - SECTION_MARK_ORDER[3:] - if section == "further" - else SECTION_MARK_ORDER[2:] + # docs + def _show(section: str) -> Optional[str]: + try: + return repo.git.show(f"{branch}:docs/{section}.md") + except git.GitCommandError: + return None + + docs = PluginDocs(intro=_show("intro"), further=_show("further")) + return PluginGitInfo(commit=commit_info, docs=docs) + except git.exc.BadName: + print( + f"Warning: Branch '{branch}' not found in {repo_url}, trying next branch...", + file=sys.stderr, ) - } - return m2r2.convert(retrieved, renderer=renderer) - else: + continue + except git.GitCommandError as e: + # Clone failures or other git errors + print( + f"Git error accessing {repo_url} on branch '{branch}': {e}", + file=sys.stderr, + ) + continue + + return PluginGitInfo(commit=None, docs=PluginDocs(intro=None, further=None)) + + +def _convert_markdown_to_rst( + markdown_content: Optional[str], section: str +) -> Optional[str]: + """ + Convert markdown documentation to RST format. + + Args: + markdown_content: Markdown content to convert + section: Documentation section ('intro' or 'further') to determine heading marks + + Returns: + Converted RST documentation or None if input is None + """ + if markdown_content is None: return None + + renderer = m2r2.RestRenderer() + renderer.hmarks = { + i + 1: mark + for i, mark in enumerate( + SECTION_MARK_ORDER[3:] if section == "further" else SECTION_MARK_ORDER[2:] + ) + } + return m2r2.convert(markdown_content, renderer=renderer) diff --git a/source/test_collect_plugins.py b/source/test_collect_plugins.py new file mode 100644 index 0000000..d304eb9 --- /dev/null +++ b/source/test_collect_plugins.py @@ -0,0 +1,295 @@ +"""Unit tests for collect_plugins.""" + +from packaging.version import Version + +from collect_plugins import ( + _convert_markdown_to_rst, + _plugin_min_snakemake, + _commit_url, + get_repo_shortname, +) + + +# Markdown conversion tests + + +def test_convert_markdown_to_rst_none(): + """Test markdown conversion handles None input.""" + assert _convert_markdown_to_rst(None, "intro") is None + + +def test_convert_markdown_to_rst_intro_heading_levels(): + """Test intro section uses correct RST heading marks.""" + result = _convert_markdown_to_rst("# H1\n## H2\n### H3", "intro") + assert result is not None + # intro starts at SECTION_MARK_ORDER[2:], so # -> =, ## -> -, ### -> ^ + assert "=" in result # H1 underline + assert "-" in result # H2 underline + + +def test_convert_markdown_to_rst_further_heading_levels(): + """Test further section uses correct RST heading marks.""" + result = _convert_markdown_to_rst("# H1\n## H2", "further") + assert result is not None + # further starts at SECTION_MARK_ORDER[3:], so # -> -, ## -> ^ + assert "-" in result # H1 underline + + +def test_convert_markdown_to_rst_preserves_content(): + """Test markdown content is preserved during conversion.""" + markdown = "# Title\n\nSome **bold** text.\n\n- Item 1\n- Item 2" + result = _convert_markdown_to_rst(markdown, "intro") + assert result is not None + assert "Title" in result + assert "bold" in result + assert "Item 1" in result + + +# Compatibility index tests + + +def test_plugin_min_snakemake_exact_match(): + """Plugin requirement exactly matches Snakemake's lower bound.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=1.0)"] + result = _plugin_min_snakemake(requires, compat_index) + assert result == ">=8.0" + + +def test_plugin_min_snakemake_plugin_too_old(): + """Plugin requires older version than Snakemake provides.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("2.0"), + Version("3.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=1.5)"] + result = _plugin_min_snakemake(requires, compat_index) + # Plugin requires >=1.5 but Snakemake needs >=2.0, so incompatible + assert result is None + + +def test_plugin_min_snakemake_plugin_too_new(): + """Plugin requires newer version than Snakemake provides.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=2.5)"] + result = _plugin_min_snakemake(requires, compat_index) + # Plugin requires >=2.5 but Snakemake supports <2.0, so incompatible + assert result is None + + +def test_plugin_min_snakemake_within_range(): + """Plugin requirement falls within Snakemake's range.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("3.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=1.5)"] + result = _plugin_min_snakemake(requires, compat_index) + assert result == ">=8.0" + + +def test_plugin_min_snakemake_multiple_versions(): + """Find first compatible Snakemake version among multiple.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ( + Version("8.1.0"), + "snakemake-interface-executor-plugins", + Version("2.0"), + Version("3.0"), + ), + ( + Version("8.2.0"), + "snakemake-interface-executor-plugins", + Version("3.0"), + Version("4.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=2.5)"] + result = _plugin_min_snakemake(requires, compat_index) + # Plugin >=2.5 is compatible with 8.1.0 [2.0, 3.0) + assert result == ">=8.1" + + +def test_plugin_min_snakemake_no_upper_bound(): + """Snakemake has no upper bound.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + None, + ), + ] + requires = ["snakemake-interface-executor-plugins (>=1.5)"] + result = _plugin_min_snakemake(requires, compat_index) + assert result == ">=8.0" + + +def test_plugin_min_snakemake_no_lower_bound(): + """Snakemake has no lower bound.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + None, + Version("2.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=1.5)"] + result = _plugin_min_snakemake(requires, compat_index) + assert result == ">=8.0" + + +def test_plugin_min_snakemake_empty_requires(): + """Plugin has no requirements.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ] + result = _plugin_min_snakemake(None, compat_index) + assert result is None + + +def test_plugin_min_snakemake_no_interface_requirement(): + """Plugin has requirements but no interface plugin requirement.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ] + requires = ["requests (>=2.0)"] + result = _plugin_min_snakemake(requires, compat_index) + assert result is None + + +def test_plugin_min_snakemake_different_interface(): + """Plugin requires different interface type.""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ] + requires = ["snakemake-interface-storage-plugins (>=1.0)"] + result = _plugin_min_snakemake(requires, compat_index) + # No matching interface in index + assert result is None + + +def test_plugin_min_snakemake_edge_case_at_upper_bound(): + """Plugin lower bound equals Snakemake upper bound (exclusive).""" + compat_index = [ + ( + Version("8.0.0"), + "snakemake-interface-executor-plugins", + Version("1.0"), + Version("2.0"), + ), + ] + requires = ["snakemake-interface-executor-plugins (>=2.0)"] + result = _plugin_min_snakemake(requires, compat_index) + # Plugin >=2.0 doesn't overlap with [1.0, 2.0) + assert result is None + + +# Commit URL construction tests + + +def test_commit_url_github(): + """Test GitHub commit URL construction.""" + url = _commit_url("https://github.com/user/repo", "github", "abc1234") + assert url == "https://github.com/user/repo/commit/abc1234" + + +def test_commit_url_gitlab(): + """Test GitLab commit URL construction.""" + url = _commit_url("https://gitlab.com/user/repo", "gitlab", "abc1234") + assert url == "https://gitlab.com/user/repo/-/commit/abc1234" + + +def test_commit_url_unknown_type(): + """Test unknown repository type returns base URL.""" + url = _commit_url("https://example.com/repo", None, "abc1234") + assert url == "https://example.com/repo" + + +def test_commit_url_unknown_type_non_none(): + """Test unknown repository type string returns base URL.""" + url = _commit_url("https://bitbucket.org/user/repo", "bitbucket", "abc1234") + assert url == "https://bitbucket.org/user/repo" + + +# Repository shortname extraction tests + + +def test_get_repo_shortname_github_https(): + """Test extracting shortname from GitHub HTTPS URL.""" + shortname = get_repo_shortname("https://github.com/user/repo") + assert shortname == "user/repo" + + +def test_get_repo_shortname_gitlab_https(): + """Test extracting shortname from GitLab HTTPS URL.""" + shortname = get_repo_shortname("https://gitlab.com/user/repo") + assert shortname == "user/repo" + + +def test_get_repo_shortname_http_protocol(): + """Test extracting shortname from HTTP protocol URL.""" + shortname = get_repo_shortname("http://github.com/user/repo") + assert shortname == "user/repo" + + +def test_get_repo_shortname_empty_string(): + """Test extracting shortname from empty string.""" + shortname = get_repo_shortname("") + assert shortname == "" + + +def test_get_repo_shortname_none(): + """Test extracting shortname from None value.""" + shortname = get_repo_shortname(None) + assert shortname == "" + + +def test_get_repo_shortname_other_domain(): + """Test URL from other domain remains unchanged.""" + shortname = get_repo_shortname("https://bitbucket.org/user/repo") + assert shortname == "https://bitbucket.org/user/repo"