From 7c310196f87e4e3ba8503d12c45f799c831954e1 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph <44614774+jjjermiah@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:30:04 -0500 Subject: [PATCH] feat: implement logging and some standards for ruff config (#134) * feat(logging): add structlog integration with custom processors and example logger * chore: remove star imports * feat(lint): update ruff configuration to include new linting rules and extend exclusions * refactor: update pixi.lock and pixi.toml for ruff command structure and remove unused betapipeline entry * refactor(logging): standardize string quotes and improve logger configuration handling * feat(logging): integrate structured logging and enhance debug information in AutoPipeline and StructureSet * fix: https://github.com/bhklab/med-imagetools/pull/134#discussion_r1846958744 * fix: https://github.com/bhklab/med-imagetools/pull/134#discussion_r1846958749 * fix: https://github.com/bhklab/med-imagetools/pull/134#discussion_r1846958747 * refactor(logging): streamline logging configuration and ensure log directory creation * chore: add log files to .gitignore to prevent tracking of generated logs * fix: Update src/imgtools/logging/__init__.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: https://github.com/bhklab/med-imagetools/pull/134#discussion_r1847024952 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: update .gitignore to include log files in imgtools directory * feat(datagraph): integrate logging for edge table processing and visualization * feat(ops): add timing for graph formation and update DataGraph initialization * chore: rename workflow from Test to CI-CD and restrict push triggers to main branch * feat(crawl): integrate logging for folder crawling and data saving processes * fix(structureset): enhance logging for ROI point retrieval errors * fix(autopipeline): update logger level to use environment variable for flexibility * fix(logging): streamline error handling for log directory creation * fix: https://github.com/bhklab/med-imagetools/pull/134#discussion_r1847278305 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(logging): enhance logging configuration and streamline JSON setup * chore(pixi.lock): add license_family field for clarity * refactor(logging): enhance LoggingManager with valid log levels and improve JSON logging setup * refactor(logging): enhance documentation and improve LoggingManager configuration options * refactor(logging): validate log level assignment before setting self.level * feat(logging): add mypy and type-checking support; refactor logging manager and improve error handling * refactor(logging): remove unused Optional import from typing --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 4 +- .gitignore | 2 + config/mypy.ini | 2 + config/ruff.toml | 109 +++++- pixi.lock | 541 +++++++++++++++++---------- pixi.toml | 42 ++- pyproject.toml | 3 +- src/imgtools/__init__.py | 4 - src/imgtools/autopipeline.py | 29 +- src/imgtools/logging/__init__.py | 388 +++++++++++++++++++ src/imgtools/logging/__main__.py | 71 ++++ src/imgtools/logging/processors.py | 145 +++++++ src/imgtools/modules/datagraph.py | 24 +- src/imgtools/modules/structureset.py | 12 +- src/imgtools/ops/ops.py | 99 +++-- src/imgtools/utils/crawl.py | 13 +- tests/test_ops.py | 2 +- 17 files changed, 1212 insertions(+), 278 deletions(-) create mode 100644 config/mypy.ini create mode 100644 src/imgtools/logging/__init__.py create mode 100644 src/imgtools/logging/__main__.py create mode 100644 src/imgtools/logging/processors.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a28367af..eb321f6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,9 @@ -name: Test +name: CI-CD # only run on pushes to main or pull requests on: push: - branches: ["*"] + branches: ["main"] pull_request: branches: ["*"] diff --git a/.gitignore b/.gitignore index 5dce9fc9..f36461d6 100644 --- a/.gitignore +++ b/.gitignore @@ -219,3 +219,5 @@ data/ # pixi environments .pixi + +.imgtools/**/*.log \ No newline at end of file diff --git a/config/mypy.ini b/config/mypy.ini new file mode 100644 index 00000000..da997493 --- /dev/null +++ b/config/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +files = src/imgtools/logging/**/*.py diff --git a/config/ruff.toml b/config/ruff.toml index 7a527d61..e563ad8e 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,6 +1,109 @@ -[ lint ] -# select = ["E4", "E7", "E9", "F", "B"] +# NOTE: +# The idea is that all future files should be included in the linting process. +# To save the headache, we are excluding everything before, and hopefully we can +# slowly fix everything -ignore = [ "E722", "F405", "F403" ] +include = [ + "src/imgtools/logging/**/*.py", +] +extend-exclude = [ + ".pixi/**/*", + "tests/**/*.py", + "src/imgtools/ops/**/*.py", + "src/imgtools/io/**/*.py", + "src/imgtools/utils/**/*.py", + "src/imgtools/modules/**/*.py", + "src/imgtools/transforms/**/*.py", + "src/imgtools/autopipeline.py", + "src/imgtools/pipeline.py", + "src/imgtools/image.py", +] + +line-length = 100 + +[lint] + +select = [ + ########################################################################### + # TYPE ANNOTATIONS + # Ensure all functions have type annotations + # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ANN", + # Use type hinting consistently + # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + "TCH", + + ########################################################################### + # IMPORTS + # Sort imports naturally + # https://docs.astral.sh/ruff/rules/#isort-i + "I", + # Follow import conventions + # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + "ICN", + # Clean up and organize imports + # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid + "TID", + + ########################################################################### + # CODE QUALITY + # Detect possible bugs, like unused variables or exception handling issues + # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "B", + # Avoid using Python builtins incorrectly + # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "A", + # Enforce correct usage of commas in lists, tuples, etc. + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM", + # Prevent use of debugging code, like breakpoints + # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 + "T10", + # Disallow print statements + # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "T20", + # Provide clear and explanatory error messages + # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + "EM", + + ########################################################################### + # STANDARDS & STYLE + # Prefer pathlib for path manipulation + # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "PTH", + # Adhere to Pylint conventions + # https://docs.astral.sh/ruff/rules/#pylint-pl + "PL", + # Simplify code to reduce complexity + # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "SIM", + # errors like undefined names and unused imports without enforcing style rules. + # https://docs.astral.sh/ruff/rules/#pyflakes-f + "F", + # + # https://docs.astral.sh/ruff/rules/#pep8-naming-n + "N", + # Pydocstyle + # https://docs.astral.sh/ruff/rules/#pydocstyle-d + # "D", +] + +ignore = [ + # allow self to not need type annotations + "ANN101", + # Allow too many arguments for functions + "PLR0913", + # Public Module Docstrings + "D100", + # Ignored because https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/#missing-trailing-comma-com812 + +] + +[format] + +quote-style = "single" +indent-style = "tab" +docstring-code-format = true diff --git a/pixi.lock b/pixi.lock index e040fbe8..026a8e5e 100644 --- a/pixi.lock +++ b/pixi.lock @@ -36,7 +36,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/click-option-group-0.5.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py312h178313f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py312h178313f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-43.0.3-py312hda17c39_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.8-py312h2ec8cdc_0.conda @@ -61,7 +61,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/hatch-1.13.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.26.3-pypyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.27.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hyperlink-21.0.0-pyhd3deb0d_0.tar.bz2 @@ -91,7 +91,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.1-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.5.0-pyha804496_0.conda @@ -121,6 +121,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.1.0-py312h178313f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.13.0-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_0.conda @@ -130,7 +132,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda @@ -156,7 +158,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.7-hc5c86c4_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gitlab-4.13.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda @@ -173,10 +175,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.21.0-py312h12e396e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.7.3-py312h2156523_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.7.4-py312h2156523_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py312h7900ff3_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.5.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.0-pyhd8ed1ab_0.tar.bz2 @@ -194,6 +196,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.10.21.16-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241003-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-pytz-2024.2.0.20241003-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-tqdm-4.66.0.20240417-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2 @@ -202,7 +206,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.7.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.1-h0f3a69f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.2-h0f3a69f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.27.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.8.0-pyhd8ed1ab_0.conda @@ -210,7 +214,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.16.0-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.17.2-py312h66e93f0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312hef9b889_1.conda @@ -236,6 +240,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/48/f8/3f00cc6d4f11b3cd934e3024c5be71ffc6d30d4620a16de7d194381f92f9/SimpleITK-2.4.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -270,7 +275,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/click-option-group-0.5.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py312ha0ccf2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py312h998013c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.8-py312hd8f9ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 @@ -292,7 +297,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/hatch-1.13.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.26.3-pypyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.27.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hyperlink-21.0.0-pyhd3deb0d_0.tar.bz2 @@ -321,7 +326,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.1-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.5.0-pyh534df25_0.conda @@ -340,6 +345,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.1.0-py312hdb8e49c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.13.0-py312h0bf5046_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_0.conda @@ -349,7 +356,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h39f12f2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda @@ -376,7 +383,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.7-h739c21a_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gitlab-4.13.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda @@ -393,9 +400,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.21.0-py312hcd83bfe_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.7.3-py312h5d18b81_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.7.4-py312h5d18b81_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.5.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.0-pyhd8ed1ab_0.tar.bz2 @@ -413,6 +420,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.10.21.16-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241003-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-pytz-2024.2.0.20241003-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-tqdm-4.66.0.20240417-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2 @@ -421,7 +430,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.7.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.1-h668ec48_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.2-h668ec48_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.27.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.8.0-pyhd8ed1ab_0.conda @@ -429,15 +438,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.16.0-py312h0bf5046_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h9f5b81c_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.17.2-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312h15fbf35_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda - pypi: https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/9d/cfbfe36e5061a8f68b154454ba2304eb01f40d4ba9b63e41d9058909baed/fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ec/79/38209f8f6235021b6209147ec7b2f748afde65c59c6274ac96fef3912094/fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/0d/74/1009b663387c025e8fa5f3ee3cf3cd0d99b1ad5c72eeb70e75366b1ce878/h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/4e/e7/26045404a30c8a200e960fb54fbaf4b73d12e58cd28e03b306b084253f4f/imageio-2.36.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl @@ -455,6 +464,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/1a/7deb52fa23aebb855431ad659b3c6a2e1709ece582cb3a63d66905e735fe/scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/79/d0aa407da1e853fa5f02e93b6d5bde599e021751294381b565e07276f0b0/SimpleITK-2.4.0-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -472,7 +482,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py310h89163eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py310h89163eb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda @@ -488,7 +498,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda @@ -502,6 +512,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl @@ -527,6 +538,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/47/78/b0c2c23880dd1e99e938ad49ccfb011ae353758a2dc5ed7ee59baff684c3/scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ee/cf/c6de71a85f81e719a41f8873ea1ef4b80b5f5d5b65176913af34e914bc8f/SimpleITK-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -537,7 +549,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py310h5799be4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py310hc74094e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda @@ -546,7 +558,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda @@ -560,10 +572,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2f/9a/9d899e7ae55b0dd30632e6ca36c0f5fa1205b1b096ec171c9be903673058/fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d8/8c/57600ebff0b2119b725bc11eeea32b17b0220f3fae71b5fff082a1891270/fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/29/a7/3c2a33fba1da64a0846744726fd067a92fb8abb887875a0dd8e3bac8b45d/h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/4e/e7/26045404a30c8a200e960fb54fbaf4b73d12e58cd28e03b306b084253f4f/imageio-2.36.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl @@ -585,6 +598,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/43/a5/8d02f9c372790326ad405d94f04d4339482ec082455b9e6e288f7100513b/scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/fd/33/bed962658beeb8e9152ff542dfa1ae3309979e098705c6bb64aaa7fc9589/SimpleITK-2.4.0-cp310-cp310-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -603,7 +617,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py311h2dc5d0c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda @@ -620,7 +634,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda @@ -634,6 +648,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl @@ -659,6 +674,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/93/6b/701776d4bd6bdd9b629c387b5140f006185bd8ddea16788a44434376b98f/scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/48/f8/3f00cc6d4f11b3cd934e3024c5be71ffc6d30d4620a16de7d194381f92f9/SimpleITK-2.4.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -669,7 +685,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py311h56c23cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py311h4921393_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda @@ -679,7 +695,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda @@ -693,10 +709,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/37/2e/f94118b92f7b6a9ec93840101b64bfdd09f295b266133857e8e852a5c35c/fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/17/50/75461e050ded02b9eaa8097df52c2a8752cf4c24db8b44b150755b76c8f1/fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/95/9d/eb91a9076aa998bb2179d6b1788055ea09cdf9d6619cd967f1d3321ed056/h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/4e/e7/26045404a30c8a200e960fb54fbaf4b73d12e58cd28e03b306b084253f4f/imageio-2.36.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl @@ -718,6 +735,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/c5/02ac82f9bb8f70818099df7e86c3ad28dae64e1347b421d8e3adf26acab6/scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/79/d0aa407da1e853fa5f02e93b6d5bde599e021751294381b565e07276f0b0/SimpleITK-2.4.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -736,7 +754,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py312h178313f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py312h178313f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda @@ -753,7 +771,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda @@ -767,6 +785,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ba/99/6794142b90b853a9155316c8f470d2e4821fe6f086b03e372aca848227dd/contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl @@ -792,6 +811,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/48/f8/3f00cc6d4f11b3cd934e3024c5be71ffc6d30d4620a16de7d194381f92f9/SimpleITK-2.4.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -802,7 +822,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py312ha0ccf2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py312h998013c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda @@ -812,7 +832,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_0.conda @@ -826,10 +846,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/67/9d/cfbfe36e5061a8f68b154454ba2304eb01f40d4ba9b63e41d9058909baed/fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ec/79/38209f8f6235021b6209147ec7b2f748afde65c59c6274ac96fef3912094/fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/0d/74/1009b663387c025e8fa5f3ee3cf3cd0d99b1ad5c72eeb70e75366b1ce878/h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/4e/e7/26045404a30c8a200e960fb54fbaf4b73d12e58cd28e03b306b084253f4f/imageio-2.36.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl @@ -851,6 +872,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/79/d0aa407da1e853fa5f02e93b6d5bde599e021751294381b565e07276f0b0/SimpleITK-2.4.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/0a/435d5d7ec64d1c8b422ac9ebe42d2f3b2ac0b3f8a56f5c04dd0f3b7ba83c/tifffile-2024.9.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl @@ -1467,6 +1489,15 @@ packages: - pkg:pypi/charset-normalizer?source=hash-mapping size: 47314 timestamp: 1728479405343 +- kind: pypi + name: click + version: 8.1.7 + url: https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl + sha256: ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 + requires_dist: + - colorama ; platform_system == 'Windows' + - importlib-metadata ; python_full_version < '3.8' + requires_python: '>=3.7' - kind: conda name: click version: 8.1.7 @@ -1690,52 +1721,52 @@ packages: requires_python: '>=3.10' - kind: conda name: coverage - version: 7.6.4 - build: py310h5799be4_0 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py310h5799be4_0.conda - sha256: 44d0cd60e7ed89500e191d43510bedb1d5002aeaf1f6e7afee734234cd954782 - md5: 380821e5415e15e36f5e85a2581dcbca + version: 7.6.7 + build: py310h89163eb_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py310h89163eb_0.conda + sha256: 9f2eac7b7b148bf48adaa946d331103bcd5306b588f319b21166c4f5851d5086 + md5: edced792209b5a2591ebccad19955a59 depends: - - __osx >=11.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - tomli license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 292349 - timestamp: 1729610262681 + size: 293679 + timestamp: 1731698707639 - kind: conda name: coverage - version: 7.6.4 - build: py310h89163eb_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py310h89163eb_0.conda - sha256: b4df2e45f8c43bc47c1695bfbd4c526ce5224812f9c41d9451c88541c36655ea - md5: 5222543cdb180f0fecc0d4b9f6b4a225 + version: 7.6.7 + build: py310hc74094e_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py310hc74094e_0.conda + sha256: 45e6658cab7fcdc2ba44bd4b02c552be155ebff322beb90c61c975fb2d9911cc + md5: 88292f9e59b79b17ca3088843d0f81e3 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - __osx >=11.0 - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - tomli license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 292863 - timestamp: 1729610244611 + size: 293578 + timestamp: 1731698812707 - kind: conda name: coverage - version: 7.6.4 + version: 7.6.7 build: py311h2dc5d0c_0 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py311h2dc5d0c_0.conda - sha256: c4cde56626b863128f7f249073aa093aee885fe8d68415d7cec74877caa39ff8 - md5: 4d74dedf541d0f87fce0b5797b66e425 + url: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py311h2dc5d0c_0.conda + sha256: 079b2b5f7d8393c4f318204ba458cbdb7238da9dbecf26c780925d96fc49293a + md5: 453d38067da1c98fed8667cbd2b5a570 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -1746,16 +1777,16 @@ packages: license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 373322 - timestamp: 1729610201298 + size: 374966 + timestamp: 1731698660188 - kind: conda name: coverage - version: 7.6.4 - build: py311h56c23cb_0 + version: 7.6.7 + build: py311h4921393_0 subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py311h56c23cb_0.conda - sha256: 44b3a9de067245302934dc85973bd2b29d56849332774d8dda3b0c2eb0f78e7c - md5: 3485c1ee29d72d316c427f872d0f368c + url: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py311h4921393_0.conda + sha256: aafe2c5612fadaeddb9de507ba1d07dbbd37c2cf644d0a0ff2a573bf70e8aaf5 + md5: 0922d5bf8b7b22b550a25726b2967fd3 depends: - __osx >=11.0 - python >=3.11,<3.12.0a0 @@ -1766,16 +1797,16 @@ packages: license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 372411 - timestamp: 1729610328835 + size: 372952 + timestamp: 1731698891980 - kind: conda name: coverage - version: 7.6.4 + version: 7.6.7 build: py312h178313f_0 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.4-py312h178313f_0.conda - sha256: 62ef1654898b67a1aae353c8910323c803db0dcf0c117d5796eb1cfb03a2d777 - md5: a32fbd2322865ac80c7db74c553f5306 + url: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.6.7-py312h178313f_0.conda + sha256: 2d8f9a588f89fc3df7b98ab5ffcf5968c82c34813db7373f40b5a7de71eabf2a + md5: f64f3206bf9e86338b881957fd498870 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -1786,16 +1817,16 @@ packages: license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 363969 - timestamp: 1729610283175 + size: 364489 + timestamp: 1731698658681 - kind: conda name: coverage - version: 7.6.4 - build: py312ha0ccf2a_0 + version: 7.6.7 + build: py312h998013c_0 subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.4-py312ha0ccf2a_0.conda - sha256: 4211bf3f5a2f9f45fbf7c73e40dfb6ad84f6123bbf77c3bee5ca04b85c8c400f - md5: ff7f61eae0c3bc906a37f0804208fd46 + url: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.6.7-py312h998013c_0.conda + sha256: 2be117611f5c776372b7d12ea8bce2d0452022e612c84e042017eb6c777b9da6 + md5: 0962c6746e00b34ce0584d3ae129d266 depends: - __osx >=11.0 - python >=3.12,<3.13.0a0 @@ -1806,8 +1837,8 @@ packages: license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 362479 - timestamp: 1729610313800 + size: 363385 + timestamp: 1731698781484 - kind: conda name: cryptography version: 43.0.3 @@ -2099,9 +2130,9 @@ packages: timestamp: 1726613593584 - kind: pypi name: fonttools - version: 4.54.1 - url: https://files.pythonhosted.org/packages/2f/9a/9d899e7ae55b0dd30632e6ca36c0f5fa1205b1b096ec171c9be903673058/fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl - sha256: 41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882 + version: 4.55.0 + url: https://files.pythonhosted.org/packages/17/50/75461e050ded02b9eaa8097df52c2a8752cf4c24db8b44b150755b76c8f1/fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl + sha256: fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51 requires_dist: - fs>=2.2.0,<3 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -2136,9 +2167,9 @@ packages: requires_python: '>=3.8' - kind: pypi name: fonttools - version: 4.54.1 - url: https://files.pythonhosted.org/packages/37/2e/f94118b92f7b6a9ec93840101b64bfdd09f295b266133857e8e852a5c35c/fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl - sha256: 301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2 + version: 4.55.0 + url: https://files.pythonhosted.org/packages/47/2b/9bf7527260d265281dd812951aa22f3d1c331bcc91e86e7038dc6b9737cb/fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6 requires_dist: - fs>=2.2.0,<3 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -2173,9 +2204,9 @@ packages: requires_python: '>=3.8' - kind: pypi name: fonttools - version: 4.54.1 - url: https://files.pythonhosted.org/packages/67/9d/cfbfe36e5061a8f68b154454ba2304eb01f40d4ba9b63e41d9058909baed/fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl - sha256: 8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08 + version: 4.55.0 + url: https://files.pythonhosted.org/packages/c4/03/8136887d1b0b7a9831c7e8e2598c0e5851e31cc2231295769350349a236b/fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60 requires_dist: - fs>=2.2.0,<3 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -2211,8 +2242,8 @@ packages: - kind: pypi name: fonttools version: 4.55.0 - url: https://files.pythonhosted.org/packages/47/2b/9bf7527260d265281dd812951aa22f3d1c331bcc91e86e7038dc6b9737cb/fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6 + url: https://files.pythonhosted.org/packages/d8/8c/57600ebff0b2119b725bc11eeea32b17b0220f3fae71b5fff082a1891270/fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl + sha256: 51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61 requires_dist: - fs>=2.2.0,<3 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -2248,8 +2279,8 @@ packages: - kind: pypi name: fonttools version: 4.55.0 - url: https://files.pythonhosted.org/packages/c4/03/8136887d1b0b7a9831c7e8e2598c0e5851e31cc2231295769350349a236b/fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: 31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60 + url: https://files.pythonhosted.org/packages/ec/79/38209f8f6235021b6209147ec7b2f748afde65c59c6274ac96fef3912094/fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl + sha256: 838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7 requires_dist: - fs>=2.2.0,<3 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -2583,26 +2614,26 @@ packages: timestamp: 1598856368685 - kind: conda name: httpcore - version: 1.0.6 - build: pyhd8ed1ab_0 + version: 1.0.7 + build: pyh29332c3_1 + build_number: 1 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.6-pyhd8ed1ab_0.conda - sha256: 8952c3f1eb18bf4d7e813176c3b23e0af4e863e8b05087e73f74f371d73077ca - md5: b8e1901ef9a215fc41ecfb6bef7e0943 + url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda + sha256: c84d012a245171f3ed666a8bf9319580c269b7843ffa79f26468842da3abd5df + md5: 2ca8e6dbc86525c8b95e3c0ffa26442e depends: - - anyio >=3.0,<5.0 - - certifi + - python >=3.8 - h11 >=0.13,<0.15 - h2 >=3,<5 - - python >=3.8 - sniffio 1.* + - anyio >=3.0,<5.0 + - certifi license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/httpcore?source=hash-mapping - size: 45711 - timestamp: 1727821031365 + purls: [] + size: 48959 + timestamp: 1731707562362 - kind: conda name: httpx version: 0.27.2 @@ -3315,18 +3346,17 @@ packages: timestamp: 1710262791393 - kind: conda name: jupyterlab - version: 4.3.0 - build: pyhd8ed1ab_0 + version: 4.3.1 + build: pyhff2d567_0 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.0-pyhd8ed1ab_0.conda - sha256: a27e5227a11c2ce7b299d02f2f2c99713df4c9bb0e78ddd6cf8ffc6a77593dc2 - md5: 4e51411b565d07405d7d3245b9a3b8c1 + url: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.1-pyhff2d567_0.conda + sha256: ff1035eb0020dbaf4e332ef4b81a7068b595dfc57dde3313e9c4a37583772644 + md5: b4f3d579fc21a44518d52c52507461b4 depends: - async-lru >=1.0.0 - httpx >=0.25.0 - importlib-metadata >=4.8.3 - - importlib_resources >=1.4 - ipykernel >=6.5.0 - jinja2 >=3.0.3 - jupyter-lsp >=2.0.0 @@ -3335,7 +3365,7 @@ packages: - jupyterlab_server >=2.27.1,<3 - notebook-shim >=0.2 - packaging - - python >=3.8 + - python >=3.9 - setuptools >=40.1.0 - tomli >=1.2.2 - tornado >=6.2.0 @@ -3344,8 +3374,8 @@ packages: license_family: BSD purls: - pkg:pypi/jupyterlab?source=hash-mapping - size: 7327279 - timestamp: 1730308848803 + size: 7101932 + timestamp: 1731776859245 - kind: conda name: jupyterlab_pygments version: 0.3.0 @@ -4167,9 +4197,9 @@ packages: timestamp: 1704317789138 - kind: pypi name: med-imagetools - version: 1.6.0rc2 + version: 1.6.0 path: . - sha256: af86b352816ef552a165800d6bdc7bf67610f6a92c61fb33dd77536cd8469f50 + sha256: fa37e4dc0853cfaa342913ba426bdf01b8a6ce9848ac46344075aac35701fe8d requires_dist: - h5py>=3.11.0,<4 - joblib>=1.4.2,<2 @@ -4185,6 +4215,8 @@ packages: - pyyaml>=6.0.1,<7 - dill>=0.3.8,<1 - attrs>=23.2.0 + - structlog>=24.0,<25 + - click>=8.1,<9 - torch ; extra == 'torch' - torchio ; extra == 'torch' - pyvis ; extra == 'debug' @@ -4264,6 +4296,67 @@ packages: - pkg:pypi/multidict?source=hash-mapping size: 55968 timestamp: 1729065664275 +- kind: conda + name: mypy + version: 1.13.0 + build: py312h0bf5046_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.13.0-py312h0bf5046_0.conda + sha256: 29ee058cff0242c843452aceb9a04a1ffb79e979bf60134318ade1d6e3d508be + md5: a214c5fa30882205c822cc40fdde84f5 + depends: + - __osx >=11.0 + - mypy_extensions >=1.0.0 + - psutil >=4.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.1.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + size: 9891567 + timestamp: 1729644203196 +- kind: conda + name: mypy + version: 1.13.0 + build: py312h66e93f0_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.13.0-py312h66e93f0_0.conda + sha256: fbd1a5ac0e0e0fb16ab65395fb9b6d86aa9fb1e32689df13ee45829294a7ec57 + md5: 824a5a98260d4ee4d12fcad92d153c47 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - mypy_extensions >=1.0.0 + - psutil >=4.0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.1.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + size: 18799862 + timestamp: 1729644961295 +- kind: conda + name: mypy_extensions + version: 1.0.0 + build: pyha770c72_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + sha256: f240217476e148e825420c6bc3a0c0efb08c0718b7042fae960400c02af858a3 + md5: 4eccaeba205f0aed9ac3a9ea58568ca3 + depends: + - python >=3.5 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy-extensions?source=hash-mapping + size: 10492 + timestamp: 1675543414256 - kind: conda name: nbclient version: 0.10.0 @@ -4553,20 +4646,21 @@ packages: - kind: conda name: packaging version: '24.2' - build: pyhd8ed1ab_0 + build: pyhff2d567_1 + build_number: 1 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_0.conda - sha256: 0f8273bf66c2a5c1de72312a509deae07f163bb0ae8de8273c52e6fe945a0850 - md5: c16469afe1ec91aaafcf4bea966c0465 + url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhff2d567_1.conda + sha256: 74843f871e5cd8a1baf5ed8c406c571139c287141efe532f8ffbdafa3664d244 + md5: 8508b703977f4c4ada34d657d051972c depends: - python >=3.8 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/packaging?source=hash-mapping - size: 60345 - timestamp: 1731457074006 + size: 60380 + timestamp: 1731802602808 - kind: pypi name: pandas version: 2.2.3 @@ -6060,22 +6154,22 @@ packages: requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' - kind: conda name: python-dateutil - version: 2.9.0 - build: pyhd8ed1ab_0 + version: 2.9.0.post0 + build: pyhff2d567_0 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - sha256: f3ceef02ac164a8d3a080d0d32f8e2ebe10dd29e3a685d240e38b3599e146320 - md5: 2cf4264fffb9e6eff6031c5b6884d61c + url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_0.conda + sha256: 3888012c5916efaef45d503e3e544bbcc571b84426c1bb9577799ada9efefb54 + md5: b6dfd90a2141e573e4b6a81630b56df5 depends: - - python >=3.7 + - python >=3.9 - six >=1.5 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/python-dateutil?source=hash-mapping - size: 222742 - timestamp: 1709299922152 + size: 221925 + timestamp: 1731919374686 - kind: conda name: python-fastjsonschema version: 2.20.0 @@ -6594,12 +6688,12 @@ packages: timestamp: 1730922974629 - kind: conda name: ruff - version: 0.7.3 + version: 0.7.4 build: py312h2156523_0 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.7.3-py312h2156523_0.conda - sha256: 313110b1c431fe61cb3fe5e1cc4f1ffce905e95aab559055d1a86039c8ea7e35 - md5: ed126dd09a1f2081996c5b71423a2b80 + url: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.7.4-py312h2156523_0.conda + sha256: 41424ae6a027f433d259aa384b29fa2fb8e366f5080e93179c1d228ba8e6bd83 + md5: 025594b21ff040de6d98e6b1ef699185 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -6612,16 +6706,16 @@ packages: license_family: MIT purls: - pkg:pypi/ruff?source=hash-mapping - size: 7767902 - timestamp: 1731084470644 + size: 7788530 + timestamp: 1731708419964 - kind: conda name: ruff - version: 0.7.3 + version: 0.7.4 build: py312h5d18b81_0 subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.7.3-py312h5d18b81_0.conda - sha256: 2b6d54bd2f8178635ad8ed91bac64d4e17855e0dac5d0d08557682d9c164bc2c - md5: de5609bb3e7206efea3c0fce458d9c8f + url: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.7.4-py312h5d18b81_0.conda + sha256: fddde6d050fa51295541a14ae59dcd9b5f090d0cbc8159ee569d2996b8871e6d + md5: e461924d00bf42f3bb3ed3c731fb9a01 depends: - __osx >=11.0 - libcxx >=18 @@ -6634,8 +6728,8 @@ packages: license_family: MIT purls: - pkg:pypi/ruff?source=hash-mapping - size: 6857509 - timestamp: 1731084713907 + size: 6888167 + timestamp: 1731709556409 - kind: pypi name: scikit-image version: 0.24.0 @@ -7722,21 +7816,21 @@ packages: timestamp: 1712585504123 - kind: conda name: setuptools - version: 75.3.0 - build: pyhd8ed1ab_0 + version: 75.5.0 + build: pyhff2d567_0 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.3.0-pyhd8ed1ab_0.conda - sha256: a36d020b9f32fc3f1a6488a1c4a9c13988c6468faf6895bf30ca69521a61230e - md5: 2ce9825396daf72baabaade36cee16da + url: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.5.0-pyhff2d567_0.conda + sha256: 54dcf5f09f74f69641e0063bc695b38340d0349fa8371b1f2ed0c45c5b2fd224 + md5: ade63405adb52eeff89d506cd55908c0 depends: - - python >=3.8 + - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/setuptools?source=hash-mapping - size: 779561 - timestamp: 1730382173961 + size: 772480 + timestamp: 1731707561164 - kind: conda name: shellingham version: 1.5.4 @@ -7869,6 +7963,37 @@ packages: - pkg:pypi/stack-data?source=hash-mapping size: 26205 timestamp: 1669632203115 +- kind: pypi + name: structlog + version: 24.4.0 + url: https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl + sha256: 597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610 + requires_dist: + - freezegun>=0.2.8 ; extra == 'dev' + - mypy>=1.4 ; extra == 'dev' + - pretend ; extra == 'dev' + - pytest-asyncio>=0.17 ; extra == 'dev' + - pytest>=6.0 ; extra == 'dev' + - rich ; extra == 'dev' + - simplejson ; extra == 'dev' + - twisted ; extra == 'dev' + - cogapp ; extra == 'docs' + - furo ; extra == 'docs' + - myst-parser ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-notfound-page ; extra == 'docs' + - sphinxcontrib-mermaid ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - twisted ; extra == 'docs' + - freezegun>=0.2.8 ; extra == 'tests' + - pretend ; extra == 'tests' + - pytest-asyncio>=0.17 ; extra == 'tests' + - pytest>=6.0 ; extra == 'tests' + - simplejson ; extra == 'tests' + - mypy>=1.4 ; extra == 'typing' + - rich ; extra == 'typing' + - twisted ; extra == 'typing' + requires_python: '>=3.8' - kind: conda name: terminado version: 0.18.1 @@ -8175,6 +8300,39 @@ packages: - pkg:pypi/types-python-dateutil?source=hash-mapping size: 21765 timestamp: 1727940339297 +- kind: conda + name: types-pytz + version: 2024.2.0.20241003 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/types-pytz-2024.2.0.20241003-pyhd8ed1ab_0.conda + sha256: 6e045899904f488888dbed49f44e3ede438e88db9523ec61289fb4fdef4a53b8 + md5: 42775c62ac0671b0d700c754256d5c19 + depends: + - python >=3.6 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/types-pytz?source=hash-mapping + size: 18634 + timestamp: 1727940306315 +- kind: conda + name: types-tqdm + version: 4.66.0.20240417 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/types-tqdm-4.66.0.20240417-pyhd8ed1ab_0.conda + sha256: aeb2a8542e111e5313f31f5c59fdf2fdc2a9a8d8320edb2611d6965c2313d3de + md5: 497bb7a2b7c9ebb6911bac629edb33e8 + depends: + - python >=3.7 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/types-tqdm?source=hash-mapping + size: 25708 + timestamp: 1713339105137 - kind: pypi name: typing-extensions version: 4.12.2 @@ -8352,12 +8510,12 @@ packages: timestamp: 1632758637093 - kind: conda name: uv - version: 0.5.1 + version: 0.5.2 build: h0f3a69f_0 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.1-h0f3a69f_0.conda - sha256: 9bda0a65af5d2865cfe887dd90dbf04ed97bf6920a1c223396036e1c1f831f58 - md5: 2f0601933384868b7c34193247a1b167 + url: https://conda.anaconda.org/conda-forge/linux-64/uv-0.5.2-h0f3a69f_0.conda + sha256: e3d51d6700c474fe88296a4f18fd0deb9aa864773c7ce72a1b5321c56ad6f833 + md5: a7e3a6ce03c7d34f835f131529a099da depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -8366,16 +8524,16 @@ packages: - __glibc >=2.17 license: Apache-2.0 OR MIT purls: [] - size: 9804886 - timestamp: 1731134694659 + size: 9891266 + timestamp: 1731646632697 - kind: conda name: uv - version: 0.5.1 + version: 0.5.2 build: h668ec48_0 subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.1-h668ec48_0.conda - sha256: a9275898733ebbe40ad0b3a3f2ca7b7c8861309d48396e188f6ba433a7206de1 - md5: 96791a25fa69cd8db922861f257adccd + url: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.5.2-h668ec48_0.conda + sha256: 0335033079eaf1b73910f2b49b589e11dcb01cca1812702d257393f318d9ad9a + md5: 945d16b87d8552e7615c6e041b2bc260 depends: - __osx >=11.0 - libcxx >=18 @@ -8383,8 +8541,8 @@ packages: - __osx >=11.0 license: Apache-2.0 OR MIT purls: [] - size: 8674748 - timestamp: 1731135650399 + size: 8740188 + timestamp: 1731647271248 - kind: conda name: virtualenv version: 20.27.1 @@ -8532,48 +8690,48 @@ packages: timestamp: 1641346969816 - kind: conda name: yarl - version: 1.16.0 - build: py312h0bf5046_0 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.16.0-py312h0bf5046_0.conda - sha256: 2485912fa1c1acf51501519cd4d0253234f7e5b243093985b26ce167fdd67407 - md5: 81e954d5e6d3465d00f040b8ac1a8f67 + version: 1.17.2 + build: py312h66e93f0_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.17.2-py312h66e93f0_0.conda + sha256: 4e870938d29f38cd2aa43247efff6f99f6ecd8973735509122cd3167ccc22add + md5: 99518ade67138dcce4f2751b47ab5b00 depends: - - __osx >=11.0 + - __glibc >=2.17,<3.0.a0 - idna >=2.0 + - libgcc >=13 - multidict >=4.0 - propcache >=0.2.0 - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping - size: 138281 - timestamp: 1729798786022 + size: 150022 + timestamp: 1731927117182 - kind: conda name: yarl - version: 1.16.0 - build: py312h66e93f0_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.16.0-py312h66e93f0_0.conda - sha256: e9718b1f67f7359dee66995164ff734890066ad2eecb483b08f3f35b3f813c2d - md5: c3f4a6b56026c22319bf31514662b283 + version: 1.17.2 + build: py312hea69d52_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.17.2-py312hea69d52_0.conda + sha256: 43d85ffae29642b81e1ef4191560a7700911f3753078ab23248b8275952abcec + md5: e3d4600d565bac01340b12d3c4cba2b2 depends: - - __glibc >=2.17,<3.0.a0 + - __osx >=11.0 - idna >=2.0 - - libgcc >=13 - multidict >=4.0 - propcache >=0.2.0 - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping - size: 147820 - timestamp: 1729798523861 + size: 140245 + timestamp: 1731927409723 - kind: conda name: zeromq version: 4.3.5 @@ -8590,28 +8748,29 @@ packages: - libsodium >=1.0.20,<1.0.21.0a0 - libstdcxx >=13 license: MPL-2.0 + license_family: MOZILLA purls: [] size: 335400 timestamp: 1731585026517 - kind: conda name: zeromq version: 4.3.5 - build: h9f5b81c_6 - build_number: 6 + build: hc1bb282_7 + build_number: 7 subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h9f5b81c_6.conda - sha256: 5c5061c976141eccbbb2aec21483ddd10fd1df4fd9bcf638e3fd57b2bd85721f - md5: 84121ef1717cdfbecedeae70142706cc + url: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda + sha256: 9e585569fe2e7d3bea71972cd4b9f06b1a7ab8fa7c5139f92a31cbceecf25a8a + md5: f7e6b65943cb73bce0143737fded08f1 depends: - __osx >=11.0 - krb5 >=1.21.3,<1.22.0a0 - - libcxx >=17 + - libcxx >=18 - libsodium >=1.0.20,<1.0.21.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 280870 - timestamp: 1728363954972 + size: 281565 + timestamp: 1731585108039 - kind: conda name: zipp version: 3.21.0 diff --git a/pixi.toml b/pixi.toml index 89132af5..d76577ab 100644 --- a/pixi.toml +++ b/pixi.toml @@ -11,7 +11,7 @@ med-imagetools = { path = ".", editable = true } [environments] default = { features = [ "test", - "style", + "quality", "build", "dev", ], solve-group = "default" } @@ -26,6 +26,7 @@ ipython = "*" ipykernel = "*" jupyterlab = "*" + ############################################## PYTHON ############################################### [feature.py310.dependencies] @@ -46,8 +47,8 @@ pytest-xdist = "*" cmd = [ "pytest", # "--numprocesses=auto", - "-s", - "--verbose", + # "-s", + # "--verbose", "--cov=imgtools", "--cov-report=xml:coverage-report/coverage.xml", "--cov-config=config/coverage.toml", @@ -62,22 +63,31 @@ inputs = ["coverage-report/coverage.xml", "config/coverage.toml"] depends-on = ["test"] description = "Run pytest and generate coverage report" -############################################## STYLE ############################################### +############################################## QUALITY ############################################### # See config/ruff.toml for the configuration -[feature.style.dependencies] +[feature.quality.dependencies] ruff = ">=0.4.4" pre-commit = ">=3.7.1,<3.8" - -[feature.style.tasks] -lint.cmd = [ - "ruff", - "--config", - "config/ruff.toml", - "check", - "src", -] -lint.inputs = ["config/ruff.toml", "src"] -lint.description = "Run ruff check" +mypy = ">=1.13.0,<2" +types-pytz = ">=2024.2.0.20241003,<2025" +types-tqdm = ">=4.66.0.20240417,<5" + +[feature.quality.tasks] +ruff-check.cmd = ["ruff", "--config", "config/ruff.toml", "check", "src"] +ruff-check.inputs = ["config/ruff.toml", "src"] +ruff-check.description = "Run ruff check" + +ruff-format.cmd = ["ruff", "--config", "config/ruff.toml", "format", "src"] +ruff-format.inputs = ["config/ruff.toml", "src"] +ruff-format.depends_on = ["ruff-check"] +ruff-format.description = "Run ruff format, run check first" + +lint.depends_on = ["ruff-format", "ruff-check"] +lint.description = "Run ruff check and format" + +type-check.cmd = ["mypy", "--config-file", "config/mypy.ini"] +type-check.inputs = ["config/mypy.ini", "src"] +type-check.description = "Run mypy type check." #################################### RELEASE & BUILD ############################################### diff --git a/pyproject.toml b/pyproject.toml index 16c325bf..5aed07b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ dependencies = [ "pyyaml>=6.0.1,<7", "dill>=0.3.8,<1", "attrs>=23.2.0", + "structlog>=24.0,<25", + "click>=8.1,<9", ] classifiers = [ @@ -53,7 +55,6 @@ debug = ["pyvis"] # Entry points for CLI commands [project.scripts] autopipeline = "imgtools.autopipeline:main" -betapipeline = "imgtools.autopipeline_refactored:main" [build-system] build-backend = "hatchling.build" diff --git a/src/imgtools/__init__.py b/src/imgtools/__init__.py index 2d4460cc..df44d337 100644 --- a/src/imgtools/__init__.py +++ b/src/imgtools/__init__.py @@ -1,5 +1 @@ -from . import io, ops, utils, pipeline - -__all__ = ["io", "ops", "utils", "pipeline"] - __version__ = "1.6.0" \ No newline at end of file diff --git a/src/imgtools/autopipeline.py b/src/imgtools/autopipeline.py index 529201a7..57fcb5fb 100644 --- a/src/imgtools/autopipeline.py +++ b/src/imgtools/autopipeline.py @@ -15,6 +15,8 @@ from imgtools.pipeline import Pipeline from imgtools.utils.nnunet import generate_dataset_json, markdown_report_images from imgtools.utils.args import parser +from imgtools.logging import get_logger + from joblib import Parallel, delayed from imgtools.modules import Segmentation from sklearn.model_selection import train_test_split @@ -26,6 +28,9 @@ ############################################################### +logger = get_logger(level=os.environ.get('IMGTOOLS_LOG_LEVEL', "INFO")) + + class AutoPipeline(Pipeline): """Example processing pipeline for the RADCURE dataset. This pipeline loads the CT images and structure sets, re-samples the images, @@ -164,9 +169,7 @@ def __init__(self, if not os.path.exists(self.output_directory): os.makedirs(self.output_directory) all_nnunet_folders = glob.glob(pathlib.Path(self.output_directory, "*", " ").as_posix()) - # print(all_nnunet_folders) numbers = [int(os.path.split(os.path.split(folder)[0])[1][4:7]) for folder in all_nnunet_folders if os.path.split(os.path.split(folder)[0])[1].startswith("Task")] - # print(numbers, continue_processing) if (len(numbers) == 0 and continue_processing) or not continue_processing or not os.path.exists(pathlib.Path(self.output_directory, f"Task{max(numbers)}_{study_name}", ".temp").as_posix()): available_numbers = list(range(500, 1000)) for folder in all_nnunet_folders: @@ -612,7 +615,6 @@ def save_data(self): if self.is_nnunet: # dataset.json for nnunet and .sh file to run to process it imagests_path = pathlib.Path(self.output_directory, "imagesTs").as_posix() images_test_location = imagests_path if os.path.exists(imagests_path) else None - # print(self.existing_roi_indices) generate_dataset_json(pathlib.Path(self.output_directory, "dataset.json").as_posix(), pathlib.Path(self.output_directory, "imagesTr").as_posix(), images_test_location, @@ -694,6 +696,7 @@ def run(self): # not supported yet, since they cannot be pickled if os.path.exists(self.output_df_path) and not self.overwrite: print("Dataset already processed...") + logger.info("Dataset already processed...") shutil.rmtree(pathlib.Path(self.output_directory, ".temp").as_posix()) else: Parallel(n_jobs=self.n_jobs, verbose=verbose, require='sharedmem')( @@ -720,17 +723,17 @@ def main(): with open(pathlib.Path(args.output_directory, ".temp", "init_parameters.pkl").as_posix(), "rb") as f: args_dict = dill.load(f) except: - print("Could not resume processing. Starting processing from the beginning.") - - print('initializing AutoPipeline...') + logger.info("Could not resume processing. Starting processing from the beginning.") + logger.debug("Starting main:", args=args_dict) + logger.info('Initializing AutoPipeline...') pipeline = AutoPipeline(**args_dict) if not args.dry_run: - print('starting AutoPipeline...') + logger.info('Starting AutoPipeline...') pipeline.run() - print('finished AutoPipeline!') + logger.info('Finished AutoPipeline!') else: - print('dry run complete, no processing done') + logger.info('Dry run complete, no processing done') """Print general summary info""" @@ -738,13 +741,13 @@ def main(): * dataset.json can be found at /path/to/dataset/json * You can train nnU-Net by cloning /path/to/nnunet/repo and run `nnUNet_plan_and_preprocess -t taskID` to let the nnU-Net package prepare """ - print(f"Outputted data to {args.output_directory}") + logger.info(f"Outputted data to {args.output_directory}") csv_path = pathlib.Path(args.output_directory, "dataset.csv").as_posix() - print(f"Dataset info found at {csv_path}") + logger.info(f"Dataset info found at {csv_path}") if args.nnunet: json_path = pathlib.Path(args.output_directory, "dataset.json").as_posix() - print(f"dataset.json for nnU-net can be found at {json_path}") - print("You can train nnU-net by cloning https://github.com/MIC-DKFZ/nnUNet/ and run `nnUNet_plan_and_preprocess -t taskID` to let the nnU-Net package prepare") + logger.info(f"dataset.json for nnU-net can be found at {json_path}") + logger.info("You can train nnU-net by cloning https://github.com/MIC-DKFZ/nnUNet/ and run `nnUNet_plan_and_preprocess -t taskID` to let the nnU-Net package prepare") if __name__ == "__main__": diff --git a/src/imgtools/logging/__init__.py b/src/imgtools/logging/__init__.py new file mode 100644 index 00000000..77b94128 --- /dev/null +++ b/src/imgtools/logging/__init__.py @@ -0,0 +1,388 @@ +""" +Logging setup using structlog for path prettification, call information formatting, +and timestamping in Eastern Standard Time (EST). + +This module provides a flexible and configurable logging framework with support for +human-readable console output and machine-parseable JSON logs. + +Usage +----- +Basic usage: + >>> from imgtools.logging import logger + >>> logger.info('This is an info message', extra_field='extra_value') + +Custom configuration: + >>> from imgtools.logging import get_logger, logging_manager + + Change log level + + >>> logger = get_logger(level='DEBUG') + + Enable JSON logging + + >>> logger = logging_manager.configure_logging( + ... json_logging=True, # Enable JSON output + ... level='DEBUG', # Set logging level + ... ) + +Configuration +------------- +Environment variables: + IMGTOOLS_LOG_LEVEL : str, optional + Default log level. Defaults to 'INFO'. + IMGTOOLS_JSON_LOGGING : str, optional + Enable JSON logging. Defaults to 'false'. + +Output formats: + - JSON: Machine-parseable logs written to `imgtools.log`. + - Console: Human-readable logs with color-coded levels. + +Log Levels: + - DEBUG: Detailed information for debugging. + - INFO: General operational information. + - WARNING: Minor issues that don't affect operation. + - ERROR: Serious issues that affect operation. + - CRITICAL: Critical issues requiring immediate attention. + +Classes +------- +LoggingManager + Manages the configuration and initialization of the logger. + +Functions +--------- +get_logger(level: str = 'INFO') -> logging.Logger + Retrieve a logger instance with the specified log level. +""" + +import json as jsonlib +import logging.config +import os +from pathlib import Path +from typing import TYPE_CHECKING, Dict, List + +import structlog +from structlog.processors import CallsiteParameter, CallsiteParameterAdder + +from imgtools.logging.processors import ( + CallPrettifier, + ESTTimeStamper, + PathPrettifier, +) + +if TYPE_CHECKING: + from structlog.typing import Processor + +DEFAULT_LOG_LEVEL = 'INFO' +LOG_DIR_NAME = '.imgtools/logs' +DEFAULT_LOG_FILENAME = 'imgtools.log' +VALID_LOG_LEVELS = {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'} + + +class LoggingManager: + """ + Manages the configuration and initialization of a structured logger. + + This class provides flexible options for configuring log levels, formats, + and output destinations. Logs can be human-readable or in JSON format for + automated systems. + + Parameters + ---------- + name : str + Name of the logger instance. + level : str, optional + Log level for the logger. Defaults to the environment variable + 'IMGTOOLS_LOG_LEVEL' or 'INFO'. + json_logging : bool, optional + Whether to enable JSON logging. Defaults to the environment variable + 'IMGTOOLS_JSON_LOGGING' or `False`. + base_dir : Path, optional + Base directory for relative path prettification. Defaults to the + current working directory. + + Attributes + ---------- + name : str + Name of the logger instance. + base_dir : Path + Base directory for path prettification. + level : str + Log level for the logger. + json_logging : bool + Whether JSON logging is enabled. + + Methods + ------- + get_logger() + Retrieve the configured logger instance. + configure_logging(json_logging=None, level=None) + Dynamically adjust logging settings. + + Raises + ------ + ValueError + If an invalid log level is provided. + RuntimeError + If the log directory or file cannot be created. + + Examples + -------- + Initialize with default settings: + >>> manager = LoggingManager(name='mylogger') + >>> logger = manager.get_logger() + >>> logger.info('Info message') + + Enable JSON logging: + >>> manager = LoggingManager(name='mylogger', json_logging=True) + >>> logger = manager.get_logger() + """ + + def __init__( + self, + name: str, + level: str = os.environ.get('IMGTOOLS_LOG_LEVEL', DEFAULT_LOG_LEVEL), + json_logging: bool = os.getenv('IMGTOOLS_JSON_LOGGING', 'false').lower() == 'true', + base_dir: Path | None = None, + ) -> None: + self.name = name + self.base_dir = base_dir or Path.cwd() + self.level = level.upper() + self.json_logging = json_logging + self._initialize_logger() + + def _setup_json_logging(self) -> str: + """ + Set up the logging configuration for JSON output. + + Ensures that the log directory exists and the log file is writable. + + Returns + ------- + str + The path to the JSON log file. + + Raises + ------ + PermissionError + If the log file is not writable. + RuntimeError + If the log directory cannot be created. + """ + try: + log_dir = self.base_dir / LOG_DIR_NAME + log_dir.mkdir(parents=True, exist_ok=True) + json_log_file = log_dir / DEFAULT_LOG_FILENAME + if json_log_file.exists() and not os.access(json_log_file, os.W_OK): + msg = f'Log file {json_log_file} is not writable' + raise PermissionError(msg) + except (PermissionError, OSError) as err: + msg = f'Failed to create log directory at {log_dir}: {err}' + raise RuntimeError(msg) from err + return str(json_log_file) + + def _create_base_logging_config(self, pre_chain: List) -> Dict: + """ + Create the basic logging configuration without JSON-specific settings. + + Parameters + ---------- + pre_chain : list + List of processors for structured logging. + + Returns + ------- + dict + Base logging configuration. + """ + return { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'console': { + '()': structlog.stdlib.ProcessorFormatter, + 'processors': [ + CallPrettifier(concise=True), + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + structlog.dev.ConsoleRenderer( + exception_formatter=structlog.dev.RichTracebackFormatter( + width=-1, show_locals=False + ), + ), + ], + 'foreign_pre_chain': pre_chain, + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'console', + }, + }, + 'loggers': { + self.name: { + 'handlers': ['console'], + 'level': self.level, + 'propagate': False, + }, + }, + } + + def _add_json_logging_config( + self, logging_config: Dict, pre_chain: List, json_log_file: str + ) -> Dict: + """ + Add JSON logging settings to the logging configuration. + + Parameters + ---------- + logging_config : dict + Existing logging configuration. + pre_chain : list + List of processors for structured logging. + json_log_file : str + Path to the JSON log file. + + Returns + ------- + dict + Updated logging configuration. + """ + json_formatter = { + 'json': { + '()': structlog.stdlib.ProcessorFormatter, + 'processors': [ + CallPrettifier(concise=False), + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + structlog.processors.dict_tracebacks, + structlog.processors.JSONRenderer(serializer=jsonlib.dumps, indent=2), + ], + 'foreign_pre_chain': pre_chain, + }, + } + json_handler = { + 'json': { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'json', + 'filename': json_log_file, + 'maxBytes': 10485760, + 'backupCount': 5, + }, + } + + logging_config['formatters'].update(json_formatter) + logging_config['handlers'].update(json_handler) + logging_config['loggers'][self.name]['handlers'].append('json') + return logging_config + + def _initialize_logger(self) -> None: + """ + Initialize the logger with the current configuration. + """ + pre_chain: List[Processor] = [ + structlog.stdlib.add_log_level, + ESTTimeStamper(), + structlog.stdlib.add_logger_name, + structlog.stdlib.PositionalArgumentsFormatter(), + CallsiteParameterAdder( + [ + CallsiteParameter.MODULE, + CallsiteParameter.FUNC_NAME, + CallsiteParameter.LINENO, + ] + ), + PathPrettifier(base_dir=self.base_dir), + structlog.stdlib.ExtraAdder(), + structlog.processors.StackInfoRenderer(), + ] + + logging_config = self._create_base_logging_config(pre_chain) + + if self.json_logging: + json_log_file = self._setup_json_logging() + logging_config = self._add_json_logging_config(logging_config, pre_chain, json_log_file) + + logging.config.dictConfig(logging_config) + structlog.configure( + processors=[ + *pre_chain, + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + def get_logger(self) -> structlog.stdlib.BoundLogger: + """ + Retrieve the logger instance. + + Returns + ------- + structlog.stdlib.BoundLogger + Configured logger instance. + """ + return structlog.get_logger(self.name) + + def configure_logging( + self, json_logging: bool = False, level: str = DEFAULT_LOG_LEVEL + ) -> structlog.stdlib.BoundLogger: + """ + Dynamically adjust logging settings. + + Parameters + ---------- + json_logging : bool, default=False + Enable or disable JSON logging. + level : str, optional + Set the log level. + + Returns + ------- + structlog.stdlib.BoundLogger + Updated logger instance. + + Raises + ------ + ValueError + If an invalid log level is specified. + """ + if level is not None: + if level not in VALID_LOG_LEVELS: + msg = f'Invalid logging level: {level}' + raise ValueError(msg) + self.level = level.upper() + + if json_logging is not None: + self.json_logging = json_logging + self._initialize_logger() + + return self.get_logger() + + +LOGGER_NAME = 'imgtools' +logging_manager = LoggingManager(LOGGER_NAME) +logger = logging_manager.configure_logging(level=DEFAULT_LOG_LEVEL) + + +def get_logger(level: str = 'INFO') -> structlog.stdlib.BoundLogger: + """ + Retrieve a logger with the specified log level. + + Parameters + ---------- + level : str + Desired logging level. + + Returns + ------- + logging.Logger + Configured logger instance. + """ + env_level = os.environ.get('IMGTOOLS_LOG_LEVEL', None) + if env_level != level and env_level is not None: + logger.warning( + f'Environment variable IMGTOOLS_LOG_LEVEL is {env_level} ' + f'but you are setting it to {level}' + ) + return logging_manager.configure_logging(level=level) diff --git a/src/imgtools/logging/__main__.py b/src/imgtools/logging/__main__.py new file mode 100644 index 00000000..e689862a --- /dev/null +++ b/src/imgtools/logging/__main__.py @@ -0,0 +1,71 @@ +import logging +import pathlib +import time + +import click +from tqdm import trange +from tqdm.contrib.logging import logging_redirect_tqdm + +from imgtools.logging import logger + + +@click.command() +def main() -> None: + """ + Create and configure an example structlog logger. + """ + logger.debug( + 'This is a debug message', + context_var1='value1', + context_var2='value2', + ) + logger.info( + 'This is an info message', + user='user123', + action='info_action', + path=pathlib.Path('/path/to/file.txt'), + ) + logger.warning( + 'This is a warning message', + file='file.txt', + line=42, + ) + logger.error( + 'This is an error message', + error_code=500, + module='main_module', + ) + logger.critical( + 'This is a critical message', + system='production', + severity='high', + ) + logger.fatal( + 'This is a fatal message', + reason='Unknown', + attempt=5, + ) + + # To use tqdm with logger, + # you need to get the logger from base logging instead of structlog + # which also works as normal + imgtools_logger = logging.getLogger('imgtools') + imgtools_logger.info('This is an imgtools logger message', extra={'foo': 'bar'}) + imgtools_logger.warning( + 'This is an imgtools logger warning message', extra={'warning': 'oh no'} + ) + + with logging_redirect_tqdm([imgtools_logger]): + for i in trange(4): + imgtools_logger.info(f'This is a tqdm logger message {i}') + time.sleep(0.5) + if i == 3: # noqa: PLR2004 + 1 / 0 # noqa: B018 + + +if __name__ == '__main__': + try: + main() + except Exception as e: + msg = 'An error occurred while running the example' + logger.exception(msg, exc_info=e) diff --git a/src/imgtools/logging/processors.py b/src/imgtools/logging/processors.py new file mode 100644 index 00000000..b2b418c2 --- /dev/null +++ b/src/imgtools/logging/processors.py @@ -0,0 +1,145 @@ +# Imports remain the same +import contextlib +import datetime +from pathlib import Path +from typing import Optional + +import pytz +from structlog.types import EventDict + + +class PathPrettifier: + """ + A processor to convert absolute paths to relative paths based on a base directory. + + Args: + base_dir (Optional[Path]): The base directory to which paths should be made relative. Defaults to the current working directory. + """ + + def __init__(self, base_dir: Optional[Path] = None) -> None: + self.base_dir = base_dir or Path.cwd() + + def __call__(self, _: object, __: object, event_dict: EventDict) -> EventDict: + """ + Process the event dictionary to convert Path objects to relative paths. + + Args: + _: Unused positional argument. + __: Unused positional argument. + event_dict (EventDict): The event dictionary containing log information. + + Returns: + EventDict: The modified event dictionary with relative paths. + """ + if not isinstance(event_dict, dict): + msg = 'event_dict must be a dictionary' + raise TypeError(msg) + + for key, path in event_dict.items(): + if isinstance(path, Path): + with contextlib.suppress(ValueError): + relative_path = path.relative_to(self.base_dir) + event_dict[key] = str(relative_path) + return event_dict + + +class JSONFormatter: + """ + A processor to format the event dictionary for JSON output. + """ + + def __call__(self, _: object, __: object, event_dict: EventDict) -> EventDict: + """ + Process the event dictionary to separate core fields from extra fields. + + Args: + _: Unused positional argument. + __: Unused positional argument. + event_dict (EventDict): The event dictionary containing log information. + + Returns: + EventDict: The modified event dictionary with core fields and extra fields separated. + """ + if not isinstance(event_dict, dict): + msg = 'event_dict must be a dictionary' + raise TypeError(msg) + + core_fields = {'event', 'level', 'timestamp', 'call', 'exception'} + extra_fields = {key: value for key, value in event_dict.items() if key not in core_fields} + for key in extra_fields: + del event_dict[key] + + event_dict['extra'] = extra_fields + return event_dict + + +class CallPrettifier: + """ + A processor to format call information in the event dictionary. + + Args: + concise (bool): Whether to use a concise format for call information. Defaults to True. + """ + + def __init__(self, concise: bool = True) -> None: + self.concise = concise + + def __call__(self, _: object, __: object, event_dict: EventDict) -> EventDict: + """ + Process the event dictionary to format call information. + + Args: + _: Unused positional argument. + __: Unused positional argument. + event_dict (EventDict): The event dictionary containing log information. + + Returns: + EventDict: The modified event dictionary with formatted call information. + """ + if not isinstance(event_dict, dict): + msg = 'event_dict must be a dictionary' + raise TypeError(msg) + + call = { + 'module': event_dict.pop('module', ''), + 'func_name': event_dict.pop('func_name', ''), + 'lineno': event_dict.pop('lineno', ''), + } + + event_dict['call'] = ( + f"{call['module']}.{call['func_name']}:{call['lineno']}" if self.concise else call + ) + return event_dict + + +class ESTTimeStamper: + """ + A processor to add a timestamp in Eastern Standard Time to the event dictionary. + + Args: + fmt (str): The format string for the timestamp. Defaults to "%Y-%m-%dT%H:%M:%S%z". + """ + + def __init__(self, fmt: str = '%Y-%m-%dT%H:%M:%S%z') -> None: + self.fmt = fmt + self.est = pytz.timezone('US/Eastern') + + def __call__(self, _: object, __: object, event_dict: EventDict) -> EventDict: + """ + Process the event dictionary to add a timestamp in Eastern Standard Time. + + Args: + _: Unused positional argument. + __: Unused positional argument. + event_dict (EventDict): The event dictionary containing log information. + + Returns: + EventDict: The modified event dictionary with the added timestamp. + """ + if not isinstance(event_dict, dict): + msg = 'event_dict must be a dictionary' + raise TypeError(msg) + + now = datetime.datetime.now(self.est) + event_dict['timestamp'] = now.strftime(self.fmt) + return event_dict diff --git a/src/imgtools/modules/datagraph.py b/src/imgtools/modules/datagraph.py index e69aac36..7eb17924 100644 --- a/src/imgtools/modules/datagraph.py +++ b/src/imgtools/modules/datagraph.py @@ -5,7 +5,7 @@ from functools import reduce import numpy as np import pandas as pd - +from imgtools.logging import logger class DataGraph: ''' @@ -29,7 +29,8 @@ class DataGraph: def __init__(self, path_crawl: str, edge_path: str = "./patient_id_full_edges.csv", - visualize: bool = False) -> None: + visualize: bool = False, + update: bool = False) -> None: ''' Parameters ---------- @@ -42,11 +43,15 @@ def __init__(self, self.df = pd.read_csv(path_crawl, index_col=0) self.edge_path = edge_path self.df_new = None - if os.path.exists(self.edge_path): - print("Edge table is already present. Loading the data...") + + if not os.path.exists(self.edge_path): + logger.info("Edge table not present. Forming the edge table based on the crawl data...") + self.form_graph() + elif not update: + logger.info("Edge table is already present. Loading the data...") self.df_edges = pd.read_csv(self.edge_path) else: - print("Edge table not present. Forming the edge table based on the crawl data...") + logger.info("Edge table present, but force updating...") self.form_graph() if visualize: self.visualize_graph() @@ -74,8 +79,6 @@ def form_graph(self): # Get all study ids # all_study = df_filter.study.unique() - start = time.time() - # Defining Master df to store all the Edge dataframes # self.df_master = [] @@ -85,15 +88,14 @@ def form_graph(self): # df_edge_patient = form_edge_study(df,all_study,i) self.df_edges = self._form_edges(self.df) # pd.concat(self.df_master, axis=0, ignore_index=True) - end = time.time() - print(f"\nTotal time taken: {end - start}") + self.df_edges.loc[self.df_edges.study_x.isna(),"study_x"] = self.df_edges.loc[self.df_edges.study_x.isna(), "study"] # dropping some columns self.df_edges.drop(columns=["study_y", "patient_ID_y", "series_description_y", "study_description_y", "study"],inplace=True) self.df_edges.sort_values(by="patient_ID_x", ascending=True) - print(f"Saving edge table in {self.edge_path}") + logger.info(f"Saving edge table in {self.edge_path}") self.df_edges.to_csv(self.edge_path, index=False) def visualize_graph(self): @@ -101,7 +103,7 @@ def visualize_graph(self): Generates visualization using Pyviz, a wrapper around visJS. The visualization can be found at datanet.html """ from pyvis.network import Network # type: ignore (PyLance) - print("Generating visualizations...") + logger.info("Generating visualizations...") data_net = Network(height='100%', width='100%', bgcolor='#222222', font_color='white') sources = self.df_edges["series_y"] diff --git a/src/imgtools/modules/structureset.py b/src/imgtools/modules/structureset.py index 828b58ab..ebf5f9a0 100644 --- a/src/imgtools/modules/structureset.py +++ b/src/imgtools/modules/structureset.py @@ -1,5 +1,4 @@ import re -from warnings import warn from typing import Dict, List, Optional, TypeVar import numpy as np @@ -8,8 +7,9 @@ from itertools import groupby from skimage.draw import polygon2mask -from .segmentation import Segmentation -from ..utils import physical_points_to_idxs +from imgtools.modules.segmentation import Segmentation +from imgtools.utils import physical_points_to_idxs +from imgtools.logging import logger T = TypeVar('T') @@ -44,8 +44,8 @@ def from_dicom_rtstruct(cls, rtstruct_path: str) -> 'StructureSet': for i, name in enumerate(roi_names): try: roi_points[name] = _get_roi_points(rtstruct, i) - except AttributeError: - warn(f"Could not get points for ROI {name} (in {rtstruct_path}).") + except AttributeError as ae: + logger.warning(f"Could not get points for ROI `{name}`.", rtstruct_path=rtstruct_path, error=ae) metadata = {} @@ -184,7 +184,7 @@ def to_segmentation(self, reference_image: sitk.Image, roi_names = [roi_names] if isinstance(roi_names, list): # won't this always trigger after the previous? labels = self._assign_labels(roi_names, roi_select_first) - print("labels:", labels) + logger.debug(f"Found {len(labels)} labels", labels=labels) all_empty = True for v in labels.values(): if v != []: diff --git a/src/imgtools/ops/ops.py b/src/imgtools/ops/ops.py index b06b005f..8aa970ec 100644 --- a/src/imgtools/ops/ops.py +++ b/src/imgtools/ops/ops.py @@ -1,14 +1,57 @@ -from typing import List, TypeVar, Sequence, Union, Tuple, Optional, Any - +import json +import os +import pathlib +import re +from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union +import time import numpy as np import SimpleITK as sitk -from .functional import * -from ..io import * -from ..utils import image_to_array, array_to_image, crawl, physical_points_to_idxs -from ..modules import map_over_labels -from ..modules import DataGraph - +# from ..io import * +from imgtools.io.loaders import ( + BaseLoader, + ImageCSVLoader, + ImageFileLoader, + read_dicom_auto, + read_image, +) +from imgtools.io.writers import ( + BaseSubjectWriter, + BaseWriter, + HDF5Writer, + ImageFileWriter, + MetadataWriter, + NumpyWriter, + SegNrrdWriter, +) +from imgtools.logging import logger + +# from ..modules import DataGraph +# from ..modules import map_over_labels +from imgtools.modules.datagraph import DataGraph +from imgtools.modules.segmentation import Segmentation, map_over_labels + +# from .functional import * +from imgtools.ops.functional import ( + bounding_box, + centroid, + clip_intensity, + crop, + crop_to_mask_bounding_box, + image_statistics, + min_max_scale, + resample, + resize, + rotate, + standard_scale, + window_intensity, + zoom, +) + +# from ..utils import image_to_array, array_to_image, crawl, physical_points_to_idxs +from imgtools.utils import crawl +from imgtools.utils.arrayutils import array_to_image +from imgtools.utils.imageutils import image_to_array, physical_points_to_idxs LoaderFunction = TypeVar('LoaderFunction') ImageFilter = TypeVar('ImageFilter') @@ -92,13 +135,14 @@ def __init__(self, tree_crawl_path = pathlib.Path(self.parent, ".imgtools", f"imgtools_{self.dataset_name}.json").as_posix() if not os.path.exists(df_crawl_path) or update: - print("Indexing the dataset...") + logger.debug("Output exists, force updating.", path=df_crawl_path) + logger.info("Indexing the dataset") db = crawl(self.dir_path, n_jobs = n_jobs) - print(f"Number of patients in the dataset: {len(db)}") + logger.info(f"Number of patients in the dataset: {len(db)}") else: - print("The dataset has already been indexed.") + logger.info("The dataset has already been indexed.") + - import json with open(tree_crawl_path, 'r') as f: tree_db = json.load(f) # currently unused, TO BE implemented in the future assert tree_db is not None, "There was no crawler output" # dodging linter @@ -107,15 +151,15 @@ def __init__(self, # ----- # Form the graph edge_path = pathlib.Path(self.parent, ".imgtools",f"imgtools_{self.dataset_name}_edges.csv").as_posix() + logger.info(f"Forming the graph based on the given modalities: {self.modalities}") graph = DataGraph(path_crawl=df_crawl_path, edge_path=edge_path, visualize=visualize) - print(f"Forming the graph based on the given modalities: {self.modalities}") self.df_combined = graph.parser(self.modalities) self.output_streams = [("_").join(cols.split("_")[1:]) for cols in self.df_combined.columns if cols.split("_")[0] == "folder"] self.column_names = [cols for cols in self.df_combined.columns if cols.split("_")[0] == "folder"] self.study_names = [cols for cols in self.df_combined.columns if cols.split("_")[0] == "study"] self.series_names = [cols for cols in self.df_combined.columns if cols.split("_")[0] == "series"] self.subseries_names = [cols for cols in self.df_combined.columns if cols.split("_")[0] == "subseries"] - print(f"There are {len(self.df_combined)} cases containing all {modalities} modalities.") + logger.info(f"There are {len(self.df_combined)} cases containing all {self.modalities} modalities.") self.readers = [read_dicom_auto for _ in range(len(self.output_streams))] @@ -159,33 +203,36 @@ def __init__(self, self.dir_path = dir_path self.modalities = modalities self.parent, self.dataset_name = os.path.split(self.dir_path) - + start = time.time() # CRAWLER # ------- # Checks if dataset has already been indexed # To be changed later - path_crawl = pathlib.Path(self.parent, ".imgtools", f"imgtools_{self.dataset_name}.csv").as_posix() - if not os.path.exists(path_crawl) or update: - print("Indexing the dataset...") + path_crawl = pathlib.Path(self.parent, ".imgtools", f"imgtools_{self.dataset_name}.csv") + if not path_crawl.exists() or update: + logger.debug("Output exists, force updating.", path=path_crawl) + logger.info("Indexing the dataset...") db = crawl(self.dir_path, n_jobs=n_jobs) - print(f"Number of patients in the dataset: {len(db)}") + logger.info(f"Number of patients in the dataset: {len(db)}") else: - print("The dataset has already been indexed.") + logger.warning("The dataset has already been indexed. Use --update to force update.") # GRAPH # ----- # Form the graph - edge_path = pathlib.Path(self.parent,".imgtools",f"imgtools_{self.dataset_name}_edges.csv").as_posix() - graph = DataGraph(path_crawl=path_crawl, edge_path=edge_path, visualize=visualize) - print(f"Forming the graph based on the given modalities: {self.modalities}") + edge_path = pathlib.Path(self.parent,".imgtools",f"imgtools_{self.dataset_name}_edges.csv") + logger.debug("Creating edge path", edge_path=edge_path) + graph = DataGraph(path_crawl=path_crawl.resolve(), edge_path=edge_path.as_posix(), visualize=visualize, update=update) + logger.info(f"Forming the graph based on the given modalities: {self.modalities}") self.df_combined = graph.parser(self.modalities) + self.output_streams = [("_").join(cols.split("_")[1:]) for cols in self.df_combined.columns if cols.split("_")[0] == "folder"] self.column_names = [cols for cols in self.df_combined.columns if cols.split("_")[0] == "folder"] self.series_names = [cols for cols in self.df_combined.columns if cols.split("_")[0] == "series"] - print(f"There are {len(self.df_combined)} cases containing all {modalities} modalities.") + logger.info(f"There are {len(self.df_combined)} cases containing all {self.modalities} modalities.") self.readers = [read_dicom_auto for _ in range(len(self.output_streams))] - + logger.info(f"Total time taken: {time.time() - start:.2f} seconds") loader = ImageCSVLoader(self.df_combined, colnames=self.column_names, seriesnames=self.series_names, @@ -1700,7 +1747,7 @@ def __call__(self, labels = self._assign_labels(self.roi_patterns, roi_select_first) else: raise ValueError(f"{self.roi_patterns} not expected datatype") - print("labels:", labels) + logger.debug(f"Found {len(labels)} labels", labels=labels) # removing empty labels from dictionary to prevent processing empty masks all_empty = True diff --git a/src/imgtools/utils/crawl.py b/src/imgtools/utils/crawl.py index 7b615347..2e15a060 100644 --- a/src/imgtools/utils/crawl.py +++ b/src/imgtools/utils/crawl.py @@ -7,7 +7,7 @@ from pydicom import dcmread from tqdm import tqdm from joblib import Parallel, delayed - +from imgtools.logging import logger def crawl_one(folder): folder_path = pathlib.Path(folder) @@ -180,9 +180,10 @@ def crawl(top, # top is the input directory in the argument parser from autotest.py database_list = [] folders = glob.glob(pathlib.Path(top, "*").as_posix()) - + logger.info(f"Crawling {len(folders)} folders in {top}") database_list = Parallel(n_jobs=n_jobs)(delayed(crawl_one)(pathlib.Path(top, folder).as_posix()) for folder in tqdm(folders)) + logger.info(f"Converting list to dictionary") # convert list to dictionary database_dict = {} for db in database_list: @@ -200,13 +201,17 @@ def crawl(top, except: pass + # save as json - with open(pathlib.Path(parent_imgtools, f'imgtools_{dataset}.json').as_posix(), 'w') as f: + json_path = pathlib.Path(parent_imgtools, f'imgtools_{dataset}.json') + logger.info(f"Saving as json to {json_path}") + with open(json_path.as_posix(), 'w') as f: json.dump(database_dict, f, indent=4) # save as dataframe - df = to_df(database_dict) df_path = pathlib.Path(parent_imgtools, f'imgtools_{dataset}.csv').as_posix() + logger.info(f"Saving as dataframe to {df_path}") + df = to_df(database_dict) df.to_csv(df_path) return database_dict diff --git a/tests/test_ops.py b/tests/test_ops.py index 15753d4b..e4f9906a 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -5,7 +5,7 @@ import numpy as np import h5py from imgtools.ops import * - +import copy @pytest.fixture(scope="session") def output_path(): curr_path = pathlib.Path(__file__).parent.parent.resolve()