diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6392437..0000000 --- a/.coveragerc +++ /dev/null @@ -1,33 +0,0 @@ -[run] - -include = numjuggler/* - -omit = - *_tab.py - -# branch = True - - - -[report] - -# Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain about missing debug-only code: - def __repr__ - if self\.debug - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - -ignore_errors = True -sort = Cover - diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..90ca8b1 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +source ./.venv/bin/activate diff --git a/.gitattributes b/.gitattributes index 0910289..ab9d90c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,15 +1,23 @@ # # Special handling for binary files -# See https://git-scm.com/book/tr/v2/Customizing-Git-Git-Attributes -# Install doc2txt https://sourceforge.net/projects/docx2txt/?source=navbar +# See https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes # -*.npz binary -*.xz binary -*.zip binary *.bz2 binary +*.dll binary +*.docx binary +*.duckdb binary *.gz binary *.npy binary -*.docx binary -#*.docx binary diff=word +*.npz binary *.png binary -#*.png diff=exif \ No newline at end of file +*.so binary +*.dll binary +*.pyd binary +*.sqlite binary +*.nc binary +*.parquet binary +*.xlsx binary +*.xz binary +*.zip binary +# SCM syntax highlighting & preventing 3-way merges +uv.lock merge=binary linguist-language=TOML linguist-generated=true diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 2694bdb..276af40 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -4,37 +4,54 @@ name: Python application on: + workflow_dispatch: + push: - branches: [ "master" ] + branches: ["master", "devel"] pull_request: - branches: [ "master" ] + branches: ["master", "devel"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }}-${{ github.ref != 'refs/heads/master' || github.sha }} + cancel-in-progress: true permissions: contents: read +env: + DEFAULT_PYTHON: "3.13" + UV_LINK_MODE: "copy" + jobs: build: - - runs-on: ubuntu-latest + name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.11", "3.12", "3.13", "3.14"] + include: + - os: windows-latest + python-version: "3.13" # Windows runner is too slow steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest six chardet - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - # - name: Lint with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - cd tests - pytest + - name: ✅ Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + - name: 📦 Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-dependency-glob: | + **/uv.lock + **/pyproject.toml + + - name: 🐍 Install the project + run: uv sync --all-extras --no-dev --group test + + - name: ✅ Run tests + run: | + uv run --no-dev --group test pytest --cov --cov-report term-missing:skip-covered --cov-report xml diff --git a/.gitignore b/.gitignore index 4ee4e3c..a5d1971 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Python setup folders build/ *.egg-info -numjuggler/version.py +src/numjuggler/version.py # Byte-compiled *.pyc @@ -14,6 +14,7 @@ numjuggler/version.py # Environments .cache/ .idea/ +.vscode/ # Dev.scripts results numjuggler.dot @@ -26,9 +27,12 @@ numjuggler.sfood2 .coverage htmlcov/ -# Travis tests -travis_tests/*.diff -travis_tests/*.res +# pyreverse +*_numjuggler.puml +*_numjuggler.html + +# docs +site +.docs-build + -# vs code -settings.json diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index c7bc4d7..0000000 --- a/.pylintrc +++ /dev/null @@ -1,407 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. This option is deprecated -# and it will be removed in Pylint 2.0. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=missing-docstring,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,import-star-module-level,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,long-suffix,old-ne-operator,old-octal-literal,suppressed-message,useless-suppression - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=132 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[BASIC] - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c3e193a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include help/*.rst diff --git a/install.sh b/install.sh index 75c9913..2334223 100755 --- a/install.sh +++ b/install.sh @@ -8,19 +8,15 @@ # # However, the numjuggler script is generated # automatically and contains current version numger. -# If this number is chaged in setup.py, the -# package should be reinstalled, in order +# The package should be reinstalled, in order # to update the numjuggler script. -# Uninstall previous version -pip uninstall numjuggler; -# install the package anew (this generates new script) -pip install -v -e . +tools/install/install-uv +tools/install/install-rustup +tools/install/install-just +just install # prepare new distribution files. These are not needed # for local installation, but might be useful for users -python setup.py clean -rm dist/* -python setup.py sdist - +uv build diff --git a/justfile b/justfile new file mode 100644 index 0000000..83ba2f0 --- /dev/null +++ b/justfile @@ -0,0 +1,216 @@ +# Examples: msgspec + +# Disable showing recipe lines before execution. +set quiet + +# Enable unstable features. +set unstable + +# Configure the shell for Windows. +set windows-shell := ["pwsh.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"] + +# We don't want to install any dev dependencies by default. +# export UV_NO_DEV := "true" + +alias t := test +alias c := check +set dotenv-load + +default_python := "3.13" +# TITLE := `uv version` +TITLE := `uv run --with setuptools_scm python -m setuptools_scm` +# VERSION := `uv version --short` +log := "warn" + +export JUST_LOG := log + +@_default: + just --list + +[group('dev')] +@version: + uv run --with setuptools_scm python -m setuptools_scm + +# create venv, if not exists +[group('dev')] +@venv: + [ -d .venv ] || uv venv --python {{ default_python }} + +# build package +[group('dev')] +@build: venv + uv build + +# check distribution with twine +[group('dev')] +@check-dist: build + uvx twine check dist/* + +# clean reproducible files +[group('dev')] +@clean: + #!/bin/bash + dirs_to_clean=( + ".benchmarks" + ".cache" + ".eggs" + ".mypy_cache" + ".pytest_cache" + ".ruff_cache" + ".venv" + "__pycache__" + "_build" + "build" + "dist" + "htmlcov" + ) + for d in "${dirs_to_clean[@]}"; do + echo "Remove $d" + find . -type d -name "$d" -exec rm -rf {} + + done + coverage erase + #pyreverse files + find . -type f -name "classes_numjuggler.*" -delete + find . -type f -name "packages_numjuggler.*" -delete + +# install package +[group('dev')] +@install: build + uv sync + +# clean build +[group('dev')] +@reinstall: clean install + +# Check style and test +[group('dev')] +@check: pre-commit test + +# Check style includeing mypy and pylint and test +# [group: 'dev'] +# @check-full: check mypy pylint pyright + +# # Bump project version +# [group: 'dev'] +# @bump *args="patch": +# uv version --bump {{args}} +# git commit -m "bump: version $(uv version)" pyproject.toml uv.lock + +# update tools +[group('dev')] +@up-tools: + pre-commit autoupdate + uv self update + pre-commit run -a + +# update dependencies +[group('dev')] +@up: + uv sync --upgrade --all-extras + pre-commit run -a + pytest + +# show dependencies +[group('dev')] +@tree *args: + uv tree --outdated {{ args }} + +# run pyupgrade +[group('dev')] +@pyupgrade *args="--py314-plus": + uvx pyupgrade {{ args }} # presumably, code is updated by ruff, just to check occasionally + +# test up to the first fail +[group('test')] +@test-ff *args: + uv run --no-dev --group test pytest -x {{ args }} + +# test with clean cache +[group('test')] +@test-cache-clear *args: + uv run --no-dev --group test pytest --cache-clear {{ args }} + +# test fast +[group('test')] +@test-fast *args: + uv run --no-dev --group test pytest -m "not slow" {{ args }} + +# run all the tests +[group('test')] +@test *args: + uv run --no-dev --group test pytest {{ args }} + +# run documentation tests +[group: 'test'] +@xdoctest *args: + uv run --no-dev --group test xdoctest --silent -c all src/numjuggler tools {{args}} + +# create coverage data +[group('test')] +@coverage: + uv run --no-dev --group test pytest --cov --cov-report=term-missing:skip-covered + +# coverage to html +[group('test')] +@coverage-html: + uv run --no-dev --group test pytest --cov --cov-report html:htmlcov + open htmlcov/index.html + +# check correct typing at runtime +# [group: 'test'] +# typeguard *args: +# @uv run --no-dev --group test --group typeguard pytest --typeguard-packages=src {{args}} + +# ruff check and format +[group('style')] +@ruff: + ruff check --fix src tests + ruff format src tests + +# Run pre-commit on all files +[group('style')] +@pre-commit: + uv run --no-dev --group style pre-commit run --show-diff-on-failure --color=always --all-files + +# Run mypy +# [group: 'lint'] +# @mypy: +# uv run --no-dev --group mypy mypy src tests docs/source/conf.py + +[group('style')] +@pylint: + uv run --no-dev --group style pylint --recursive=y --output-format colorized src tests + +# [group: 'lint'] +# @pyright: +# uv run --no-dev --group pyright pyright src tests + +# Lint with ty +[group('style')] +@ty: + uv run --no-dev --group style ty check + +# Draw UML diagrams +[group('style')] +@pyreverse: + uv run --no-dev --group style pyreverse --project numjuggler --colorized --output puml --output-directory .pyreverse --ignore data --source-roots src/**/*.py + +# Find code duplicates +[group('style')] +@symilar: + uv run --no-dev --group style symilar src/**/*.py + +# # Check rst-texts +# [group: 'docs'] +# @rstcheck: +# uv run --no-dev --group docs rstcheck --recursive *.rst docs +# +# build documentation +[group('docs')] +@docs-build *args: + uv run --no-dev --group docs mkdocs build -d .docs-build --theme readthedocs {{ args }} + +# browse and edit documentation with auto build +[group('docs')] +@docs: + uv run --no-dev --group docs mkdocs serve --dirty --watch docs --theme readthedocs diff --git a/numjuggler.clusters b/numjuggler.clusters deleted file mode 100644 index e69de29..0000000 diff --git a/numjuggler/__main__.py b/numjuggler/__main__.py deleted file mode 100644 index 5d6a810..0000000 --- a/numjuggler/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .main import main - -main() diff --git a/numjuggler/readme.rst b/numjuggler/readme.rst deleted file mode 100644 index 60b167b..0000000 --- a/numjuggler/readme.rst +++ /dev/null @@ -1,4 +0,0 @@ -Help messages are written in `main.py`_. - - -.. _`main.py`: main.py diff --git a/numjuggler/trial_map.txt b/numjuggler/trial_map.txt deleted file mode 100644 index dd01e49..0000000 --- a/numjuggler/trial_map.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Trial map for likefunc module -c 1 3 4: +10 -c 5--9: +20 -c 7 8: 1 -c : +100 - diff --git a/numjuggler/utils/PartialFormatter.py b/numjuggler/utils/PartialFormatter.py deleted file mode 100644 index 5f977de..0000000 --- a/numjuggler/utils/PartialFormatter.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import print_function, absolute_import, division - -import six -import string - - -def make_label(item): - if 0 < len(item): - return '{' + item + '}' - else: - return item - - -class SafeDict(dict): - - def __getitem__(self, item): - return super(SafeDict, self).__getitem__(item) or '' - - def __missing__(self, key): - return make_label(key) - - -# ' 1 #' -class PartialFormatter(string.Formatter): - - def format(*args, **kwargs): - if not args: - raise TypeError("descriptor 'format' of 'Formatter' object " - "needs an argument") - self, args = args[0], args[1:] # allow the "self" keyword be passed - try: - format_string, args = args[0], args[1:] # allow the "format_string" keyword be passed - except IndexError: - if 'format_string' in kwargs: - format_string = kwargs.pop('format_string') - else: - raise TypeError("format() missing 1 required positional " - "argument: 'format_string'") - return string.Formatter.vformat(self, format_string, args, SafeDict(kwargs)) - - def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0): - """Clone from Python3 version to fix Python2 mess with unnamed {} format specifiers""" - if recursion_depth < 0: - raise ValueError('Max string recursion exceeded') - result = [] - for literal_text, field_name, format_spec, conversion in \ - self.parse(format_string): - - # output the literal text - if literal_text: - result.append(literal_text) - - # if there's a field, output it - if field_name is not None: - # this is some markup, find the object and do - # the formatting - - # handle arg indexing when empty field_names are given. - if field_name == '': - if auto_arg_index is False: - raise ValueError('cannot switch from manual field ' - 'specification to automatic field ' - 'numbering') - field_name = str(auto_arg_index) - auto_arg_index += 1 - elif field_name.isdigit(): - if auto_arg_index: - raise ValueError('cannot switch from manual field ' - 'specification to automatic field ' - 'numbering') - # disable auto arg incrementing, if it gets - # used later on, then an exception will be raised - auto_arg_index = False - - # given the field_name, find the object it references - # and the argument it came from - obj, arg_used = self.get_field(field_name, args, kwargs) - used_args.add(arg_used) - - # do any conversion on the resulting object - obj = self.convert_field(obj, conversion) - - # expand the format spec, if needed - if six.PY2: - format_spec = self._vformat( - format_spec, args, kwargs, - used_args, recursion_depth-1, - auto_arg_index=auto_arg_index) - else: - format_spec, auto_arg_index = self._vformat( - format_spec, args, kwargs, - used_args, recursion_depth-1, - auto_arg_index=auto_arg_index) - # format the object and append to the result - result.append(self.format_field(obj, format_spec)) - - result = ''.join(result) - - if six.PY2: - return result - else: - return result, auto_arg_index diff --git a/numjuggler/utils/io.py b/numjuggler/utils/io.py deleted file mode 100644 index 20e6d13..0000000 --- a/numjuggler/utils/io.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import sys - -from .resource import Path -from contextlib import contextmanager - - -@contextmanager -def cd_temporarily(cd_to): - cur_dir = str(Path.cwd()) - try: - os.chdir(str(cd_to)) - yield - finally: - os.chdir(cur_dir) - - -@contextmanager -def resolve_fname_or_stream(fname_or_stream, mode="r"): - is_input = mode == 'r' - if fname_or_stream is None: - if is_input: - yield sys.stdin - else: - yield sys.stdout - elif is_input and hasattr(fname_or_stream, "read") or not is_input and hasattr(fname_or_stream, "write"): - yield fname_or_stream - else: - with open(fname_or_stream, mode=mode) as fid: - yield fid diff --git a/numjuggler/utils/resource.py b/numjuggler/utils/resource.py deleted file mode 100644 index 613a609..0000000 --- a/numjuggler/utils/resource.py +++ /dev/null @@ -1,7 +0,0 @@ -import inspect - -# noinspection PyCompatibility -try: - from pathlib import Path -except: - from pathlib2 import Path diff --git a/pylintrc.toml b/pylintrc.toml new file mode 100644 index 0000000..7ddfdc6 --- /dev/null +++ b/pylintrc.toml @@ -0,0 +1,555 @@ +[tool.pylint.main] +# Analyse import fallback blocks. This can be used to support both Python 2 and 3 +# compatible code, which means that the block might have code that exists only in +# one or another interpreter, leading to false positives when analysed. +# analyse-fallback-blocks = + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint in +# a server-like mode. +# clear-cache-post-run = + +# Always return a 0 (non-error) status code, even if lint errors are found. This +# is primarily useful in continuous integration scripts. +# exit-zero = + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list = ["duckdb", "mckit"] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +# extension-pkg-whitelist = + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +# fail-on = + +# Specify a score threshold under which the program will exit with error. +#fail-under = 10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +# from-stdin = + +# Files or directories to be skipped. They should be base names, not paths. +#ignore = ["CVS"] + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +# ignore-paths = + +# Files or directories matching the regular expression patterns are skipped. The +# regex matches against base names, not paths. The default value ignores Emacs +# file locks +#ignore-patterns = ["^\\.#"] + +# List of module names for which member attributes should not be checked and will +# not be imported (useful for modules/projects where namespaces are manipulated +# during runtime and thus existing member attributes cannot be deduced by static +# analysis). It supports qualified module names, as well as Unix pattern +# matching. +# ignored-modules = + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +# init-hook = +# init-hook='import sys, os; sys.path.insert(0, os.path.join(sys.prefix, sys.platlibdir, f"python{sys.version_info.major}.{sys.version_info.minor}", "site-packages"))' + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs = 0 + +# Control the amount of potential inferred values when inferring a single object. +# This can help the performance when dealing with large functions or complex, +# nested conditions. +#limit-inference-results = 100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins = [ "pylint_per_file_ignores" ] + +# Pickle collected data for later comparisons. +#persistent = true + +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +# prefer-stubs = + +# Minimum Python version to use for version dependent checks. Will default to the +# version used to run pylint. +#py-version = "3.13" + +# Discover python modules and packages in the file system subtree. +# recursive = + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +# source-roots = + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +#suggestion-mode = true + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +# unsafe-load-any-extension = + +#[tool.pylint.basic] +# Naming style matching correct argument names. +#argument-naming-style = "snake_case" + +# Regular expression matching correct argument names. Overrides argument-naming- +# style. If left empty, argument names will be checked with the set naming style. +# argument-rgx = + +# Naming style matching correct attribute names. +#attr-naming-style = "snake_case" + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +# attr-rgx = + +# Bad variable names which should always be refused, separated by a comma. +#bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +# bad-names-rgxs = + +# Naming style matching correct class attribute names. +#class-attribute-naming-style = "any" + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +# class-attribute-rgx = + +# Naming style matching correct class constant names. +#class-const-naming-style = "UPPER_CASE" + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +# class-const-rgx = + +# Naming style matching correct class names. +#class-naming-style = "PascalCase" + +# Regular expression matching correct class names. Overrides class-naming-style. +# If left empty, class names will be checked with the set naming style. +# class-rgx = + +# Naming style matching correct constant names. +#const-naming-style = "UPPER_CASE" + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming style. +# const-rgx = + +# Minimum line length for functions/classes that require docstrings, shorter ones +# are exempt. +#docstring-min-length = -1 + +# Naming style matching correct function names. +#function-naming-style = "snake_case" + +# Regular expression matching correct function names. Overrides function-naming- +# style. If left empty, function names will be checked with the set naming style. +# function-rgx = + +# Good variable names which should always be accepted, separated by a comma. +#good-names = ["i", "j", "k", "ex", "Run", "_"] + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +#good-names-rgxs = ["."] + +# Include a hint for the correct naming format with invalid-name. +# include-naming-hint = + +# Naming style matching correct inline iteration names. +#inlinevar-naming-style = "any" + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +# inlinevar-rgx = + +# Naming style matching correct method names. +#method-naming-style = "snake_case" + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +# method-rgx = + +# Naming style matching correct module names. +#module-naming-style = "snake_case" + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +# module-rgx = + +# Colon-delimited sets of names that determine each other's naming style when the +# name regexes allow several styles. +# name-group = + +# Regular expression which should only match function or class names that do not +# require a docstring. +#no-docstring-rgx = "^_" + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. These +# decorators are taken in consideration only for invalid-name. +#property-classes = ["abc.abstractproperty"] + +# Regular expression matching correct type alias names. If left empty, type alias +# names will be checked with the set naming style. +# typealias-rgx = + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +# typevar-rgx = + +# Naming style matching correct variable names. +#variable-naming-style = "snake_case" + +# Regular expression matching correct variable names. Overrides variable-naming- +# style. If left empty, variable names will be checked with the set naming style. +# variable-rgx = + +#[tool.pylint.classes] +# Warn about protected attribute access inside special methods +# check-protected-access-in-special-methods = + +# List of method names used to declare (i.e. assign) instance attributes. +#defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] + +# List of member names, which should be excluded from the protected access +# warning. +#exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] + +# List of valid names for the first argument in a class method. +#valid-classmethod-first-arg = ["cls"] + +# List of valid names for the first argument in a metaclass class method. +#valid-metaclass-classmethod-first-arg = ["mcs"] + +#[tool.pylint.design] +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +# exclude-too-few-public-methods = + +# List of qualified class names to ignore when counting class parents (see R0901) +# ignored-parents = + +# Maximum number of arguments for function / method. +#max-args = 5 + +# Maximum number of attributes for a class (see R0902). +#max-attributes = 7 + +# Maximum number of boolean expressions in an if statement (see R0916). +#max-bool-expr = 5 + +# Maximum number of branch for function / method body. +#max-branches = 12 + +# Maximum number of locals for function / method body. +#max-locals = 15 + +# Maximum number of parents for a class (see R0901). +#max-parents = 7 + +# Maximum number of positional arguments for function / method. +#max-positional-arguments = 5 + +# Maximum number of public methods for a class (see R0904). +#max-public-methods = 20 + +# Maximum number of return / yield for function / method body. +#max-returns = 6 + +# Maximum number of statements in function / method body. +#max-statements = 50 + +# Minimum number of public methods for a class (see R0903). +#min-public-methods = 2 + +#[tool.pylint.exceptions] +# Exceptions that will emit a warning when caught. +#overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] + +[tool.pylint.format] +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format = + +# Regexp for a line that is allowed to be longer than the limit. +#ignore-long-lines = "^\\s*(# )??$" + +# Number of spaces of indent required inside a hanging or continued line. +#indent-after-paren = 4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +#indent-string = " " + +# Maximum number of characters on a single line. +max-line-length = 120 + +# Maximum number of lines in a module. +#max-module-lines = 1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +# single-line-class-stmt = + +# Allow the body of an if to be on the same line as the test if there is no else. +# single-line-if-stmt = + +#[tool.pylint.imports] +# List of modules that can be imported at any level, not just the top level one. +# allow-any-import-level = + +# Allow explicit reexports by alias from a package __init__. +# allow-reexport-from-package = + +# Allow wildcard imports from modules that define __all__. +# allow-wildcard-with-all = + +# Deprecated modules which should not be used, separated by a comma. +#deprecated-modules = ["six"] + +# Output a graph (.gv or any supported image format) of external dependencies to +# the given file (report RP0402 must not be disabled). +# ext-import-graph = + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be disabled). +# import-graph = + +# Output a graph (.gv or any supported image format) of internal dependencies to +# the given file (report RP0402 must not be disabled). +# int-import-graph = + +# Force import order to recognize a module as part of the standard compatibility +# libraries. +# known-standard-library = + +# Force import order to recognize a module as part of a third party library. +known-third-party = ["enchant", "pytest"] + +# Couples of modules and preferred modules, separated by a comma. +# preferred-modules = + +#[tool.pylint.logging] +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +#logging-format-style = "new" + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +#logging-modules = ["logging"] + +[tool.pylint."messages control"] +# Only show warnings with the listed confidence levels. Leave empty to show all. +# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +#confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] +per-file-ignores = [ + "**/__main__.py:too-many-arguments", + "tests/**/*:C0114,C0116,W0621", +] + + +# Disable the message, report, category or checker with the given id(s). You can +# either give multiple identifiers separated by comma (,) or put this option +# multiple times (only on the command line, not in the configuration file where +# it should appear only once). You can also use "--disable=all" to disable +# everything first and then re-enable specific checks. For example, if you want +# to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +#disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "use-symbolic-message-instead", "wrong-import-order"] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where it +# should appear only once). See also the "--disable" option for examples. +# enable = + +#[tool.pylint.method_args] +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +#timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] + +#[tool.pylint.miscellaneous] +# List of note tags to take in consideration, separated by a comma. +#notes = ["FIXME", "XXX", "TODO"] + +# Regular expression of note tags to take in consideration. +# notes-rgx = + +#[tool.pylint.refactoring] +# Maximum number of nested blocks for function / method body +#max-nested-blocks = 5 + +# Complete name of functions that never returns. When checking for inconsistent- +# return-statements if a never returning function is called then it will be +# considered as an explicit return statement and no message will be printed. +#never-returning-functions = ["sys.exit", "argparse.parse_error"] + +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - ".join(items)``) +#suggest-join-with-non-empty-separator = true + +#[tool.pylint.reports] +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each category, +# as well as 'statement' which is the total number of statements analyzed. This +# score is used by the global evaluation report (RP0004). +#evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +# msg-template = + +# Set the output format. Available formats are: 'text', 'parseable', 'colorized', +# 'json2' (improved json format), 'json' (old json format), msvs (visual studio) +# and 'gitHub' (GitHub actions). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +# output-format = + +# Tells whether to display a full report or only the messages. +# reports = + +# Activate the evaluation score. +#score = true + +#[tool.pylint.similarities] +# Comments are removed from the similarity computation +#ignore-comments = true + +# Docstrings are removed from the similarity computation +#ignore-docstrings = true + +# Imports are removed from the similarity computation +#ignore-imports = true + +# Signatures are removed from the similarity computation +#ignore-signatures = true + +# Minimum lines number of a similarity. +#min-similarity-lines = 4 + +#[tool.pylint.spelling] +# Limits count of emitted suggestions for spelling mistakes. +#max-spelling-suggestions = 4 + +# Spelling dictionary name. No available dictionaries : You need to install both +# the python package and the system dependency for enchant to work. +# spelling-dict = + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +#spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" + +# List of comma separated words that should not be checked. +# spelling-ignore-words = + +# A path to a file that contains the private dictionary; one word per line. +# spelling-private-dict-file = + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +# spelling-store-unknown-words = + +#[tool.pylint.typecheck] +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +#contextmanager-decorators = ["contextlib.contextmanager"] + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +# generated-members = + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +# Tells whether to warn about missing members when the owner of the attribute is +# inferred to be None. +#ignore-none = true + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference can +# return multiple potential results while evaluating a Python object, but some +# branches might not be evaluated, which results in partial inference. In that +# case, it might be useful to still emit no-member and other checks for the rest +# of the inferred objects. +#ignore-on-opaque-inference = true + +# List of symbolic message names to ignore for Mixin members. +#ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +#ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "argparse.Namespace"] + +# Show a hint with possible names when a member name was not found. The aspect of +# finding the hint is based on edit distance. +#missing-member-hint = true + +# The maximum edit distance a name should have in order to be considered a +# similar match for a missing member name. +#missing-member-hint-distance = 1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +#missing-member-max-choices = 1 + +# Regex pattern to define which classes are considered mixins. +#mixin-class-rgx = ".*[Mm]ixin" + +# List of decorators that change the signature of a decorated function. +# signature-mutators = + +#[tool.pylint.variables] +# List of additional names supposed to be defined in builtins. Remember that you +# should avoid defining new builtins when possible. +# additional-builtins = + +# Tells whether unused global variables should be treated as a violation. +#allow-global-unused-variables = true + +# List of names allowed to shadow builtins +# allowed-redefined-builtins = + +# List of strings which can identify a callback function by name. A callback name +# must start or end with one of those strings. +#callbacks = ["cb_", "_cb"] + +# A regular expression matching the name of dummy variables (i.e. expected to not +# be used). +#dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" + +# Argument names that match this expression will be ignored. +#ignored-argument-names = "_.*|^ignored_|^unused_" + +# Tells whether we should check for unused import in __init__ files. +# init-import = + +# List of qualified module names which can have objects that can redefine +# builtins. +#redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"] diff --git a/pyproject.toml b/pyproject.toml index 7477710..106d7f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,99 @@ -# [tool.towncrier] -# directory = 'changes' - [build-system] - requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4'] +requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4'] + +[project] +name = "numjuggler" +dynamic = ["version"] +description = "MCNP input file renumbering tool" +readme = "README.md" +keywords = ["MCNP", "ITER", "PARSER", "RENUMBER"] +license = "GPL-3.0-or-later" +authors = [{ name = "A.Travleev", email = "anton.travleev@gmail.com" }] +requires-python = ">=3.11" +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Intended Audience :: Eduation", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Utilities", +] +dependencies = ["chardet>=7.4.3", "numpy>=2.4.4", "six>=1.17.0"] +urls."Bug Tracker" = "https://github.com/travleev/numjuggler/issues" +urls.Changelog = "https://github.com/travleev/numjuggler/releases" +urls.documentation = "https://numjuggler.readthedocs.io/" +urls.homepage = "https://github.com/travleev/numjuggler/" +urls.repository = "https://github.com/travleev/numjuggler/" + [tool.setuptools_scm] - write_to = 'numjuggler/version.py' +write_to = "src/numjuggler/version.py" + +[project.scripts] +numjuggler = "numjuggler.main:main" +[dependency-groups] +dev = [ + { include-group = "docs" }, + { include-group = "style" }, + { include-group = "test" }, +] +docs = [ + "mkdocs", + "mkdocs-include-markdown-plugin", + "mkdocs-material", + "mkdocstrings[python]>=0.29", + "pygments", + "griffe-public-wildcard-imports", +] +style = [ + { include-group = "pylint" }, + # { include-group = "mypy" }, + # { include-group = "pyright" }, + "pre-commit>=4.5.1", + "ruff>=0.15.11", + "ty>=0.0.31", +] +pylint = ["pylint", "pylint-per-file-ignores"] +test = [ + "pygments>=2.20.0", + "pytest>=9.0.3", + "pytest-cov>=7.1.0", + "pytest-mock>=3.15.1", + "xdoctest>=1.3.2", +] +[tool.coverage] +run.branch = true +run.omit = ["__main__.py", "**/__init__.py", "*_tab.py", "tools/*.py"] +run.parallel = true +run.source = ["src"] +paths.source = ["src"] +# Regexes for lines to exclude from consideration +report.exclude_lines = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if __name__ == .__main__.:", + # Don't complain if non-runnable code isn't run: + "if 0:", + "if self.debug", + "if TYPE_CHECKING:", + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", +] +report.fail_under = 30 # TODO @dvp: should be at least 80% +report.ignore_errors = true +report.omit = ["**/__init__.py", "**/types.py", "*_tab.py"] +report.show_missing = true +report.skip_covered = true +report.sort = "Cover" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index ae660f4..0000000 --- a/pytest.ini +++ /dev/null @@ -1,14 +0,0 @@ -[pytest] -#markers (linelist) markers for test functions -norecursedirs = .* build dist {arch} *.egg adhoc examples notebooks experiment dev_tests data out wrk -testpaths=tests -#usefixtures (args) list of default fixtures to be used with this project -python_files=test*.py # (args) glob-style file patterns for Python test module discovery -python_classes=Test* # (args) prefixes or glob names for Python test class discovery -python_functions=test* # (args) prefixes or glob names for Python test function and method discovery -#xfail_strict (bool) default for the strict parameter of xfail markers when not given explicitly (default: Fals -doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ALLOW_UNICODE ALLOW_BYTES -addopts = --ignore setup.py --ignore *_tab.py --doctest-modules --color=yes -# coverage doesn't allow to work with PyCharm debugger, run test_coverage.sh script to update coverage -#addopts = --ignore setup.py --doctest-modules --doctest-glob='*.rst' --cov-report term-missing --cov m2t -#minversion (string) minimally required pytest version diff --git a/pytest.toml b/pytest.toml new file mode 100644 index 0000000..9dd7b24 --- /dev/null +++ b/pytest.toml @@ -0,0 +1,48 @@ +[pytest] +minversion = "9.0" +cache_dir = ".cache/pytest" +strict = true +norecursedirs = [ + ".egg-info", + ".*", + "build", + "data", + "dist", + "docs/_build", + "docs/examples", + "htmlcov", + "notebooks", + "tools", + "wrk", +] +python_functions = ["test_*", "profile_*"] +addopts = [ + "-ra", + "-q", + "--cov-config", "pyproject.toml", + "--tb=short", + "--doctest-modules", + "--strict-markers", + "--ignore=setup.py", + "--failed-first", + "--xdoctest" +] +doctest_optionflags = [ + "ELLIPSIS", + "NORMALIZE_WHITESPACE", + "IGNORE_EXCEPTION_DETAIL", + "ALLOW_UNICODE", + "ALLOW_BYTES", + "NUMBER", +] +testpaths = ["tests", "src", "docs"] +markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"] +filterwarnings = [ + "error", + "ignore:.*not typechecking multipledispatch.dispatcher.*UserWarning", + 'ignore:.*io.FileIO \[closed\]', + 'ignore:.*Implicit None on return values:DeprecationWarning', +] +log_level = "INFO" +log_format = "%(asctime)s %(levelname)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" diff --git a/pytest_coverage b/pytest_coverage deleted file mode 100755 index 2454e9e..0000000 --- a/pytest_coverage +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -pytest --cov-report=term-missing:skip-covered --cov=numjuggler $* - diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..129cd67 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,138 @@ +target-version = "py313" +line-length = 100 +src = ["src", "tests"] +exclude = [ + ".cache", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pycache__", + "__pypackages__", + "_build", + "adhoc", + "build", + "data", + "dist", + "docs/source/conf.py", + "node_modules", + "notebooks", + "site-packages", + "tests/data", + "venv", + "wrk", +] + +[lint] +select = ["ALL"] +ignore = [ # TODO @dvp2015: TD is rather high, fix + "ANN", + "ARG", + "B", + "BLE", + "C", + "C901", + "COM812", + "D", + "D203", + "D211", + "D213", + "E", + "EM", + "E741", + "ERA", + "EXE", + "F", + "FBT", + "FIX", + "FURB", + "I", + "ICN", + "Q", + "N", + "PERF", + "PLC", + "PLR", + "PLW", + "PT", + "PTH", + "RET", + "RUF", + "S", + "SIM", + "SLF", + "T201", + "TD", + "TRY", + "UP", +] + +[lint.per-file-ignores] +"tests/*" = ["ANN", "D100", "D101", "D102", "D103", "D104", "PLR2004", "S101"] + +[lint.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 15 + +[lint.flake8-annotations] +mypy-init-return = true # skip return type for __init__() methods + +[lint.flake8-pytest-style] +parametrize-names-type = "csv" + +[lint.flake8-type-checking] +strict = true + +[lint.pep8-naming] +ignore-names = ["*eV*", "*He*"] + +# warning: The isort option `isort.split-on-trailing-comma` is incompatible +# with the formatter `format.skip-magic-trailing-comma=true` option. +# To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` +# or `format.skip-magic-trailing-comma=false`. + +[lint.isort] +known-first-party = ["src"] +known-third-party = [ + "mpl_toolkits", + "matplotlib", + "numpy", + "scipy", + "loguru", + "tqdm", + "dotenv", +] +default-section = "third-party" +lines-between-types = 1 +required-imports = ["from __future__ import annotations"] +case-sensitive = true +section-order = [ + "future", + "typing", + "standard-library", + "third-party", + "first-party", + "local-folder", + "testing", +] + +[lint.isort.sections] +"typing" = ["typing", "typing_extension"] +"testing" = ["tests"] + +[lint.pydocstyle] +convention = "numpy" + +[format] +docstring-code-format = true diff --git a/run_coverage b/run_coverage deleted file mode 100755 index d03db57..0000000 --- a/run_coverage +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -coverage run -m pytest tests -coverage report --show-missing --skip-covered -coverage html - diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a662eae..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[aliaces] -test=pytest diff --git a/setup.py b/setup.py deleted file mode 100644 index 3efd403..0000000 --- a/setup.py +++ /dev/null @@ -1,67 +0,0 @@ -import sys -from setuptools import setup, find_packages -from setuptools.command.test import test as test_command - - -# noinspection PyAttributeOutsideInit -class PyTest(test_command): - """ - See recomendations at https://docs.pytest.org/en/latest/goodpractices.html - """ - user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")] - - def initialize_options(self): - test_command.initialize_options(self) - self.pytest_args = "" - - def run_tests(self): - import shlex - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(shlex.split(self.pytest_args)) - sys.exit(errno) - - -packages = find_packages( - include=("numjuggler", "numjuggler.*",), -) - - -with open('README.md', 'r') as f: - long_description = f.read() - long_description_type = 'text/markdown' - - -setup( - name='numjuggler', - description='MCNP input file renumbering tool', - long_description=long_description, - long_description_content_type=long_description_type, - author='A.Travleev', - author_email='anton.travleev@gmail.com', - packages=packages, - use_scm_version=True, - setup_requires=['setuptools_scm'], - tests_require=['pytest', 'pytest-cov>=2.3.1'], - install_requires=[ - 'six', - 'pathlib2', - 'chardet', - ], - cmdclass={'test': PyTest}, - entry_points={'console_scripts': ['numjuggler = numjuggler.main:main']}, - # url='https://github.com/travleev/numjuggler', - url='https://numjuggler.readthedocs.io', - keywords='MCNP ITER PARSER RENUMBER'.split(), - classifiers=[ - 'Development Status :: 4 - Beta', # changed, when tests for all modes - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Education', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.13', - 'Topic :: Utilities', - ], -) diff --git a/numjuggler/__init__.py b/src/numjuggler/__init__.py similarity index 70% rename from numjuggler/__init__.py rename to src/numjuggler/__init__.py index defed47..54d5e9f 100644 --- a/numjuggler/__init__.py +++ b/src/numjuggler/__init__.py @@ -4,11 +4,11 @@ Package provides script mcnp.juggler. See its help for description. """ -from numjuggler.utils.PartialFormatter import PartialFormatter +from __future__ import annotations try: from .version import version except ImportError: # When cloned directly from git, version.py is not here - version = 'git.development' + version = "git.development" __version__ = version diff --git a/src/numjuggler/__main__.py b/src/numjuggler/__main__.py new file mode 100644 index 0000000..ace94d4 --- /dev/null +++ b/src/numjuggler/__main__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .main import main + +main() diff --git a/numjuggler/fmesh.py b/src/numjuggler/fmesh.py similarity index 97% rename from numjuggler/fmesh.py rename to src/numjuggler/fmesh.py index ce1e288..42142dd 100644 --- a/numjuggler/fmesh.py +++ b/src/numjuggler/fmesh.py @@ -3,6 +3,7 @@ """ Analyse fmesh cards. """ +from __future__ import annotations from numjuggler.parser import Card, are_close_vals @@ -63,7 +64,7 @@ def get_values(self): """ Redefine to include call to _analyse """ - super(FmeshCard, self).get_values() + super().get_values() self._analyse() def _analyse(self): @@ -80,7 +81,6 @@ def _analyse(self): setattr(self, t, tuple(_get_elements(tokens, float))) elif t[1:] in ('ints',): setattr(self, t, tuple(_get_elements(tokens, int))) - return def ints(self, d=0): """ diff --git a/help/cdens.md.txt b/src/numjuggler/help/cdens.md.txt similarity index 100% rename from help/cdens.md.txt rename to src/numjuggler/help/cdens.md.txt diff --git a/help/from_doc.sh b/src/numjuggler/help/from_doc.sh similarity index 100% rename from help/from_doc.sh rename to src/numjuggler/help/from_doc.sh diff --git a/help/index.md.txt b/src/numjuggler/help/index.md.txt similarity index 100% rename from help/index.md.txt rename to src/numjuggler/help/index.md.txt diff --git a/help/info.md.txt b/src/numjuggler/help/info.md.txt similarity index 100% rename from help/info.md.txt rename to src/numjuggler/help/info.md.txt diff --git a/help/map.md.txt b/src/numjuggler/help/map.md.txt similarity index 100% rename from help/map.md.txt rename to src/numjuggler/help/map.md.txt diff --git a/help/renum.md.txt b/src/numjuggler/help/renum.md.txt similarity index 100% rename from help/renum.md.txt rename to src/numjuggler/help/renum.md.txt diff --git a/numjuggler/likefunc.py b/src/numjuggler/likefunc.py similarity index 80% rename from numjuggler/likefunc.py rename to src/numjuggler/likefunc.py index 8f6bc15..1697ec7 100644 --- a/numjuggler/likefunc.py +++ b/src/numjuggler/likefunc.py @@ -1,19 +1,21 @@ -# -*- conding: utf-8 -*- - """ New implementation of mapping, where one can specify different functions for different ranges and separate values. """ -from __future__ import print_function +from __future__ import annotations + from collections import OrderedDict -from numjuggler.utils.io import resolve_fname_or_stream +from numjuggler.parser import get_numbers +from numjuggler.utils import resolve_fname_or_stream def trivial(x): """Trivial function returning its argument.""" return x + + # PEP 257: docstring should not be used for function signature. _mydoc # attribute must contain function expression, with argument denoted as `x` trivial._mydoc = "x" @@ -23,20 +25,23 @@ def const_func(c): """ Return function f(x) = c. """ + def f(x): return c - f._mydoc = '{}'.format(c) + + f._mydoc = f"{c}" return f def add_func(c): def f(x): return x + c - f._mydoc = 'x + {}'.format(c) + + f._mydoc = f"x + {c}" return f -class LikeFunctionBase(object): +class LikeFunctionBase: """ Base class for other like-function classes. @@ -67,7 +72,6 @@ def __init__(self, log=False): # String printed at begin str(self) self.doc = "" - return def __call__(self, x): res = self.get_value(x) @@ -89,15 +93,15 @@ def __str__(self): res.extend(self._str()) - res.append('other -> {}'.format(self.default._mydoc)) - return '\n'.join(res) + res.append(f"other -> {self.default._mydoc}") + return "\n".join(res) def write_log_as_map(self, t, fname_or_stream=None): if not self.log: raise ValueError("Cannon write log for unlogged mapping.") with resolve_fname_or_stream(fname_or_stream, "w") as fout: for nold, nnew in self.ld.items(): - print('{} {}: {}'.format(t, nnew, nold), file=fout) + print(f"{t} {nnew}: {nold}", file=fout) class LikeFunction(LikeFunctionBase): @@ -107,14 +111,13 @@ class LikeFunction(LikeFunctionBase): Mapping information is in self.mappings, which is an OrderedDict of the form `range -> function`. """ + def __init__(self, log=False): - super(LikeFunction, self).__init__(log) + super().__init__(log) # OrderedDict of range -> function self.mappings = OrderedDict() - return - def get_value(self, x): for rng, f in reversed(self.mappings.items()): if x in rng: @@ -124,7 +127,7 @@ def get_value(self, x): def _str(self): res = [] for r, f in self.mappings.items(): - res.append('{} -> {}'.format(r, f._mydoc)) + res.append(f"{r} -> {f._mydoc}") return res @@ -134,8 +137,9 @@ class LikeIndexFunction(LikeFunctionBase): List to be indexed is in self.vals. """ + def __init__(self, log=False, i0=1, skip=[], vals=[]): - super(LikeIndexFunction, self).__init__(log) + super().__init__(log) # List of values to index: self.vals = vals @@ -147,7 +151,6 @@ def __init__(self, log=False, i0=1, skip=[], vals=[]): self.skip = skip self.get_value = self.get_valueI - return def unique(self): """ @@ -173,13 +176,11 @@ def compile(self): d[x] = i + self.i0 self.d = d self.get_value = self.get_valueD - return def get_valueI(self, x): if x not in self.skip: # and x in self.vals: return self.vals.index(x) + self.i0 - else: - return self.default(x) + return self.default(x) def get_valueD(self, x): return self.d[x] @@ -187,14 +188,15 @@ def get_valueD(self, x): def _str(self): res = [] for x in self.vals: - res.append('{} -> {}'.format(x, self.get_value(x))) + res.append(f"{x} -> {self.get_value(x)}") return res -class Range(object): +class Range: """ Represents a range or a point. Should be considered as immutable. """ + def __init__(self, x1, x2=None): if x2 is None: self.__x1 = x1 @@ -203,19 +205,16 @@ def __init__(self, x1, x2=None): x1, x2 = sorted((x1, x2)) self.__x1 = x1 self.__x2 = x2 - return def __contains__(self, value): if self.__x2 is None: return value == self.__x1 - else: - return (self.__x1 <= value <= self.__x2) + return self.__x1 <= value <= self.__x2 def __str__(self): if self.__x2 is None: return str(self.__x1) - else: - return '[{} -- {}]'.format(self.__x1, self.__x2) + return f"[{self.__x1} -- {self.__x2}]" def __hash__(self): return hash((self.__x1, self.__x2)) @@ -226,8 +225,9 @@ def __eq__(self, o): def __ne__(self, o): return hash(self) != hash(o) + # Possible number types: -ntList = ('cel', 'sur', 'u', 'tr', 'mat') +ntList = ("cel", "sur", "u", "tr", "mat") _ntd = dict(map(lambda x: (x[0], x), ntList)) @@ -239,7 +239,7 @@ def read_map_file(fname, log=False): # Dictionary type -> LikeFunction maps = {} - with resolve_fname_or_stream(fname, 'r') as mapfile: + with resolve_fname_or_stream(fname, "r") as mapfile: for l in mapfile: t, ranges, f = _parse_map_line(l) if t is None: @@ -247,7 +247,7 @@ def read_map_file(fname, log=False): continue if t not in maps: m = LikeFunction(log=log) - m.doc = 'Mappping for `{}` from `{}`'.format(t, fname) + m.doc = f"Mappping for `{t}` from `{fname}`" maps[t] = m m = maps[t] for r in ranges: @@ -263,20 +263,20 @@ def _parse_map_line(l): """ # Prepare line and check if not a comment: l = l.lower().lstrip() - if not l or l[0] == '#': + if not l or l[0] == "#": return None, None, None # Number type t = _ntd[l[0]] - rs, os = l[1:].split(':') + rs, os = l[1:].split(":") # Use only 1-st entry in the map rule os = os.split()[0].lstrip() # Sign and dn dn = int(os) - sign = os[0] in '+-' + sign = os[0] in "+-" ranges = list(_get_map_ranges(rs)) @@ -291,24 +291,23 @@ def _parse_map_line(l): def _get_map_ranges(s): # don't require from user spaces before and after `--` # and allow user commas - s = s.replace('--', ' -- ') - s = s.replace(',', ' ') - tl = (s + ' 0').split() + s = s.replace("--", " -- ") + s = s.replace(",", " ") + tl = (s + " 0").split() v1 = None is_range = False for t in tl: - if t == '--': + if t == "--": is_range = True + elif is_range: + yield Range(v1, x2=int(t)) + v1 = None + is_range = False else: - if is_range: - yield Range(v1, x2=int(t)) - v1 = None - is_range = False - else: - if v1 is not None: - yield Range(v1) - v1 = int(t) + if v1 is not None: + yield Range(v1) + v1 = int(t) def get_indices(scards, log=False): @@ -319,14 +318,14 @@ def get_indices(scards, log=False): The LikeFuncitons describe mapping for cell, surface, material and universe numbers to their indices -- as they appear in the MCNP input file. """ - from numjuggler.numbering import get_numbers + # get list of numbers as they appear in input d = get_numbers(scards) res = {} for k, l in d.items(): # do not rename universe 0 and material 0 - if k in ('u', 'mat'): + if k in ("u", "mat"): skip = [0] else: skip = [] @@ -337,11 +336,11 @@ def get_indices(scards, log=False): return res -if __name__ == '__main__': - maps = read_map_file('trial_map.txt', log=True) +if __name__ == "__main__": + maps = read_map_file("trial_map.txt", log=True) for t, m in maps.items(): print(t) print(m) for x in range(15): print(x, m(x)) - m.write_log_as_map('c') + m.write_log_as_map("c") diff --git a/numjuggler/main.py b/src/numjuggler/main.py similarity index 61% rename from numjuggler/main.py rename to src/numjuggler/main.py index fd5a8b2..dbf5ca6 100644 --- a/numjuggler/main.py +++ b/src/numjuggler/main.py @@ -1,58 +1,63 @@ #!/usr/bin/env python -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import annotations import argparse as ap -import sys -from math import pi as Pi import os +import sys + from io import StringIO +from math import pi as Pi +from typing import TextIO + +from numjuggler import likefunc as lf from numjuggler import numbering as mn from numjuggler import parser as mp from numjuggler import ri_notation as rin from numjuggler import string_cells as stc -from numjuggler import likefunc as lf from numjuggler import version try: - import pirs.mcnp.mctal.Mctal as Mctal, Vector3, Material + import Material + import Vector3 + + from pirs.mcnp.mctal import Mctal except: Material = None Mctal = None Vector3 = None -def multiline(lines, prefix=''): - return prefix + ('\n' + prefix).join(lines) +def multiline(lines, prefix=""): + return prefix + ("\n" + prefix).join(lines) -def tr2str(pl, fmt1='{:12.9f}', fmte='{:16.8e}'): +def tr2str(pl, fmt1="{:12.9f}", fmte="{:16.8e}"): """ Compact format for tr card. """ - r = ['tr{:<}'] + r = ["tr{:<}"] for p in pl: if p == 0: - ps = '0' + ps = "0" elif p == -1: - ps = '-1' + ps = "-1" elif p == 1: - ps = '1' + ps = "1" elif -10 < p < 10: ps = fmt1.format(p) else: ps = fmte.format(p) # Remove unnecessary characters - ps = ps.replace('e-0', 'e-') - ps = ps.replace('e+0', 'e+') - ps = ps.replace(' ', '') + ps = ps.replace("e-0", "e-") + ps = ps.replace("e+0", "e+") + ps = ps.replace(" ", "") if len(r[-1]) + len(ps) > 80: - r.append('\n ') - r[-1] += ' ' + ps - return ''.join(r) + r.append("\n ") + r[-1] += " " + ps + return "".join(r) # help messages already wrapped: @@ -80,87 +85,119 @@ def tr2str(pl, fmt1='{:12.9f}', fmte='{:16.8e}'): help. """ -modes = ('renum', 'info', 'wrap', 'uexp', 'rems', 'remc', 'remh', 'remrp', 'minfo', - 'split', 'mdupl', 'matan', 'sdupl', 'msimp', 'extr', - 'nogq', 'nogq2', 'count', 'nofill', 'matinfo', 'uinfo', - 'impinfo', 'fillempty', 'sinfo', 'vsource', - 'tallies', 'addgeom', 'merge', 'remu', 'zrotate', - 'annotate', 'getc', 'mnew', 'combinec', 'cdens') +modes = ( + "renum", + "info", + "wrap", + "uexp", + "rems", + "remc", + "remh", + "remrp", + "minfo", + "split", + "mdupl", + "matan", + "sdupl", + "msimp", + "extr", + "nogq", + "nogq2", + "count", + "nofill", + "matinfo", + "uinfo", + "impinfo", + "fillempty", + "sinfo", + "vsource", + "tallies", + "addgeom", + "merge", + "remu", + "zrotate", + "annotate", + "getc", + "mnew", + "combinec", + "cdens", +) -def processing(args, cards, debuglog): +def processing(args, cards: list[mp.Card], debuglog: TextIO | None) -> StringIO: # define output string stream outstr = StringIO() # perform operations based on 77 - if args.mode == 'info': - indent = ' '*8 + if args.mode == "info": + indent = " " * 8 for c in cards: c.get_values() - d = mn.get_numbers(cards) + d = mp.get_numbers(cards) if args.debug: types = sorted(d.keys()) else: - types = ['cel', 'sur', 'mat', 'u', 'tal', 'tr'] + types = ["cel", "sur", "mat", "u", "tal", "tr"] for t in types: - if t[0] != '#': # for meaning of '#' see parser. + if t[0] != "#": # for meaning of '#' see parser. nset = set(d.get(t, [])) - print('-' * 40, t, len(nset), file=outstr) - print('-' * 20, t, ' list', end='', file=outstr) - print(' '.join(map(str, rin.shorten(sorted(nset)))), file=outstr) + print("-" * 40, t, len(nset), file=outstr) + print("-" * 20, t, " list", end="", file=outstr) + print(" ".join(map(str, rin.shorten(sorted(nset)))), file=outstr) rp = None for r1, r2 in mn._get_ranges_from_set(nset): - print('{}{:>3s}'.format(indent, t[0]), end='', file=outstr) + print(f"{indent}{t[0]:>3s}", end="", file=outstr) if r1 == r2: - rs = ' {}'.format(r1) + rs = f" {r1}" else: - rs = ' {} -- {}'.format(r1, r2) + rs = f" {r1} -- {r2}" if rp is not None: - fr = '{}'.format(r1 - rp - 1) + fr = f"{r1 - rp - 1}" else: - fr = '' - ur = '{}'.format(r2 - r1 + 1) - print('{:<30s} {:>8s} {:>8s}'.format(rs, ur, fr), file=outstr) + fr = "" + ur = f"{r2 - r1 + 1}" + print(f"{rs:<30s} {ur:>8s} {fr:>8s}", file=outstr) rp = r2 - elif args.mode == 'remh': - stc.remove_hash(cards,args.log) + elif args.mode == "remh": + stc.remove_hash(cards, args.log) for c in cards: - if c.cstrg : # strg: flags setting if card has been modified with remove - c.get_input() - print(c.card(True), end='', file=outstr) - else: - print(c.card(), end='', file=outstr) - elif args.mode == 'remrp': + if c.cstrg: # strg: flags setting if card has been modified with remove + c.get_input() + print(c.card(True), end="", file=outstr) + else: + print(c.card(), end="", file=outstr) + elif args.mode == "remrp": print_log = False - if args.log != '' : - flog = open(args.log,'w') - flog.write(' Cell : Parentheses removed\n') - print_log = True + if args.log != "": + flog = open(args.log, "w") + flog.write(" Cell : Parentheses removed\n") + print_log = True for c in cards: - if c.ctype == mp.CID.cell: - cardstr = stc.cell_card_string(''.join(c.lines)) - cardstr.geom.remove_redundant(remopt=args.opt) - c.lines = cardstr.get_lines() - c.get_input() - if print_log: - if cardstr.geom.removedp : - cname = cardstr.headstr.split()[0] - if (cardstr.geom.removedp[0] != cardstr.geom.removedp[1] ): - flog.write(' {:>9s} : unbalanced\n'.format(cname)) - elif ( args.opt == 'nochg' and cardstr.geom.removedp[0] == 0) : - flog.write(' {:>9s} : nochg\n'.format(cname)) - else: - flog.write(' {:>9s} : {:>5}\n'.format(cname,cardstr.geom.removedp[0])) - print(c.card(True), end='', file=outstr) - else: - print(c.card(), end='', file=outstr) - elif args.mode == 'ext': + if c.ctype == mp.CID.cell: + cardstr = stc.cell_card_string("".join(c.lines)) + cardstr.geom.remove_redundant(remopt=args.opt) + c.lines = cardstr.get_lines() + c.get_input() + if print_log: + if cardstr.geom.removedp: + cname = cardstr.headstr.split()[0] + if cardstr.geom.removedp[0] != cardstr.geom.removedp[1]: + flog.write(f" {cname:>9s} : unbalanced\n") + elif args.opt == "nochg" and cardstr.geom.removedp[0] == 0: + flog.write(f" {cname:>9s} : nochg\n") + else: + flog.write(f" {cname:>9s} : {cardstr.geom.removedp[0]:>5}\n") + print(c.card(True), end="", file=outstr) + else: + print(c.card(), end="", file=outstr) + elif args.mode == "ext": # output list of cells for ext:n card for c in cards: c.get_values() - d = mn.get_numbers(cards) - elif args.mode == 'cdens': + d = mp.get_numbers(cards) + elif args.mode == "cdens": from .mapparsers import cdens + # Change density of cells, specified in the map file. Map file m, mdef = cdens(args.map) for c in cards: @@ -169,13 +206,13 @@ def processing(args, cards, debuglog): modified = False for tr in m.keys(): t, r = tr - if t == 'c' and c.name in r: + if t == "c" and c.name in r: dorig = c.get_d() coef, fmt = m[tr] dnew = dorig * coef c.set_d(fmt.format(dnew)) modified = True - if t == 'm' and c.get_m() in r: + if t == "m" and c.get_m() in r: dorig = c.get_d() coef, fmt = m[tr] dnew = dorig * coef @@ -188,15 +225,16 @@ def processing(args, cards, debuglog): for t, (val, fmt) in mdef.items(): dnew *= val c.set_d(fmt.format(dnew)) - print(c.card(), end='', file=outstr) - elif args.mode == 'tallies': + print(c.card(), end="", file=outstr) + elif args.mode == "tallies": # New version: tally number and universes should be specified in the # format string passed via -m argument. -m must be present and have # form: 'f4:n (u4 < u5)', where uN -- placeholders for lists of # cells that belong to universe N. Usual cell numbers can be used # as well. import re - r = re.compile(r'(u)(\d+)') + + r = re.compile(r"(u)(\d+)") csets = {} ulst = [] fmt = args.m[:] @@ -204,7 +242,7 @@ def processing(args, cards, debuglog): ss = s[0] + s[1] u = int(s[1]) csets[u] = set() - fmt = fmt.replace(ss, '{' + ss + '}', 1) + fmt = fmt.replace(ss, "{" + ss + "}", 1) ulst += [u] for c in cards: c.get_values() @@ -218,27 +256,27 @@ def processing(args, cards, debuglog): break farg = {} for k, v in list(csets.items()): - farg['u' + str(k)] = ' '.join(map(str, rin.shorten(sorted(v)))) + farg["u" + str(k)] = " ".join(map(str, rin.shorten(sorted(v)))) # Tally card print(fmt.format(**farg), file=outstr) # SD card. Requires tally number and number of cells - nt = fmt.split(':')[0][1:] + nt = fmt.split(":")[0][1:] nc = len(csets[ulst[0]]) # number of cells - print('sd{} 1 {}r'.format(nt, nc - 1), file=outstr) - print('fc{} '.format(nt), end='', file=outstr) + print(f"sd{nt} 1 {nc - 1}r", file=outstr) + print(f"fc{nt} ", end="", file=outstr) for u in ulst: - print(len(csets[u]), end='', file=outstr) - print('', file=outstr) - elif args.mode == 'addgeom': + print(len(csets[u]), end="", file=outstr) + print(file=outstr) + elif args.mode == "addgeom": # add stuff to geometry definition of cells. # Get info from the --map file: extr = {} rem = set() - if ',' in args.m: - ds1, ds2 = args.m.split(',') + if "," in args.m: + ds1, ds2 = args.m.split(",") else: - ds1 = '' - ds2 = '' + ds1 = "" + ds2 = "" if args.map: for l in open(args.map): l = l.strip() @@ -251,13 +289,13 @@ def processing(args, cards, debuglog): else: c, s = tokens c = int(c) - if ',' in s: - s1, s2 = s.split(',') - s1 = ' ' + s1.strip() + ' ' - s2 = ' ' + s2.strip() + ' ' + if "," in s: + s1, s2 = s.split(",") + s1 = " " + s1.strip() + " " + s2 = " " + s2.strip() + " " else: - s1 = ' ' + s.strip() + ' ' - s2 = '' + s1 = " " + s.strip() + " " + s2 = "" extr[c] = (s1, s2) for c in cards: if c.ctype == mp.CID.cell: @@ -269,10 +307,10 @@ def processing(args, cards, debuglog): c.geom_prefix = s1 c.geom_suffix = s2 if c.name not in rem: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) else: - print(c.card(), end='', file=outstr) - elif args.mode == 'merge': + print(c.card(), end="", file=outstr) + elif args.mode == "merge": # Merge treats models as the main one and an additional one. Title # of the resulting model -- from the main model, or user specified # by -t option. Blocks of the additional model are denoted by @@ -293,8 +331,8 @@ def processing(args, cards, debuglog): mb = [] if mb: for c in mb: - print(c.card(), end='', file=outstr) - print('', file=outstr) + print(c.card(), end="", file=outstr) + print(file=outstr) # Title: if args.t == "0": # default one. Use title of the main input @@ -308,14 +346,14 @@ def processing(args, cards, debuglog): # inputs have title cards, i.e. not continuation input files. t2 = blk2[mp.CID.title][0] # emphasize second title - cmnt = 'c {} {} cards ' + '"{}"'.format(t2.card()[:-1]) + cmnt = "c {} {} cards " + f'"{t2.card()[:-1]}"' else: - cmnt = 'c {} {} cards ' + args.c + cmnt = "c {} {} cards " + args.c # Cells, surfaces and data: for t in [mp.CID.cell, mp.CID.surface, mp.CID.data]: for c in blk1[t]: - print(c.card(), end='', file=outstr) - if t in blk2 and blk2[t]: + print(c.card(), end="", file=outstr) + if blk2.get(t): # First check if blk2 actually contains any cards: flg = False for c in blk2[t]: @@ -323,14 +361,14 @@ def processing(args, cards, debuglog): flg = True break if flg: - print(cmnt.format('start', mp.CID.get_name(t)), file=outstr) + print(cmnt.format("start", mp.CID.get_name(t)), file=outstr) for c in blk2[t]: - print(c.card(), end='', file=outstr) - print(cmnt.format('end', mp.CID.get_name(t)), file=outstr) + print(c.card(), end="", file=outstr) + print(cmnt.format("end", mp.CID.get_name(t)), file=outstr) if t != mp.CID.data: # do not add empty line after data block - print('', file=outstr) - elif args.mode == 'uexp': + print(file=outstr) + elif args.mode == "uexp": if args.u == "0": N = " u=0 " else: @@ -343,54 +381,57 @@ def cfunc(n): elif "!" in args.c: # Check only cells not mentioned in -c cset = set(rin.expand(args.c.replace("!", " ").split())) + def cfunc(n): return n not in cset else: # Check only cells mentioned in -c cset = set(rin.expand(args.c.replace("!", " ").split())) + def cfunc(n): return n in cset + for c in cards: if c.ctype == mp.CID.cell: c.get_values() - if (cfunc(c.name) and - 'u' not in [t[1] for t in c.values]): + if cfunc(c.name) and "u" not in [t[1] for t in c.values]: c.input[-1] += N # ' u=0' - print(c.card(), end='', file=outstr) - elif args.mode == 'wrap': + print(c.card(), end="", file=outstr) + elif args.mode == "wrap": for c in cards: - print(c.card(wrap=True), end='', file=outstr) - elif args.mode == 'rems': + print(c.card(wrap=True), end="", file=outstr) + elif args.mode == "rems": for c in cards: c.remove_spaces() - print(c.card(), end='', file=outstr) - elif args.mode == 'remc': + print(c.card(), end="", file=outstr) + elif args.mode == "remc": for c in cards: if c.ctype == mp.CID.comment: continue - print(c.card(), end='', file=outstr) - elif args.mode == 'split': + print(c.card(), end="", file=outstr) + elif args.mode == "split": blocks = mp.get_blocks(cards) for k, cl in list(blocks.items()): if cl: i = mp.CID.get_name(k) - with open(args.inp + '.{}{}'.format(k, i), 'w') as fout: + with open(args.inp + f".{k}{i}", "w") as fout: for c in cl: - print(c.card(), end='', file=fout) + print(c.card(), end="", file=fout) # create file with blank line delimiter if k in (mp.CID.cell, mp.CID.surface): - fout = open(args.inp + '.{}z'.format(k), 'w') - print(' ', file=fout) + fout = open(args.inp + f".{k}z", "w") + print(" ", file=fout) fout.close() - elif args.mode == 'matan': + elif args.mode == "matan": # Compare pairwise mateiral cards. Two materials are compared by # their string representation from pirs.mcnp import Material + # read all material cards and convert to string representation sd = {} for c in cards: c.get_values() - if c.ctype == mp.CID.data and c.dtype == 'Mn': + if c.ctype == mp.CID.data and c.dtype == "Mn": m = Material.parseCard(c) m.remove_duplicates() m.normalize(1.0) @@ -404,99 +445,91 @@ def cfunc(n): for s, m in list(sd.items()): print(i, m, file=outstr) i += 1 - elif args.mode == 'mdupl': + elif args.mode == "mdupl": # remove duplicate material cards, if they are equal. mset = set() for c in cards: c.get_values() - if c.ctype == mp.CID.data and c.dtype == 'Mn': + if c.ctype == mp.CID.data and c.dtype == "Mn": if c.values[0][0] not in mset: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) mset.add(c.values[0][0]) else: - print(c.card(), end='', file=outstr) - elif args.mode == 'mnew': + print(c.card(), end="", file=outstr) + elif args.mode == "mnew": # read from map definition of new materials in terms of existing # materials, and add new to the modified input. # Read new material definitions from the map file: - dd = {} # definition dictionary - rms = set() # reference materials set + dd = {} # definition dictionary + rms = set() # reference materials set with open(args.map) as fmap: for l in fmap: tl = l.split() rml = map(int, tl[1::3]) - dd[tl[0]] = zip(rml, - map(float, tl[2::3]), - map(float, tl[3::3])) + dd[tl[0]] = zip(rml, map(float, tl[2::3]), map(float, tl[3::3])) rms.update(rml) # read reference materials and create Materials - assert Material is not None, "pirs Material class is not imported" + assert Material is not None, "pirs Material class is not imported" rmd = {} for c in cards: if c.ctype == mp.CID.data: c.get_values() - if c.dtype == 'Mn' and c.name in rml: + if c.dtype == "Mn" and c.name in rml: m = Material.parseCard(c) - m.name = 'm{} from {}'.format(c.name, args.inp) + m.name = f"m{c.name} from {args.inp}" rmd[c.name] = m # create new materials for n, d in dd.items(): - r = [] # recipe + r = [] # recipe for i in d: r.append(rmd[i[0]]) r.append((i[1] * i[2], 2)) m = Material(*r) - print('\nc '.join(m.report().splitlines()), file=outstr) + print("\nc ".join(m.report().splitlines()), file=outstr) print(m.card().format(n), file=outstr) - elif args.mode == 'sdupl': + elif args.mode == "sdupl": # report duplicate (close) surfaces. # dict of unique surafces us = {} # surface types coefficients that can only be proportional - pcl = { - 'p': (0,), - 'sq': (0, 7), - 'gq': (0,) - } + pcl = {"p": (0,), "sq": (0, 7), "gq": (0,)} for c in cards: c.get_values() if c.ctype == mp.CID.surface: # compare this surface with all previous and if unique, add # to dict - if c.stype not in us.keys(): + if c.stype not in us: us[c.stype] = {} ust = us[c.stype] for sn, s in list(ust.items()): - if mp.are_close_lists(s.scoefs, c.scoefs, - pci=pcl.get(c.stype, [])): + if mp.are_close_lists(s.scoefs, c.scoefs, pci=pcl.get(c.stype, [])): # If c is close to s, print s instead s.values[0] = (c.values[0][0], s.values[0][1]) - print(s.card(), end='', file=outstr) + print(s.card(), end="", file=outstr) s.values[0] = (sn, s.values[0][1]) break else: # add c to us: cn = c.values[0][0] # surface name ust[cn] = c - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) # print 'is unique' else: - print(c.card(), end='', file=outstr) - elif args.mode == 'msimp': + print(c.card(), end="", file=outstr) + elif args.mode == "msimp": # simplify material cards for c in cards: if c.ctype == mp.CID.data: c.get_values() - if c.dtype == 'Mn': + if c.dtype == "Mn": inp = [] - inp.append(c.input[0].replace('}', - '} 1001 1.0 $ msimpl ', 1)) + inp.append(c.input[0].replace("}", "} 1001 1.0 $ msimpl ", 1)) for i in c.input[1:]: - inp.append('c msimpl ' + i) + inp.append("c msimpl " + i) c.input = inp - print(c.card(), end='', file=outstr) - elif args.mode == 'remu': - if args.u[0] == '!': + print(c.card(), end="", file=outstr) + elif args.mode == "remu": + if args.u[0] == "!": # -u option starts with !. In this case, remove all other # universes. iflag = True @@ -508,12 +541,12 @@ def cfunc(n): cset = set() sset = set() mset = set() - if args.map != '': + if args.map != "": # read cells to remove from --map file for l in open(args.map): for v in l.split(): cset.add(int(v)) - if args.c != '0': + if args.c != "0": le = rin.expand(args.c.split()) for v in le: cset.add(int(v)) @@ -540,35 +573,32 @@ def cfunc(n): elif c.name not in cset: # collect surfaces needed for other cells for v, t in c.values: - if t == 'sur': + if t == "sur": sset.add(v) - if t == 'mat': + if t == "mat": mset.add(v) # Prepare additional lines to be added to cell and surface blocks: - newcell = 'c ' - newsurf = 'c ' - if args.m != '0': # -c is already used! + newcell = "c " + newsurf = "c " + if args.m != "0": # -c is already used! newcell = args.m - if args.s != '0': + if args.s != "0": newsurf = args.s prevctype = None for c in cards: - if c.ctype == mp.CID.cell and c.name in cset: - pass - elif c.ctype == mp.CID.surface and c.name not in sset: - pass - elif (c.ctype == mp.CID.data and - c.dtype == 'Mn' and - c.values[0][0] not in mset): - print('c qqq', repr(c.values[0][0]), file=outstr) + if (c.ctype == mp.CID.cell and c.name in cset) or ( + c.ctype == mp.CID.surface and c.name not in sset + ): pass + elif c.ctype == mp.CID.data and c.dtype == "Mn" and c.values[0][0] not in mset: + print("c qqq", repr(c.values[0][0]), file=outstr) else: # check that cell card does not depend on one of cset: if c.get_refcells(): for i in range(len(c.values)): v, t = c.values[i] - if t == 'cel' and v in cset: - c.values[i] = ('___', 'cel') + if t == "cel" and v in cset: + c.values[i] = ("___", "cel") # If the cell is filled with a universe to delete, # change its fill to newfill: # if c.get_f() in uref: @@ -576,23 +606,23 @@ def cfunc(n): # Insert additional cell if prevctype == mp.CID.cell and c.ctype == mp.CID.blankline: print(newcell, file=outstr) - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) # Insert additional surface if prevctype == mp.CID.cell and c.ctype == mp.CID.blankline: print(newsurf, file=outstr) if c.ctype != mp.CID.comment: prevctype = c.ctype - print('c sset', ' '.join(map(str, rin.shorten(sorted(sset)))), file=outstr) - print('c uref', ' '.join(map(str, rin.shorten(sorted(uref)))), file=outstr) + print("c sset", " ".join(map(str, rin.shorten(sorted(sset)))), file=outstr) + print("c uref", " ".join(map(str, rin.shorten(sorted(uref)))), file=outstr) # print dummy universes, just in case they are needed - print('', file=outstr) + print(file=outstr) l = len(str(max(uref))) - f = '{{0:0{}d}}'.format(l) + f = f"{{0:0{l}d}}" for u in sorted(uref): s = f.format(u) - print('dummy_prefix{0} 0 dummy_surface u={0}'.format(s), file=outstr) - print('c mset', ' '.join(map(str, rin.shorten(sorted(mset)))), file=outstr) - elif args.mode == 'combinec': + print(f"dummy_prefix{s} 0 dummy_surface u={s}", file=outstr) + print("c mset", " ".join(map(str, rin.shorten(sorted(mset)))), file=outstr) + elif args.mode == "combinec": # Combine cells, listed in -c flag. # Get cells to be combined from command line parameter clst1 = map(int, rin.expand(args.c.split())) @@ -606,29 +636,28 @@ def cfunc(n): if d and c.ctype == mp.CID.blankline: break new_card = d[clst1[0]] - new_card.geom_prefix = ' (' - new_card.geom_suffix = ') ' + new_card.geom_prefix = " (" + new_card.geom_suffix = ") " for n in clst1[1:]: g = d[n].get_geom() - g = ' '.join(g.splitlines()) - new_card.geom_suffix += '({}) '.format(g) + g = " ".join(g.splitlines()) + new_card.geom_suffix += f"({g}) " # Print out the new file for c in cards: if c.ctype == mp.CID.cell: if c.name in clst1[1:]: - print('c ' + '\nc '.join(c.card().splitlines()), file=outstr) + print("c " + "\nc ".join(c.card().splitlines()), file=outstr) else: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) else: - print(c.card(), end='', file=outstr) - elif args.mode == 'zrotate': - assert Vector3 is not None, "pirs Vector3 class is not imported" - ag = float(args.c) # in grad - ar = ag * Pi / 180. # in radians + print(c.card(), end="", file=outstr) + elif args.mode == "zrotate": + assert Vector3 is not None, "pirs Vector3 class is not imported" + ag = float(args.c) # in grad + ar = ag * Pi / 180.0 # in radians # new transformation number: trn = args.t - trcard = '*tr{} 0 0 0 {} {} 90 {} {} 90 90 90 0'.format( - trn, ag, ag-90, 90+ag, ag) + trcard = f"*tr{trn} 0 0 0 {ag} {ag - 90} 90 {90 + ag} {ag} 90 90 90 0" # change all tr cards and surface cards: for c in cards: if c.ctype == mp.CID.surface: @@ -637,12 +666,12 @@ def cfunc(n): # transformation. if len(c.values) == 1: # surface has no transformation. Add the new one - inpt = '\n'.join(c.input) - inpt = inpt.replace('} ', '} ' + trn + ' ', 1) - c.input = inpt.split('\n') + inpt = "\n".join(c.input) + inpt = inpt.replace("} ", "} " + trn + " ", 1) + c.input = inpt.split("\n") if c.ctype == mp.CID.data: c.get_values() - if c.dtype == 'TRn': + if c.dtype == "TRn": # put new tr card just before the 1-st tr card in the # input: if trcard: @@ -652,12 +681,14 @@ def cfunc(n): # entries: o = Vector3(car=[v[0] for v in c.values[1:4]]) o.t += ar - c.values[1] = (o.x, 'float') - c.values[2] = (o.y, 'float') - c.values[3] = (o.z, 'float') + c.values[1] = (o.x, "float") + c.values[2] = (o.y, "float") + c.values[3] = (o.z, "float") + def e1(v): return v[0] - if c.unit == '': + + if c.unit == "": # rotation matrix contains cos b1 = Vector3(car=list(map(e1, c.values[4:7]))) b2 = Vector3(car=list(map(e1, c.values[7:10]))) @@ -665,47 +696,46 @@ def e1(v): b1.t += ar b2.t += ar b3.t += ar - c.values[4] = (b1.x, 'float') - c.values[5] = (b1.y, 'float') - c.values[6] = (b1.z, 'float') - c.values[7] = (b2.x, 'float') - c.values[8] = (b2.y, 'float') - c.values[9] = (b2.z, 'float') - c.values[10] = (b3.x, 'float') - c.values[11] = (b3.y, 'float') - c.values[12] = (b3.z, 'float') + c.values[4] = (b1.x, "float") + c.values[5] = (b1.y, "float") + c.values[6] = (b1.z, "float") + c.values[7] = (b2.x, "float") + c.values[8] = (b2.y, "float") + c.values[9] = (b2.z, "float") + c.values[10] = (b3.x, "float") + c.values[11] = (b3.y, "float") + c.values[12] = (b3.z, "float") else: # rotation matrix contains angles in grad. b1, b2, b3, b4, b5, b6, b7, b8, b9 = list(map(e1, c.values[4:13])) # Assume that it already describes rotation around z # axis - assert (b3 == 90 and b6 == 90 and b7 == 90 and - b8 == 90 and b9 == 0) + assert b3 == 90 and b6 == 90 and b7 == 90 and b8 == 90 and b9 == 0 b1 += ag b2 += ag b4 += ag b5 += ag - c.values[4] = (b1, 'float') - c.values[5] = (b2, 'float') - c.values[7] = (b4, 'float') - c.values[8] = (b5, 'float') - print(c.card(), end='', file=outstr) - elif args.mode == 'annotate': + c.values[4] = (b1, "float") + c.values[5] = (b2, "float") + c.values[7] = (b4, "float") + c.values[8] = (b5, "float") + print(c.card(), end="", file=outstr) + elif args.mode == "annotate": # Read text from map file, add "c" to each line and put after the # title. if args.c == "0": # default commenting string: - cs = 'c ' + cs = "c " else: # user-specified commenting string: cs = args.c - txt = [cs + l for l in open(args.map).readlines()] + txt = [cs + l for l in open(args.map)] for c in cards: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) if c.ctype == mp.CID.title: for l in txt: - print(l, end='', file=outstr) # readlines() method returns lines with \n - elif args.mode == 'getc': + print(l, end="", file=outstr) # readlines() method returns lines with \n + elif args.mode == "getc": # Extract comments that take more than 10 lines: if args.c != "0": N = int(args.c) @@ -717,21 +747,21 @@ def e1(v): l = len(ccc.splitlines()) if l >= N: print(c.pos, l, file=outstr) - print(ccc, end='', file=outstr) - elif args.mode == 'extr': + print(ccc, end="", file=outstr) + elif args.mode == "extr": # extract cell specified in -c keyword and necessary materials, and # surfaces. cset = set() - flag = '' # can be '!' - if args.c != '0': - if '!' in args.c: - args.c = args.c.replace('!', ' ') - flag = '!' + flag = "" # can be '!' + if args.c != "0": + if "!" in args.c: + args.c = args.c.replace("!", " ") + flag = "!" le = rin.expand(args.c.split()) cset = set(le) - if args.map != '': + if args.map != "": # cset = set() - for l in open(args.map, 'r'): + for l in open(args.map): for c in l.split(): cset.add(int(c)) # get set of all cells: @@ -741,10 +771,10 @@ def e1(v): if c.ctype == mp.CID.cell: aset.add(c.name) extract_parents_flag = True - if args.u != '0': - if '_' in args.u: + if args.u != "0": + if "_" in args.u: extract_parents_flag = False - args.u = args.u.replace('_', ' ') + args.u = args.u.replace("_", " ") else: extract_parents_flag = True uref = int(args.u) @@ -756,7 +786,7 @@ def e1(v): cset.add(c.name) # '!' means that the specified cells should NOT be extracted, but # all other. - if flag == '!': + if flag == "!": cset = aset.difference(cset) if not cset: print("No cells to extract are specified", file=outstr) @@ -810,57 +840,56 @@ def e1(v): if c.ctype == mp.CID.cell and c.name in cset: # get all surface names and the material, if any. for v, t in c.values: - if t == 'sur': + if t == "sur": sset.add(v) - elif t == 'mat': + elif t == "mat": mset.add(v) - elif t == 'tr': + elif t == "tr": tset.add(v) if c.ctype == mp.CID.surface and c.name in sset: # surface card can refer to tr for v, t in c.values: - if t == 'tr': + if t == "tr": tset.add(v) blk = None for c in cards: if c.ctype == mp.CID.title: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) if c.ctype == mp.CID.cell and c.name in cset: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) blk = c.ctype if c.ctype == mp.CID.surface: if blk == mp.CID.cell: - print('', file=outstr) + print(file=outstr) blk = c.ctype if c.name in sset: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) if c.ctype == mp.CID.data: if blk != c.ctype: - print('', file=outstr) + print(file=outstr) blk = c.ctype - if c.dtype == 'Mn' and c.values[0][0] in mset: - print(c.card(), end='', file=outstr) - if c.dtype == 'TRn': # and c.values[0][0] in tset: - print(c.card(), end='', file=outstr) - elif args.mode == 'nogq': + if c.dtype == "Mn" and c.values[0][0] in mset: + print(c.card(), end="", file=outstr) + if c.dtype == "TRn": # and c.values[0][0] in tset: + print(c.card(), end="", file=outstr) + elif args.mode == "nogq": from numjuggler import nogq + trn0 = int(args.t) cflag = False if args.c == "0" else True - vfmt = ' {:15.8e}'*3 - tfmt = 'tr{} 0 0 0 ' + ('\n ' + vfmt)*3 + vfmt = " {:15.8e}" * 3 + tfmt = "tr{} 0 0 0 " + ("\n " + vfmt) * 3 trd = {} # replace GQ cylinders with c/x + tr for c in cards: crd = c.card() if c.ctype == mp.CID.surface: c.get_values() - if c.stype == 'gq': - p = nogq.get_gq_params(' '.join(c.input)) + if c.stype == "gq": + p = nogq.get_gq_params(" ".join(c.input)) a2, g, kk = nogq.get_k(p) if cflag: - crd = (crd[:-1] + - '$ a^2={:12.6e} c={:12.6e}\n'.format(a2, - g + a2)) + crd = crd[:-1] + f"$ a^2={a2:12.6e} c={g + a2:12.6e}\n" if abs((g + a2) / a2) < 1e-6: # this is a cylinder. Comment original card and # write another one @@ -876,29 +905,27 @@ def e1(v): trd[trn] = tr # replace surface card if cflag: - crd = ('c ' + - '\nc '.join(c.card().splitlines()) + - '\n') + crd = "c " + "\nc ".join(c.card().splitlines()) + "\n" else: - crd = '' - crd += '{} {} c/z {:15.8e} 0 {:15.8e}\n'.format( - c.name, trn + trn0, x0, R) + crd = "" + crd += f"{c.name} {trn + trn0} c/z {x0:15.8e} 0 {R:15.8e}\n" # crd += 'c a^2={:12.6e} g={:12.6e} k={}\n'.format(a2, g, kk) - print(crd, end='', file=outstr) + print(crd, end="", file=outstr) if trd and c.ctype == mp.CID.blankline: # this is blankline after surfaces. Put tr cards here for k, v in sorted(trd.items()): ijk = (k + trn0,) + v print(tfmt.format(*ijk), file=outstr) trd = {} - elif args.mode == 'nogq2': + elif args.mode == "nogq2": from numjuggler import nogq2 + trn0 = int(args.t) cflag = False if args.c == "0" else True - vfmt = ' {:15.8e}' - tfmt = 'tr{} 0 0 0 ' + ('\n ' + 3*vfmt) * 3 - cfmt = '{} {} {} ' + 3*vfmt + '\n' - kfmt = '{} {} {} ' + 3*vfmt + '\n ' + vfmt + '\n' + vfmt = " {:15.8e}" + tfmt = "tr{} 0 0 0 " + ("\n " + 3 * vfmt) * 3 + cfmt = "{} {} {} " + 3 * vfmt + "\n" + kfmt = "{} {} {} " + 3 * vfmt + "\n " + vfmt + "\n" trd = {} trn = 0 # replace GQ cylinders with c/x + tr @@ -906,14 +933,14 @@ def e1(v): crd = c.card() if c.ctype == mp.CID.surface: c.get_values() - if c.stype == 'gq': - tuf, pl = nogq2.get_params(' '.join(c.input)) + if c.stype == "gq": + tuf, pl = nogq2.get_params(" ".join(c.input)) typ, a, o, t2, r2, cl = nogq2.get_cone_or_cyl(pl) - print('c Log for GQ card {}'.format(c.name), file=outstr) + print(f"c Log for GQ card {c.name}", file=outstr) for comment in cl: print(comment, file=outstr) crd1 = crd.splitlines() - if typ in 'ck' and not tuf and trn + trn0 < 999: + if typ in "ck" and not tuf and trn + trn0 < 999: bbb, aaa = nogq2.basis_on_axis(a) # bbb is the basis, where the cylinder/cone axis is # parallel to axis aaa. In this basis, the origin o @@ -921,10 +948,10 @@ def e1(v): oprime = nogq2.transform(o, bbb, (0, 0, 0)) # add transformation set tr = sum(bbb, ()) # sum of 3 3-tuples is a 9-tuple - if aaa == 'x': + if aaa == "x": c1 = oprime[1] c2 = oprime[2] - elif aaa == 'y': + elif aaa == "y": c1 = oprime[0] c2 = oprime[2] else: @@ -943,32 +970,26 @@ def e1(v): # Comment the original surface card and add # information about type, axis, origin and params if cflag: - crd = multiline(crd1 + cl, 'c ') + '\n' + crd = multiline(crd1 + cl, "c ") + "\n" else: - crd = '' - if typ == 'c': - aaa = 'c/' + aaa + crd = "" + if typ == "c": + aaa = "c/" + aaa p = r2**0.5 # cylinder radius - crd += cfmt.format(c.name, - trn + trn0, - aaa, - c1, c2, p) - elif typ == 'k': - aaa = 'k/' + aaa + crd += cfmt.format(c.name, trn + trn0, aaa, c1, c2, p) + elif typ == "k": + aaa = "k/" + aaa p = t2 # square tan of half-angle c0, c1, c2 = oprime - crd += kfmt.format(c.name, - trn + trn0, - aaa, - c0, c1, c2, p) + crd += kfmt.format(c.name, trn + trn0, aaa, c0, c1, c2, p) elif tuf: # GQ with transform, do not modify - crd = multiline(crd1) + '\n' + crd = multiline(crd1) + "\n" else: # failed to convert GQ card. Use the original one # and print additional information - crd = multiline(crd1 + cl) + '\n' - print(crd, end='', file=outstr) + crd = multiline(crd1 + cl) + "\n" + print(crd, end="", file=outstr) if trd and c.ctype == mp.CID.blankline: # this is blankline after surfaces. Put tr cards here for k, v in sorted(trd.items()): @@ -977,16 +998,12 @@ def e1(v): # ijk = (k + trn0,) + v # print(tfmt.format(*ijk)) trd = {} - elif args.mode == 'count': + elif args.mode == "count": # take the maximal number of surfaces from -s: Nmax = int(args.s) if Nmax == 0: Nmax = 100 # default max value. - print(('{:>10s}'*5).format('Cell', - 'Line', - 'all', - 'unique', - '>{}'.format(Nmax)), file=outstr) + print(("{:>10s}" * 5).format("Cell", "Line", "all", "unique", f">{Nmax}"), file=outstr) sc = 0 # cell counter sa = 0 # all surfaces counter su = 0 # unique surface counter @@ -998,47 +1015,49 @@ def e1(v): # get list of surfaces used in the cell: los = [] for v, t in c.values: - if 'sur' in t: + if "sur" in t: los.append(v) # output number of surfaces: - a = len(los) # number of all surfaces + a = len(los) # number of all surfaces u = len(set(los)) # number of unique surfaces - print(('{:>10d}'*4).format(c.name, c.pos, a, u), end='', file=outstr) + print(("{:>10d}" * 4).format(c.name, c.pos, a, u), end="", file=outstr) if a > Nmax: - print(' *', file=outstr) + print(" *", file=outstr) else: - print(' ', file=outstr) + print(" ", file=outstr) sc += 1 sa += a su += u ma = max(ma, a) mu = max(mu, u) - print('', file=outstr) - print('sum', ('{:>10d}'*3).format(sc, sa, su), file=outstr) - print('max', ('{:>10d}'*3).format(00, ma, mu), file=outstr) - elif args.mode == 'nofill': + print(file=outstr) + print("sum", ("{:>10d}" * 3).format(sc, sa, su), file=outstr) + print("max", ("{:>10d}" * 3).format(00, ma, mu), file=outstr) + elif args.mode == "nofill": # remove all fill= keywords from cell cards. # Get universes to withdraw from command line parameters - uset = set(rin.expand(args.u.replace('!', ' ').split())) + uset = set(rin.expand(args.u.replace("!", " ").split())) # If -u contains !, reverse uset - if '!' in args.u: + if "!" in args.u: + def check(v, s): return v not in s else: + def check(v, s): return v in s + for c in cards: if c.ctype == mp.CID.cell: c.get_values() for v, t in c.values: - if t == 'fill': + if t == "fill": if check(v, uset): c.remove_fill() break - lines = '\n'.join(filter(lambda s: s.strip(), - c.card().splitlines())) + lines = "\n".join(filter(lambda s: s.strip(), c.card().splitlines())) print(lines, file=outstr) - elif args.mode == 'matinfo': + elif args.mode == "matinfo": # for each material used in cell cards, output list of cells # together with density and universe. res = {} @@ -1055,58 +1074,56 @@ def check(v, s): if c.ctype == mp.CID.surface: break # print out information - fmt = ' '*8 + '{:>16}'*3 - print(fmt.format('Cell', 'density', 'universe'), file=outstr) + fmt = " " * 8 + "{:>16}" * 3 + print(fmt.format("Cell", "density", "universe"), file=outstr) for m in sorted(res.keys()): uset = set() for c, d, u in res[m]: uset.add(u) - print('m{} -------------- {} {}'.format(m, - len(uset), - sorted(uset)), file=outstr) + print(f"m{m} -------------- {len(uset)} {sorted(uset)}", file=outstr) for c, d, u in res[m]: print(fmt.format(c, d, u), file=outstr) # Get a compact list of cells for material m cells = list(e[0] for e in res[m]) - print('Compact list of cells for material m{}: '.format(m), file=outstr) - print(' '.join(map(str, rin.shorten(cells))), file=outstr) + print(f"Compact list of cells for material m{m}: ", file=outstr) + print(" ".join(map(str, rin.shorten(cells))), file=outstr) # If -m option is given, try to get cell volumes from there # -m argument is the mctal name followed by tally number of the # tally containing cell volumes. - if args.m != '0': + if args.m != "0": assert Mctal is not None, "pirs Mctal class is not imported" mctal = Mctal() fname, tn = args.m.split() tn = int(tn) mctal.read_complete(fname) tv = mctal.mctaltallies[tn] - cn = tv.fnl_numpy # cell numbers - cv = tv.vals_numpy # cell volumes + cn = tv.fnl_numpy # cell numbers + cv = tv.vals_numpy # cell volumes # Compute material weights - res = {} # values are tuples (volume, weight) + res = {} # values are tuples (volume, weight) for c in cards: if c.ctype == mp.CID.cell: if c.name in cn: m = c.get_m() d = c.get_d() if m not in res: - res[m] = (0., 0.) + res[m] = (0.0, 0.0) v = cv[0, cn == c.name][0] - res[m] = (res[m][0] + v, res[m][1] + v*d) + res[m] = (res[m][0] + v, res[m][1] + v * d) else: - print('No volume for cell ', c.name) + print("No volume for cell ", c.name) if c.ctype == mp.CID.surface: break - print(('{:>20s}'*3).format('Material', 'Volume', 'Weight'), file=outstr) + print(("{:>20s}" * 3).format("Material", "Volume", "Weight"), file=outstr) sv = 0.0 sw = 0.0 for m, (v, w) in sorted(res.items()): - print('{:20d}{:20e}{:20e}'.format(m, v, w), file=outstr) + print(f"{m:20d}{v:20e}{w:20e}", file=outstr) if m > 0: sv += v sw += w - print('{:>20s}{:20e}{:20e}'.format('total nonvoid:', sv, sw), file=outstr) - elif args.mode == 'uinfo': + print("{:>20s}{:20e}{:20e}".format("total nonvoid:", sv, sw), file=outstr) + elif args.mode == "uinfo": # for each universe return list of its cells. res = {} fd = {} # dictionary of cells, filled with another universe @@ -1125,14 +1142,14 @@ def check(v, s): elif c.ctype == mp.CID.surface: break # print out - if args.u == '0': + if args.u == "0": for u, l in sorted(res.items()): if sflag: l = sorted(l) - print('u{} '.format(u), end='', file=outstr) + print(f"u{u} ", end="", file=outstr) for e in rin.shorten(l): - print(e, end=' ', file=outstr) - print('', file=outstr) + print(e, end=" ", file=outstr) + print(file=outstr) print(len(l), file=outstr) else: uref = int(args.u) @@ -1140,20 +1157,20 @@ def check(v, s): if sflag: l = sorted(l) for e in rin.shorten(l): - print(e, end='', file=outstr) + print(e, end="", file=outstr) # print tabulated "tree", see E-mail of Marco Fabri, 8.11.2017 for u, cl in sorted(res.items()): - print('Cells in universe ', u, file=outstr) + print("Cells in universe ", u, file=outstr) for c in cl: - print(c, fd.get(c, ''), file=outstr) - elif args.mode == 'impinfo': - if args.m == '0': + print(c, fd.get(c, ""), file=outstr) + elif args.mode == "impinfo": + if args.m == "0": for c in cards: if c.ctype == mp.CID.cell: c.get_values() i = c.get_imp() if 0 in list(i.values()): - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) else: nv = {} for t in args.m.split(): @@ -1163,8 +1180,8 @@ def check(v, s): if c.ctype == mp.CID.cell: c.get_values() c.get_imp(nv) - print(c.card(), end='', file=outstr) - elif args.mode == 'sinfo': + print(c.card(), end="", file=outstr) + elif args.mode == "sinfo": # first, get the list of surfaces: sl = {} st = set() # set of used surface types @@ -1178,136 +1195,139 @@ def check(v, s): if c.ctype == mp.CID.cell: c.get_values() for v, t in c.values: - if t == 'sur': + if t == "sur": sl[v][0].add(c.name) # print out: for s, (cs, t) in sorted(sl.items()): print(s, t, sorted(cs), file=outstr) for s in sorted(st): print(s, file=outstr) - elif args.mode == 'minfo': + elif args.mode == "minfo": countfmt = """ Total words :{d[0]:9} Total hash :{d[1]:9} Hashcel :{d[2]:9} Hashsurf :{d[3]:9}""" - cellfmt = """ Longest cell :{:9} + cellfmt = """ Longest cell :{:9} Words in longest cell :{:9}""" - mcnpfmt = """ + mcnpfmt = """ MCNP estimation : mlja :{:11} Estimated memory requirement : {:5.1f}{} %cell length, %number # : {:4.1%} {:4.1%}""" hashcellfmt = " {:>9s} {d[0]:3} {d[1]:3} {d[2]:3}" - munits=['bytes','kB','MB','GB','TB'] - stat_tot = [0,0,0,0] + munits = ["bytes", "kB", "MB", "GB", "TB"] + stat_tot = [0, 0, 0, 0] longest_c = None - maxword = 0 + maxword = 0 ic = 0 mlja = 0 hashlist = [] for c in cards: - if c.ctype == mp.CID.cell: - ic += 1 - cardstr = stc.cell_card_string(''.join(c.lines)) - cs=cardstr.get_stat() - if ( ic > 50 and mlja > 3250) : - mlja += 7 * cs[0] - else: - mlja += 17 * cs[0] - if ( cs[0] > maxword ) : - c.get_values() - maxword = cs[0] - longest_c = c.name - stat_tot=[a+b for a,b in zip(stat_tot,cs)] - if ( cs[1] > 0 ): - hashlist.append([cardstr.headstr.split()[0],cs[1:]]) + if c.ctype == mp.CID.cell: + ic += 1 + cardstr = stc.cell_card_string("".join(c.lines)) + cs = cardstr.get_stat() + if ic > 50 and mlja > 3250: + mlja += 7 * cs[0] + else: + mlja += 17 * cs[0] + if cs[0] > maxword: + c.get_values() + maxword = cs[0] + longest_c = c.name + stat_tot = [a + b for a, b in zip(stat_tot, cs)] + if cs[1] > 0: + hashlist.append([cardstr.headstr.split()[0], cs[1:]]) mljacell = mlja - mlja = mlja + 2*17*maxword*stat_tot[1] - mem = mlja * 4 * 4. # 4 times mlja, 4 bytes integer + mlja = mlja + 2 * 17 * maxword * stat_tot[1] + mem = mlja * 4 * 4.0 # 4 times mlja, 4 bytes integer im = 0 - while mem > 1024 : + while mem > 1024: im += 1 mem = mem / 1024 - pcel = float(mljacell)/mlja + pcel = float(mljacell) / mlja phash = 1 - pcel - print( countfmt.format(d=stat_tot) , file=outstr) - print( cellfmt.format( longest_c, maxword ), file=outstr) - print( mcnpfmt.format( mlja, mem, munits[im], pcel, phash ), file=outstr) - print( '\n Cell name total # cell # surf #' , file=outstr) + print(countfmt.format(d=stat_tot), file=outstr) + print(cellfmt.format(longest_c, maxword), file=outstr) + print(mcnpfmt.format(mlja, mem, munits[im], pcel, phash), file=outstr) + print("\n Cell name total # cell # surf #", file=outstr) for c in hashlist: - print( hashcellfmt.format(c[0],d=c[1]) , file=outstr) - elif args.mode == 'vsource': - def print_planar(params, d=1e-5, u='0'): + print(hashcellfmt.format(c[0], d=c[1]), file=outstr) + elif args.mode == "vsource": + + def print_planar(params, d=1e-5, u="0"): # u defines which sdef is printed: - if '_' in u: + if "_" in u: v = -1 - u = u.replace('_', '') - elif '+' in u: + u = u.replace("_", "") + elif "+" in u: v = 1 - u = u.replace('+', '') + u = u.replace("+", "") else: v = 1 - c = ['c x', 'c y', 'c z'] - if u in 'xX': - c[0] = ' ' - elif u in 'yY': - c[1] = ' ' - elif u in 'zZ': - c[2] = ' ' + c = ["c x", "c y", "c z"] + if u in "xX": + c[0] = " " + elif u in "yY": + c[1] = " " + elif u in "zZ": + c[2] = " " x1, x2, y1, y2, z1, z2 = params[:] dx = x2 - x1 dy = y2 - y1 dz = z2 - z1 - mx = (x1 + x2)*0.5 - my = (y1 + y2)*0.5 - mz = (z1 + z2)*0.5 - xs = mx - (dx*0.5 - d)*v - ys = my - (dy*0.5 - d)*v - zs = mz - (dz*0.5 - d)*v - if u in 'xX': - fmt = 'sdef x {:12} y d2 z d3 vec {} dir 1 wgt {}' - print(fmt.format(xs, '{} 0 0'.format(v), dz*dy), file=outstr) - elif u in 'yY': - fmt = 'sdef y {:12} x d1 z d3 vec {} dir 1 wgt {}' - print(fmt.format(ys, '0 {} 0'.format(v), dx*dz), file=outstr) - elif u in 'zZ': - fmt = 'sdef z {:12} x d1 y d2 vec {} dir 1 wgt {}' - print(fmt.format(zs, '0 0 {}'.format(v), dx*dy), file=outstr) - fm2 = 'si{:1} h {:12} {:12} $ {} {}' + mx = (x1 + x2) * 0.5 + my = (y1 + y2) * 0.5 + mz = (z1 + z2) * 0.5 + xs = mx - (dx * 0.5 - d) * v + ys = my - (dy * 0.5 - d) * v + zs = mz - (dz * 0.5 - d) * v + if u in "xX": + fmt = "sdef x {:12} y d2 z d3 vec {} dir 1 wgt {}" + print(fmt.format(xs, f"{v} 0 0", dz * dy), file=outstr) + elif u in "yY": + fmt = "sdef y {:12} x d1 z d3 vec {} dir 1 wgt {}" + print(fmt.format(ys, f"0 {v} 0", dx * dz), file=outstr) + elif u in "zZ": + fmt = "sdef z {:12} x d1 y d2 vec {} dir 1 wgt {}" + print(fmt.format(zs, f"0 0 {v}", dx * dy), file=outstr) + fm2 = "si{:1} h {:12} {:12} $ {} {}" print(fm2.format(1, x1 + d, x2 - d, dx, mx), file=outstr) print(fm2.format(2, y1 + d, y2 - d, dy, my), file=outstr) print(fm2.format(3, z1 + d, z2 - d, dz, mz), file=outstr) - fm3 = 'sp{:1} d 0 1' + fm3 = "sp{:1} d 0 1" print(fm3.format(1), file=outstr) print(fm3.format(2), file=outstr) print(fm3.format(3), file=outstr) + def print_spherical(s, r): """ s -- spherical surface number, r -- its radius. Radius is needed to compute weight for volume calculations. """ - print('sdef sur {} nrm -1 wgt {:12.7e}'.format(s, Pi * r**2), file=outstr) + print(f"sdef sur {s} nrm -1 wgt {Pi * r**2:12.7e}", file=outstr) + # Set of surface names to be checked for surface source candidates sset = set() - if args.s != '0': + if args.s != "0": sset = set(rin.expand(args.s.split())) # Try to find proper surfaces: - surfaces = dict(zip('xyzs', (None,)*4)) + surfaces = dict(zip("xyzs", (None,) * 4)) for c in cards: if c.ctype == mp.CID.cell: c.get_values() if c.ctype == mp.CID.surface: c.get_values() if not sset or c.name in sset: - if c.stype in ('px', 'py', 'pz', 'so', 's'): + if c.stype in ("px", "py", "pz", "so", "s"): # this is surface-candidate. Check its parameters: - k = c.stype.replace('p', '').replace('o', '') - if k == 'p': + k = c.stype.replace("p", "").replace("o", "") + if k == "p": v = c.scoefs[0] # plane position else: - v = c.scoefs[-1] # sphere radius + v = c.scoefs[-1] # sphere radius if surfaces[k] is None: surfaces[k] = (c.name, v, c.name, v) else: @@ -1320,77 +1340,73 @@ def print_spherical(s, r): for k, v in surfaces.items(): if v is not None: n1, v1, n2, v2 = v - print('c ', k, n1, v1, file=outstr) - print('c ', k, n2, v2, file=outstr) - elif k == 's' and args.u in 'sS': + print("c ", k, n1, v1, file=outstr) + print("c ", k, n2, v2, file=outstr) + elif k == "s" and args.u in "sS": # propose parameters of the circumscribing sphere - x = surfaces['x'] - y = surfaces['y'] - z = surfaces['z'] - cx = (x[1] + x[3])*0.5 - cy = (y[1] + y[3])*0.5 - cz = (z[1] + z[3])*0.5 - r = ((x[3] - x[1])**2 + - (y[3] - y[1])**2 + - (z[3] - z[1])**2)**(0.5) * 0.55 + x = surfaces["x"] + y = surfaces["y"] + z = surfaces["z"] + cx = (x[1] + x[3]) * 0.5 + cy = (y[1] + y[3]) * 0.5 + cz = (z[1] + z[3]) * 0.5 + r = ((x[3] - x[1]) ** 2 + (y[3] - y[1]) ** 2 + (z[3] - z[1]) ** 2) ** (0.5) * 0.55 # Next free surface number: - d = mn.get_numbers(cards) - ns = max(d['sur']) + 1 - nc = max(d['cel']) + 1 - print('c universe with circumscribing sphere', file=outstr) - print('{} 0 {} imp:n=1 imp:p=1 u=1 '.format(nc, -ns), file=outstr) - print('{} 0 {} imp:n=0 imp:p=0 u=1 '.format(nc+1, ns), file=outstr) - print('', file=outstr) - print('c Circumscribing sphere: ', file=outstr) + d = mp.get_numbers(cards) + ns = max(d["sur"]) + 1 + nc = max(d["cel"]) + 1 + print("c universe with circumscribing sphere", file=outstr) + print(f"{nc} 0 {-ns} imp:n=1 imp:p=1 u=1 ", file=outstr) + print(f"{nc + 1} 0 {ns} imp:n=0 imp:p=0 u=1 ", file=outstr) + print(file=outstr) + print("c Circumscribing sphere: ", file=outstr) print(ns, k, cx, cy, cz, r, file=outstr) surfaces[k] = (ns, r, ns, r) print_sdef = False # Process -u key - if args.u[-1] in 'xXyYzZ': + if args.u[-1] in "xXyYzZ": # planar source params = [] - for k in 'xyz': + for k in "xyz": if surfaces[k] is None: print(k, file=outstr) - raise ValueError('Planes not found for planar source') - else: - n1, v1, n2, v2 = surfaces[k] - params.extend([v1, v2]) + raise ValueError("Planes not found for planar source") + n1, v1, n2, v2 = surfaces[k] + params.extend([v1, v2]) print_planar(params, d=1e-5, u=args.u) - elif args.u == 's': - if surfaces['s'] is None: - raise ValueError('Spheres not found for spherical source') - else: - n1, v1, n2, v2 = surfaces['s'] + elif args.u == "s": + if surfaces["s"] is None: + raise ValueError("Spheres not found for spherical source") + n1, v1, n2, v2 = surfaces["s"] if print_sdef: print_spherical(n2, v2) - if args.c != '0': - print('c source from -c parameters', file=outstr) + if args.c != "0": + print("c source from -c parameters", file=outstr) vals = list(map(float, args.c.split())) if len(vals) == 6: # x, y and z range of a box: print_planar(vals, u=args.u) else: - raise ValueError('Wrong number of entries in the -c option') - elif args.mode == 'fillempty': + raise ValueError("Wrong number of entries in the -c option") + elif args.mode == "fillempty": # add 'FILL =' to all void non-filled cells. # N = ' fill={} '.format(args.u) N = args.u M = int(args.m) cll = [] # list of cell lists to be filled with new u - fl = [] # list of new u that fills cells from cll - if args.map != '': + fl = [] # list of new u that fills cells from cll + if args.map != "": # read from map list of cells where to insert the fill card for l in open(args.map): - if ':' in l: + if ":" in l: # l contains cell numbers and its filling - s1, s2 = l.split(':') + s1, s2 = l.split(":") cll.append(rin.expand(s1.split())) fl.append(s2) else: cll.append(rin.expand(l.split())) fl.append(N) - if args.c != '0': + if args.c != "0": cll.append(rin.expand(args.c.split())) fl.append(N) # put cll and fl into a single dict: @@ -1409,38 +1425,38 @@ def print_spherical(s, r): m = c.get_m() f = c.get_f() imp = c.get_imp() - if imp['imp:n'] > 0 and m == M and f in [0, None]: + if imp["imp:n"] > 0 and m == M and f in [0, None]: c.input[-1] += N - print(c.card(), end='', file=outstr) - elif args.mode == 'renum': + print(c.card(), end="", file=outstr) + elif args.mode == "renum": if args.map: - maps = lf.read_map_file(args.map, log=args.log != '') + maps = lf.read_map_file(args.map, log=args.log != "") else: maps = {} for c in cards: c.get_values() # index dictionary only if needed: - if 'i' in (args.c, args.s, args.m, args.u): - imaps = lf.get_indices(cards, log=args.log != '') - for t in ['cel', 'sur', 'mat', 'u', 'tr']: + if "i" in (args.c, args.s, args.m, args.u): + imaps = lf.get_indices(cards, log=args.log != "") + for t in ["cel", "sur", "mat", "u", "tr"]: # If command line paramters are specified, they rewrite maps # from the map file dn = getattr(args, t[0]) - if dn == 'i': + if dn == "i": maps[t] = imaps[t] - maps[t].doc = 'Indexing function for {}'.format(t) - maps[t].default = None # This will raise error if applied to non-existent value - elif dn != '0': - maps[t] = lf.LikeFunction(log=args.log != '') + maps[t].doc = f"Indexing function for {t}" + maps[t].default = None # This will raise error if applied to non-existent value + elif dn != "0": + maps[t] = lf.LikeFunction(log=args.log != "") maps[t].default = lf.add_func(int(dn)) # do not modify zero numbers (important for material # numbers) maps[t].mappings[lf.Range(0)] = lf.const_func(0) - maps[t].doc = 'Function for {} from command line'.format(t) + maps[t].doc = f"Function for {t} from command line" for c in cards: c.apply_map(maps) - print(c.card(), end='', file=outstr) - if args.log != '': + print(c.card(), end="", file=outstr) + if args.log != "": for k, m in maps.items(): m.write_log_as_map(k[0], args.log) @@ -1448,68 +1464,59 @@ def print_spherical(s, r): def main(args=sys.argv[1:]): - p = ap.ArgumentParser(prog='numjuggler', description=descr, epilog=epilog) - p.add_argument('--version', action='version', - version='%(prog)s {}'.format(version)) - p.add_argument('inp', help='MCNP input file') - p.add_argument('-c', help=help_c, - type=str, - default='0') - p.add_argument('-s', help=help_s.format('Surface'), - type=str, - default='0') - p.add_argument('-m', help=help_s.format('Material'), - type=str, - default='0') - p.add_argument('-u', help=help_s.format('Universe'), - type=str, - default='0') - p.add_argument('-t', help=help_s.format('Transformation'), - type=str, - default='0') - p.add_argument('-opt', help='remrp option. "cc" set all cells as complex cell; "all" remove all redundant parentheses ', - type=str, - choices=['nochg','cc','all'], - default='nochg') - p.add_argument('-all', help='remrp option. All redundant parentheses are removed', - action='store_true') - p.add_argument('--map', help=help_m, - type=str, - default='') - p.add_argument('--mode', help='Execution mode, "renum" by default', - type=str, - choices=modes, - default='renum') - p.add_argument('--debug', help='Additional output for debugging', - action='store_true') - p.add_argument('--preservetabs', - help='Do not convert tabs to spaces. By default tabs are replaced with spaces according to MCNP5 rules (User''s manual Vol. II p. 1-3)', - action='store_true') - p.add_argument('--log', help='Log file.', - type=str, - default='') + p = ap.ArgumentParser(prog="numjuggler", description=descr, epilog=epilog) + p.add_argument("--version", action="version", version=f"%(prog)s {version}") + p.add_argument("inp", help="MCNP input file") + p.add_argument("-c", help=help_c, type=str, default="0") + p.add_argument("-s", help=help_s.format("Surface"), type=str, default="0") + p.add_argument("-m", help=help_s.format("Material"), type=str, default="0") + p.add_argument("-u", help=help_s.format("Universe"), type=str, default="0") + p.add_argument("-t", help=help_s.format("Transformation"), type=str, default="0") + p.add_argument( + "-opt", + help='remrp option. "cc" set all cells as complex cell; "all" remove all redundant parentheses ', + type=str, + choices=["nochg", "cc", "all"], + default="nochg", + ) + p.add_argument( + "-all", help="remrp option. All redundant parentheses are removed", action="store_true" + ) + p.add_argument("--map", help=help_m, type=str, default="") + p.add_argument( + "--mode", + help='Execution mode, "renum" by default', + type=str, + choices=modes, + default="renum", + ) + p.add_argument("--debug", help="Additional output for debugging", action="store_true") + p.add_argument( + "--preservetabs", + help="Do not convert tabs to spaces. By default tabs are replaced with spaces according to MCNP5 rules (Users manual Vol. II p. 1-3)", + action="store_true", + ) + p.add_argument("--log", help="Log file.", type=str, default="") # parse help option in another parser: ph = ap.ArgumentParser(add_help=False) - ph.add_argument('-h', help='Print help and exit', - nargs='?', - default='', - const='gen') + ph.add_argument("-h", help="Print help and exit", nargs="?", default="", const="gen") harg, clo = ph.parse_known_args(args) if harg.h: - if harg.h == 'gen': + if harg.h == "gen": p.print_help() else: # try to read correspondent file from help folder try: import numjuggler as nj + dir1 = os.path.split(nj.__file__)[0] # remove filename - dir1 = os.path.split(dir1)[0] # remove the most deep dir - hlp = os.path.join(dir1, 'help/{}.rst'.format(harg.h)) - print('Reading help from {}'.format(hlp)) + dir1 = os.path.split(dir1)[0] # remove the most deep dir + hlp = os.path.join(dir1, f"numjuggler/help/{harg.h}.md.txt") + print(f"Reading help from {hlp}") print(open(hlp).read()) - except Exception as e: - print('Cannot read help file for "{}"'.format(harg.h)) + except Exception: + print(f'Cannot read help file for "{harg.h}"') # elif harg.h in dhelp: # print(dhelp[harg.h]) @@ -1525,38 +1532,39 @@ def main(args=sys.argv[1:]): # args.inp can be a path with folders. Ensure that the prefix # 'debug.juggler' is added to the base filename only. d, f = os.path.split(args.inp) - debug_file_name = os.path.join(d, 'debug.juggler.' + f) + debug_file_name = os.path.join(d, "debug.juggler." + f) # print # print 'Debug info written to ', debug_file_name # print - debuglog = open(debug_file_name, 'w') - print('command line arguments:', args, file=debuglog) + debuglog = open(debug_file_name, "w") + print("command line arguments:", args, file=debuglog) else: debuglog = None - # process input file only once: - cards = list(mp.get_cards(args.inp, - debuglog, - preservetabs=args.preservetabs)) + cards = list(mp.get_cards(args.inp, debuglog, preservetabs=args.preservetabs)) # processing based on selected mode of operation outstr = processing(args, cards, debuglog) # dump output string to stdout - print(outstr.getvalue(), end = '') + print(outstr.getvalue(), end="") # determine outut file name filename, _ = os.path.splitext(args.inp) - outfile = '.'.join((filename, args.mode + '.txt')) + outfile = ".".join((filename, args.mode + ".txt")) # dump output string after stripping trailing spaces outstr.seek(0) - with open(outfile, mode='wb') as fout: - fout.write('\n'.join([l.rstrip() for l in outstr]).encode(encoding='ascii', errors='backslashreplace')) + with open(outfile, mode="wb") as fout: + fout.write( + "\n".join([l.rstrip() for l in outstr]).encode( + encoding="ascii", errors="backslashreplace" + ) + ) outstr.close() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/numjuggler/mapparsers.py b/src/numjuggler/mapparsers.py similarity index 95% rename from numjuggler/mapparsers.py rename to src/numjuggler/mapparsers.py index 5efb844..7c3272c 100644 --- a/numjuggler/mapparsers.py +++ b/src/numjuggler/mapparsers.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- + +from __future__ import annotations from collections import OrderedDict @@ -13,7 +14,7 @@ def lines(fname): Empty lines and everything after `#` is skipped. """ - with open(fname, 'r') as f: + with open(fname) as f: for l in f: ll = l.split('#')[0].strip() if ll and ':' in ll: diff --git a/numjuggler/nogq.py b/src/numjuggler/nogq.py similarity index 98% rename from numjuggler/nogq.py rename to src/numjuggler/nogq.py index af9519b..89687c2 100644 --- a/numjuggler/nogq.py +++ b/src/numjuggler/nogq.py @@ -1,8 +1,7 @@ """ Functions to convert an arbitrary GQ cylinder into C/X+TR-defined cylinder. """ - -from __future__ import print_function +from __future__ import annotations try: # try because numpy might be unavailable. @@ -10,9 +9,9 @@ except ImportError: print("Numpy package is required for --mode nogq but cannot ") print("be found. Install it with ") - print("") + print() print(" > pip install numpy") - print("") + print() raise except: raise @@ -32,7 +31,7 @@ def get_k(p): # at least one of DEF is non-zero. ii = abs(DEF).argmax() iv = DEF[ii] - a2 = numpy.roll(ABC, 1)[ii] - DEF[DEF != iv].prod()/iv + a2 = numpy.roll(ABC, 1)[ii] - DEF[iv != DEF].prod()/iv # expression for gamma holds for any ABC and DEF: g = ABC.sum() - 3.0*a2 @@ -82,7 +81,6 @@ def get_a2(p): if F != 0.: a2dic['F'] = B - D*E/F*0.5 if D == E == F == 0.: - # pass for k, a2 in list(a2dic.items()): c = ABC - 2.0*a2 @@ -100,11 +98,11 @@ def is_gq_cylinder(p): # A, B and C are non-negative if a < 0 or b < 0 or c < 0: print(' not a cylinder since a, b or c is negative: ', p[0:3]) - raise ValueError() + raise ValueError if not numpy.isclose(d*e*f, -8.*(1.-a)*(1.-b)*(1.-c)): print(' D*E*F differs from -8(1-A)(1-B)(1-C)', p) - raise ValueError() + raise ValueError return 1.0/p[0:3].sum() * p diff --git a/numjuggler/nogq2.py b/src/numjuggler/nogq2.py similarity index 83% rename from numjuggler/nogq2.py rename to src/numjuggler/nogq2.py index 8a47193..fb75cea 100644 --- a/numjuggler/nogq2.py +++ b/src/numjuggler/nogq2.py @@ -1,7 +1,9 @@ -from __future__ import print_function, division, nested_scopes +from __future__ import annotations + import six + try: - import pirs.core.trageom.vector as vector + from pirs.core.trageom import vector except: vector = None @@ -24,14 +26,14 @@ def areclose(l, rtol=1e-4, atol=1e-7, cmnt=None, name='', detailed=True): ares = False astr = f1.format(atol) else: - ares = B - A <= atol + ares = atol >= B - A astr = f2.format(atol) if rtol is None: rres = False rstr = f1.format(rtol) else: b = max(map(abs, l)) - rres = B - A <= rtol*b + rres = rtol*b >= B - A rstr = f2.format(rtol*b) result = ares or rres @@ -39,12 +41,12 @@ def areclose(l, rtol=1e-4, atol=1e-7, cmnt=None, name='', detailed=True): if cmnt is not None: # assume it is a list of comments. Add here information c = cmnt.append - c('Are close check: ' + name + ': {}'.format(result)) + c('Are close check: ' + name + f': {result}') if detailed: - c(' values: ' + ' '.join('{:15.8e}'.format(v) for v in l)) - c(' B: {:15.8e}'.format(B)) - c(' A: {:15.8e}'.format(A)) - c(' B - A: {:15.8e}'.format(B - A)) + c(' values: ' + ' '.join(f'{v:15.8e}' for v in l)) + c(f' B: {B:15.8e}') + c(f' A: {A:15.8e}') + c(f' B - A: {B - A:15.8e}') c(' atol: ' + astr) c(' rtol*b: ' + rstr) return result @@ -69,10 +71,10 @@ def get_cone_or_cyl(pl): # Define normalization coeff gamma: A, B, C, D, E, F, G, H, J, K = pl ABC = sum((A, B, C)) - cmnt.append(' A, B, C:' + ' '.join('{:15.8e}'.format(v) for v in (A, B, C))) - cmnt.append(' D, E, F:' + ' '.join('{:15.8e}'.format(v) for v in (D, E, F))) - cmnt.append(' G, H, J:' + ' '.join('{:15.8e}'.format(v) for v in (G, H, J))) - cmnt.append(' K:' + ' {:15.8e}'.format(K)) + cmnt.append(' A, B, C:' + ' '.join(f'{v:15.8e}' for v in (A, B, C))) + cmnt.append(' D, E, F:' + ' '.join(f'{v:15.8e}' for v in (D, E, F))) + cmnt.append(' G, H, J:' + ' '.join(f'{v:15.8e}' for v in (G, H, J))) + cmnt.append(' K:' + f' {K:15.8e}') # List of normalization coefficients. 1 is always assumed gammas = [1.0, -1.0] @@ -82,43 +84,43 @@ def get_cone_or_cyl(pl): if areclose((1, B), atol=tABC, rtol=None): g = 1.0/B gammas.append(g) - cmnt.append('gamma_B = {:15.8e}'.format(g)) + cmnt.append(f'gamma_B = {g:15.8e}') if areclose((1, C), atol=tABC, rtol=None): g = 1.0/C gammas.append(g) - cmnt.append('gamma_C = {:15.8e}'.format(g)) + cmnt.append(f'gamma_C = {g:15.8e}') else: g = 2*E/(2*E*A - D*F) gammas.append(g) - cmnt.append('gamma_E = {:15.8e}'.format(g)) + cmnt.append(f'gamma_E = {g:15.8e}') if areclose((0, F), atol=tDEF, rtol=None): if areclose((1, A), atol=tABC, rtol=None): g = 1.0/A gammas.append(g) - cmnt.append('gamma_A = {:15.8e}'.format(g)) + cmnt.append(f'gamma_A = {g:15.8e}') if areclose((1, C), atol=tABC, rtol=None): g = 1.0/C gammas.append(g) - cmnt.append('gamma_C = {:15.8e}'.format(g)) + cmnt.append(f'gamma_C = {g:15.8e}') else: g = 2*F/(2*F*B - D*E) gammas.append(g) - cmnt.append('gamma_F = {:15.8e}'.format(g)) + cmnt.append(f'gamma_F = {g:15.8e}') if areclose((0, D), atol=tDEF, rtol=None): if areclose((1, B), atol=tABC, rtol=None): g = 1.0/B gammas.append(g) - cmnt.append('gamma_B = {:15.8e}'.format(g)) + cmnt.append(f'gamma_B = {g:15.8e}') if areclose((1, A), atol=tABC, rtol=None): g = 1.0/A gammas.append(g) - cmnt.append('gamma_A = {:15.8e}'.format(g)) + cmnt.append(f'gamma_A = {g:15.8e}') else: g = 2*D/(2*D*C - E*F) gammas.append(g) - cmnt.append('gamma_d = {:15.8e}'.format(g)) + cmnt.append(f'gamma_d = {g:15.8e}') # Ensure that gamma=1 is considered first gammas = set(gammas) @@ -135,19 +137,19 @@ def get_cone_or_cyl(pl): tt = t2**0.5 if t2 >= 0 else float('nan') rc = r2c**0.5 if r2c >= 0 else float('nan') rk = r2k**0.5 if r2k >= 0 else float('nan') - cmnt.append('gamma: 1 + {:15.8e}'.format(gamma - 1.0)) - cmnt.append(' t^2: {:15.8e}'.format(t2)) - cmnt.append(' t : {:15.8e}'.format(tt)) - cmnt.append(' n : ' + ' '.join('{:15.8e}'.format(v) for v in n)) - cmnt.append(' (n,n): {:15.8e}'.format(n2)) - cmnt.append(' r^2: {:15.8e} {:15.8e}'.format(r2c, r2k)) - cmnt.append(' r: {:15.8e} {:15.8e}'.format(rc, rk)) - cmnt.append(' R0c: ' + ' '.join('{:15.8e}'.format(v) for v in R0c)) - cmnt.append(' R0k: ' + ' '.join('{:15.8e}'.format(v) for v in R0k)) - cmnt.append(' (n,R0): {:15.8e} {:15.8e}'.format(R0cn, R0kn)) - cmnt.append(' (R0,R0): {:15.8e} {:15.8e}'.format(R0c2, R0k2)) - cmnt.append(' c1: {:15.8e}'.format(c1)) - cmnt.append(' c2: {:15.8e}'.format(c2)) + cmnt.append(f'gamma: 1 + {gamma - 1.0:15.8e}') + cmnt.append(f' t^2: {t2:15.8e}') + cmnt.append(f' t : {tt:15.8e}') + cmnt.append(' n : ' + ' '.join(f'{v:15.8e}' for v in n)) + cmnt.append(f' (n,n): {n2:15.8e}') + cmnt.append(f' r^2: {r2c:15.8e} {r2k:15.8e}') + cmnt.append(f' r: {rc:15.8e} {rk:15.8e}') + cmnt.append(' R0c: ' + ' '.join(f'{v:15.8e}' for v in R0c)) + cmnt.append(' R0k: ' + ' '.join(f'{v:15.8e}' for v in R0k)) + cmnt.append(f' (n,R0): {R0cn:15.8e} {R0kn:15.8e}') + cmnt.append(f' (R0,R0): {R0c2:15.8e} {R0k2:15.8e}') + cmnt.append(f' c1: {c1:15.8e}') + cmnt.append(f' c2: {c2:15.8e}') # Check parameters common for cone and cylinder if isnan(n2) or areclose((0, n2), atol=1e-4, rtol=None): @@ -224,14 +226,13 @@ def get_cone_or_cyl(pl): rsd = rsdc rsdmax = max(map(abs, sum(rsd.values(), ()))) - cmnt.append(' Residuals for {}, {:15.8e}'.format(typ, rsdmax)) + cmnt.append(f' Residuals for {typ}, {rsdmax:15.8e}') for d in distances: - cmnt.append(' at d={:10.3e}:'.format(d) + ' '.join('{:15.8e}'.format(v) for v in rsd[d])) + cmnt.append(f' at d={d:10.3e}:' + ' '.join(f'{v:15.8e}' for v in rsd[d])) if rsdmax > 1e-1: typ = 'o' continue - else: - cmnt.append(' Final max. residual for {}, {:15.8e}'.format(typ, rsdmax)) + cmnt.append(f' Final max. residual for {typ}, {rsdmax:15.8e}') cmnt = ['c ' + c for c in cmnt] return typ, n, org, t2, r2, cmnt diff --git a/numjuggler/numbering.py b/src/numjuggler/numbering.py similarity index 71% rename from numjuggler/numbering.py rename to src/numjuggler/numbering.py index 2f62efd..a5f057c 100644 --- a/numjuggler/numbering.py +++ b/src/numjuggler/numbering.py @@ -1,16 +1,22 @@ """ Functions to renumber cells, surfaces, etc. in MCNP input file. """ -from __future__ import print_function + +from __future__ import annotations + +from typing import Callable, TYPE_CHECKING import warnings -import collections + +if TYPE_CHECKING: + from pathlib import Path -class _Range(object): +class _Range: """ Represents a range or a point. """ + def __init__(self, n1, n2=None): if n2 is None: self.n1 = n1 @@ -19,21 +25,28 @@ def __init__(self, n1, n2=None): n1, n2 = sorted((n1, n2)) self.n1 = n1 self.n2 = n2 - return def __contains__(self, value): if self.n2 is None: return value == self.n1 - else: - return (self.n1 <= value <= self.n2) + return self.n1 <= value <= self.n2 def __str__(self): if self.n2 is None: return str(self.n1) - else: - return '[{} -- {}]'.format(self.n1, self.n2) + return f"[{self.n1} -- {self.n2}]" -class LikeFunction(object): + +MappingFunc = Callable[[int], int] +MappingItem = int | MappingFunc +MappingTuple = tuple[int, int, MappingFunc] +MappingTrivialMap = dict[int, int] +MappingResolutionList = list[MappingTuple] | MappingTrivialMap +MappingSpec = tuple[MappingItem, MappingResolutionList] +MappingDict = dict[str, MappingSpec] + + +class LikeFunction: """ Class of callables that take two arguments, a number (integer) and a type (char or string): @@ -50,7 +63,7 @@ class LikeFunction(object): > d['c'] = [dn0, rl] where dn0 is an integer or callable, and rl is a list of tuples - representing ranges and mappring on this range, or a dictionary representing + representing ranges and mapping on this range, or a dictionary representing mapping of separate values. In case rl is a list, its form is: > rl = [(n1, m1, dn1), (n1, m2, dn2), ...] @@ -70,42 +83,38 @@ class LikeFunction(object): callable, the mapping n -> dni(n) is applied. """ - def __init__(self, pdict, log=False): - self.__p = pdict - self.__lf = log # flag to log or not. - self.__ld = {} # here log is written, if log. - return + def __init__(self, pdict: MappingDict, log: bool = False): + self.__p = pdict + self.__lf = log # flag to log or not. + self.__ld = {} # here log is written, if log. @staticmethod - def __applyD(f, n): - if isinstance(f, collections.Callable): + def __applyD(f: MappingItem, n: int) -> MappingItem: + if isinstance(f, Callable): return f(n) - else: - return f + return f @staticmethod - def __applyL(f, n): - if isinstance(f, collections.Callable): + def __applyL(f: MappingItem, n: int) -> int: + if isinstance(f, Callable): return f(n) - else: - return n + int(f) + return n + int(f) - def __get_mapping(self, t): + def __get_mapping(self, t: str) -> MappingSpec: for key in [t, t[0]]: if key in self.__p: return self.__p[key] - else: - return None, None + return None, None - def __call__(self, n, t): + def __call__(self, n: int, t: str) -> int: dn0, param = self.__get_mapping(t) if (dn0, param) == (None, None): # type not found. Do not apply any mapping nnew = n # and do not log this mapping return n - elif isinstance(param, dict): + if isinstance(param, dict): # param is a dictionary of the form {nold: nnew} nnew = param.get(n, dn0) nnew = self.__applyD(nnew, n) @@ -123,16 +132,15 @@ def __call__(self, n, t): k = (t, nnew) if k in ld: if ld[k] != n: - warnings.warn('Non-injective mapping. ' + - '({}, {}) and ({}, {}) ' + - 'are mapped to {}'.format(t, ld[k], - t, n, nnew)) + warnings.warn( + f"Non-injective mapping. ({{}}, {{}}) and ({{}}, {{}}) are mapped to {t}" + ) else: ld[k] = n # check that void material not changed: - if t[0].lower() == 'm' and n == 0 and nnew != 0: - print('WARNING: material {} replaced with {}.'.format(n, nnew)) - print('Add cell density to the resulting input file.') + if t[0].lower() == "m" and n == 0 and nnew != 0: + print(f"WARNING: material {n} replaced with {nnew}.") + print("Add cell density to the resulting input file.") return nnew def write_log_as_map(self, fname): @@ -140,32 +148,18 @@ def write_log_as_map(self, fname): Writes log to fname in format of map file. """ d = {} - for t in 'csmut': + for t in "csmut": d[t] = {} for (t, nnew), n in list(self.__ld.items()): d[t[0]][n] = nnew - with open(fname, 'w') as f: - for t in 'csmut': - print('-'*80, file=f) + with open(fname, "w") as f: + for t in "csmut": + print("-" * 80, file=f) for n in sorted(d[t].keys()): nnew = d[t][n] if nnew != n: - print('{} {:>6d}: {:>6d}'.format(t, nnew, n), file=f) - - -def get_numbers(scards): - """ - Return dictionary with keys -- number types and values -- list of numbers - used in the input file. - """ - r = {} - for c in scards: - for v, t in c.values: - if t not in r: - r[t] = [] - r[t].append(v) - return r + print(f"{t} {nnew:>6d}: {n:>6d}", file=f) def get_indices(scards): @@ -197,16 +191,16 @@ def get_indices(scards): def _get_ranges_from_set(nn): nnl = sorted(nn) - if nnl: # nnl can be empty + if nnl: # nnl can be empty if [e for e in nn if not isinstance(e, int)]: # for float elements of nn only one range, (min, max), is returned yield (nnl[0], nnl[-1]) else: n1 = nnl.pop(0) # start of 1-st range - np = n1 # previous item + np = n1 # previous item while nnl: n = nnl.pop(0) - if np in [n-1, n]: + if np in [n - 1, n]: # range is continued np = n else: @@ -216,7 +210,7 @@ def _get_ranges_from_set(nn): yield (n1, np) -def read_map_file(fname): +def read_map_file(fname: str | Path): """ Read map file and return functions to be used for mapping. @@ -228,19 +222,15 @@ def read_map_file(fname): c: 50 # default cell offset. If not specified, it is 0. """ # type short names and accepted types: - td = {'c': 'cel', - 's': 'sur', - 'u': 'u', - 't': 'tr', - 'm': 'mat'} + td = {"c": "cel", "s": "sur", "u": "u", "t": "tr", "m": "mat"} d = {} for k in list(td.keys()): d[td[k]] = [0, []] # default dn and list of ranges. - with open(fname, 'r') as f: + with open(fname) as f: for l in f: ll = l.lower().lstrip() - if ll and ll[0] in list(td.keys()) and ':' in ll: + if ll and ll[0] in list(td.keys()) and ":" in ll: t, ranges, s, dn = _parse_map_line(ll) t = td[t] @@ -279,23 +269,23 @@ def read_map_file(fname): return d -def _parse_map_line(l): +def _parse_map_line(l: str): """ - For the map lie returns t, list of ranges and dn. + For the map line returns t, list of ranges and dn. """ # range type t = l[0] - rs, os = l[1:].split(':') + rs, os = l[1:].split(":") # Allow commas and no spaces in ranges - rs = rs.replace('--', ' -- ').replace(',' ' ') + rs = rs.replace("--", " -- ").replace(", ") # Use only 1-st entry in the map rule os = os.split()[0].lstrip() # Sign and dn dn = int(os) - if os[0] in '-+': + if os[0] in "-+": sign = True else: sign = False @@ -304,24 +294,19 @@ def _parse_map_line(l): return t, ranges, sign, dn -def _get_map_ranges(s): - tl = (s + ' 0').split() +def _get_map_ranges(s: str): + tl = (s + " 0").split() v1 = None is_range = False for t in tl: - if t == '--': + if t == "--": is_range = True + elif is_range: + yield v1, int(t) + v1 = None + is_range = False else: - if is_range: - yield v1, int(t) - v1 = None - is_range = False - else: - if v1 is not None: - yield v1, v1 - v1 = int(t) - - -if __name__ == '__main__': - pass + if v1 is not None: + yield v1, v1 + v1 = int(t) diff --git a/numjuggler/parser.py b/src/numjuggler/parser.py similarity index 56% rename from numjuggler/parser.py rename to src/numjuggler/parser.py index 7e950e2..df2163a 100644 --- a/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -1,53 +1,53 @@ -# -*- coding: utf-8 -*- - """ Functions for parsing MCNP input files. """ -from __future__ import print_function +from __future__ import annotations + +from typing import Iterable, Literal, TextIO, TYPE_CHECKING import re import warnings -import six -import os -from chardet import UniversalDetector + from io import StringIO -from numjuggler import PartialFormatter +from pathlib import Path + +from chardet import UniversalDetector + +from numjuggler.utils import PartialFormatter + +if TYPE_CHECKING: + from numbers import Number -try: - # This clause define the fallback for cPickle, which is an accelerated - # version of pickle in Python2. In Python3 the acceleration is considered - # to be package-internal details, therefore the whole clause is an overkill - # -- an accelerated version will be imported with pickle, if available. - import cPickle -except ImportError: - import pickle as cPickle # integer with one prefix character -re_int = re.compile(r'\D{0,1}\d+') +re_int = re.compile(r"\D{0,1}\d+") # interior of square brackets for tally in lattices -re_ind = re.compile(r'\[.+\]', flags=re.DOTALL) +re_ind = re.compile(r"\[.+\]", flags=re.DOTALL) # repitition syntax of MCNP input file -re_rpt = re.compile(r'\d+[ri]', flags=re.IGNORECASE) +re_rpt = re.compile(r"\d+[ri]", flags=re.IGNORECASE) # Regexp for parameters to be hidden, e.g. tmp, imp:, vol. -re_prm = re.compile(r""" +re_prm = re.compile( + r""" ((?: imp:[npe] | tmp | vol) \s* ={0,1} \s*) # Value prefix, e.g. "imp:n=", "tmp ", "vol = " (\S+) # Value itself - """, re.IGNORECASE + re.VERBOSE) + """, + re.IGNORECASE + re.VERBOSE, +) # fill keyword -re_fll = re.compile(r'\*{0,1}fill[=\s]+', flags=re.IGNORECASE) # TODO: this will also match fill=== +re_fll = re.compile(r"\*{0,1}fill[=\s]+", flags=re.IGNORECASE) # TODO: this will also match fill=== # If type specifier not given, any data type can be formatted: def fmt_gen(s): - return '{' + ':<{}'.format(len(s)) + '}' + return "{" + f":<{len(s)}" + "}" fmt_d = fmt_gen @@ -57,7 +57,7 @@ def fmt_gen(s): partial_formmatter = PartialFormatter() -class __CIDClass(object): +class __CIDClass: """ There are two levels of card types. 1-st level is purely defined by card position in the input file. There can be: @@ -75,6 +75,7 @@ class __CIDClass(object): This class is to describe the 1-st level of card types. """ + # no-information cards comment = -1 blankline = -2 @@ -86,168 +87,206 @@ class __CIDClass(object): data = 5 @classmethod - def get_name(cls, cid): + def get_name(cls, cid: str) -> str: """ Return the name of the card type by its index. """ for k, v in list(cls.__dict__.items()): - if '__' not in k and v == cid: + if "__" not in k and v == cid: return k - else: - print('No attribute with name', cid) - raise ValueError() + msg = f"No CID names with value {cid}" + raise ValueError(msg) CID = __CIDClass() -class Card(object): +class Card: """ - Representation of a card. + Representation of an MCNP card. + + Attributes + ---------- + + lines + original lines of an MCNP specification + ctype + card type by its position in the input + pos + Input file line number, where the card was found + debug, optional + File-like object to write debug info (default None) + cstrg + True if self.lines has changed after initialization + used in remove_hash function (default False) + dtype + data card type. Defined from the get_values() method. + Has sense only to data cards (see ctype). + For other card types is None. + template + template string. Represents the general structure of the card. It is + a copy of lines, but meaningful parts are replaced by format + specifiers, {} + input + List of strings represenging meaningful parts of the card. The + original multi-line string card is obtained as + template.format(*input) + hidden + Dictionary of parts that are removed from input before processing it. + For example, repitition syntax (e.g. 5r or 7i) is replaced with '!' + to prevent its modification. + values + List of (value, value_type) tuples """ - def __init__(self, lines, ctype, pos, debug=None): - # Original lines, as read from the input file + def __init__( + self, lines: list[str], ctype: Literal[1, 2, 3, 4, 5], pos: int, debug: TextIO | None = None + ): + """Setup Card instance. + + Parameters + ---------- + lines + Original lines, as read from the input file + ctype + card type by its position in the input + pos + Input file line number, where the card was found + debug, optional + Optional file-like object to write debug info, by default None + """ + self.lines = lines + """Original lines, as read from the input file""" - # card type by its position in the input. See CID class. self.ctype = ctype + """card type by its position in the input. + See CID class""" - # True if self.lines has changed after initialization - # used in remove_hash function self.cstrg = False + """True if self.lines has changed after initialization + used in remove_hash function""" - # data card type. Defined from the get_values() method. - # Has sense only to data cards (see ctype). For other card types - # is None. self.dtype = None + """data card type. Defined from the get_values() method. + Has sense only to data cards (see ctype). For other card types is None.""" - # Input file line number, where the card was found. self.pos = pos + """Input file line number, where the card was found.""" - # File-like object to write debug info (if not None) self.debug = debug + """File-like object to write debug ino (optional)""" - # template string. Represents the general structure of the card. It is - # a copy of lines, but meaningful parts are replaced by format - # specifiers, {} - self.template = '' + self.template: str = "" + """template string. Represents the general structure of the card. It is + a copy of lines, but meaningful parts are replaced by format + specifiers, {}""" - # List of strings represenging meaningful parts of the card. The - # original multi-line string card is obtained as - # template.format(*input) - self.input = [] + self.input: list[str] = [] + """List of strings represenging meaningful parts of the card. The + original multi-line string card is obtained as + template.format(*input)""" - # Dictionary of parts that are removed from input before processing it. - # For example, repitition syntax (e.g. 5r or 7i) is replaced with '!' - # to prevent its modification. self.hidden = {} + """Dictionary of parts that are removed from input before processing it. + For example, repitition syntax (e.g. 5r or 7i) is replaced with '!' + to prevent its modification.""" - # List of (v, t) tuples, where v -- value and t -- its type. - self.values = [] - - # geometry prefix and suffix - # self.geom_prefix = '' - # self.geom_suffix = '' + self.values: list[tuple[int, str]] = [] + """List of (v, t) tuples, where v -- value and t -- its type.""" # some properties defined on demand # cell properties self.__u = -1 # -1 means undefined. None -- not specified in input self.__f = -1 # fill self.__m = -1 # material - self.__d = '' # density + self.__d = "" # density self.__i = -1 # importances self.__cr = -1 # set of reference cells. # surface properties - self.__st = '' # '' means undefined. + self.__st = "" # '' means undefined. # Split card to template and meaningful part is always needed. Other # operations are optional. self.get_input() - return def _get_value_by_type(self, t): """ Returns the first value of type t found in self.values. """ - vl, tl = zip(*self.values) + vl, tl = zip(*self.values, strict=False) try: i = tl.index(t) except ValueError: return None - finally: - return vl[i] + return vl[i] def _set_value_by_type(self, t, v): """ Sets the first value of type t to v in self.values. """ - vl, tl = zip(*self.values) + _vl, tl = zip(*self.values, strict=False) i = tl.index(t) self.values[i] = (v, t) @property def geom_prefix(self): - return self._get_value_by_type('#gpr') + return self._get_value_by_type("#gpr") @geom_prefix.setter def geom_prefix(self, value): - return self._set_value_by_type('#gpr', value) + return self._set_value_by_type("#gpr", value) @property def geom_suffix(self): - return self._get_value_by_type('#gsu') + return self._get_value_by_type("#gsu") @geom_suffix.setter def geom_suffix(self, value): - return self._set_value_by_type('#gsu', value) + return self._set_value_by_type("#gsu", value) - def print_debug(self, comment, key='tihv'): + def print_debug(self, comment, key="tihv"): d = self.debug if d: - print('Line {}, {} card. {}'.format(self.pos, - CID.get_name(self.ctype), - comment), file=d) - if 't' in key: - print(' template:', repr(self.template), file=d) - if 'i' in key: - print(' input: ', self.input, file=d) - if 'h' in key: - print(' hidden: ', self.hidden, file=d) - if 'v' in key: - print(' values: ', self.values, file=d) - - def get_input(self, check_bad_chars=False): + print(f"Line {self.pos}, {CID.get_name(self.ctype)} card. {comment}", file=d) + if "t" in key: + print(" template:", repr(self.template), file=d) + if "i" in key: + print(" input: ", self.input, file=d) + if "h" in key: + print(" hidden: ", self.hidden, file=d) + if "v" in key: + print(" values: ", self.values, file=d) + + def get_input(self, check_bad_chars: bool = False) -> None: """ Recompute template, input and hidden attributes from lines """ - mline = ''.join(self.lines) + mline = "".join(self.lines) if check_bad_chars: - bad_chars = '\t' + bad_chars = "\t" for c in bad_chars: if c in mline: if self.debug: - self.print_debug('get_input: bad char in input cards', - '') + self.print_debug("get_input: bad char in input cards", "") else: - raise ValueError('Bad character in input file. ' + - 'Run with --debug option.') + raise ValueError("Bad character in input file. Run with --debug option.") if self.ctype in (CID.comment, CID.blankline): # nothing to do for comments or blanklines: - self.input = '' + self.input = "" self.template = mline else: # TODO: protect { and } in comment parts of the card. tmpl = [] # part of template inpt = [] # input, meaningful parts of the card. - if mline.split()[0][:2] == 'fc': + if mline.split()[0][:2] == "fc": # this is tally comment. It always in one line and is not # delimited by & or $ i = mline[:80] - t = mline.replace(i, '{}', 1) + t = mline.replace(i, "{}", 1) inpt = [i] tmpl = [t] else: @@ -257,7 +296,7 @@ def get_input(self, check_bad_chars=False): else: # entries optionally delimited from comments by $ or & # requires that delimiters prefixed with space - d = index_(l, '$&') + d = index_(l, "$&") # d1 = l.find(' $') # d2 = l.find(' &') # if -1 < d1 and -1 < d2: @@ -275,12 +314,11 @@ def get_input(self, check_bad_chars=False): inpt.append(i) tmpl.append(fmt_s(i) + t) self.input = inpt - self.template = ''.join(tmpl) + self.template = "".join(tmpl) # TODO: dtype and name of the card can be defined already here. - self.print_debug('get_input', 'ti') - return + self.print_debug("get_input", "ti") def _protect_nums(self): """ @@ -288,52 +326,53 @@ def _protect_nums(self): represent cell, surface or a cell parameter with some unused char. """ - inpt = '\n'.join(self.input) + inpt = "\n".join(self.input) d = {} # in cell card: if self.ctype == CID.cell: # and 'like' not in inpt: - d['~'] = [] # float values in cells + d["~"] = [] # float values in cells # Replace material density - if 'like' not in inpt: - tokens = inpt.replace('=', ' ').split() + if "like" not in inpt.lower(): + tokens = inpt.replace("=", " ").split() cell, mat, rho = tokens[:3] if int(mat) != 0: for s in (cell, mat, rho): - inpt = inpt.replace(s, '~', 1) - inpt = inpt.replace('~', cell, 1) - inpt = inpt.replace('~', mat, 1) - d['~'].append(rho) + inpt = inpt.replace(s, "~", 1) + inpt = inpt.replace("~", cell, 1) + inpt = inpt.replace("~", mat, 1) + d["~"].append(rho) # hide parameters that not to be changed: for s1, s2 in re_prm.findall(inpt): - d['~'].append(s2) - inpt = inpt.replace(s1 + s2, s1 + '~', 1) + d["~"].append(s2) + inpt = inpt.replace(s1 + s2, s1 + "~", 1) # replace repitition syntax in junks: sbl = re_rpt.findall(inpt) if sbl: for s in sbl: - inpt = inpt.replace(s, '!', 1) - d['!'] = sbl - - if (self.ctype == CID.data and - inpt.lstrip().lower()[0] == 'f' and - inpt.lstrip()[1].isdigit()): + inpt = inpt.replace(s, "!", 1) + d["!"] = sbl + + if ( + self.ctype == CID.data + and inpt.lstrip().lower()[0] == "f" + and inpt.lstrip()[1].isdigit() + ): # this is tally card. Hide indexes in square brackets sbl = re_ind.findall(inpt) if sbl: for s in sbl: - inpt = inpt.replace(s, '|', 1) - d['|'] = sbl + inpt = inpt.replace(s, "|", 1) + d["|"] = sbl - self.input = inpt.split('\n') + self.input = inpt.split("\n") self.hidden = d - self.print_debug('_protect_nums', 'ih') - return + self.print_debug("_protect_nums", "ih") def get_values(self): """ @@ -352,7 +391,7 @@ def get_values(self): elif self.ctype == CID.data: inpt, vt, dtype = _split_data(self.input) self.dtype = dtype - if dtype == 'TRn': + if dtype == "TRn": unit, inpt, fvals = _parse_tr(inpt) self.unit = unit vt += fvals @@ -365,8 +404,7 @@ def get_values(self): self.input = inpt self.values = vt - self.print_debug('get_values', 'iv') - return + self.print_debug("get_values", "iv") def get_refcells(self): """ @@ -376,13 +414,12 @@ def get_refcells(self): return None if self.__cr != -1: return self.__cr - else: - s = set() - for v, t in self.values: - if t == 'cel': - s.add(v) - self.__cr = s - return self.__cr + s = set() + for v, t in self.values: + if t == "cel": + s.add(v) + self.__cr = s + return self.__cr def get_geom(self): """ @@ -390,9 +427,9 @@ def get_geom(self): string. """ p, s = self.geom_prefix, self.geom_suffix - self.geom_prefix = '§' - self.geom_suffix = '§' - geom = self.card().split('§')[1] + self.geom_prefix = "§" + self.geom_suffix = "§" + geom = self.card().split("§")[1] self.geom_prefix = p self.geom_suffix = s return geom @@ -405,15 +442,14 @@ def get_u(self): return None if self.__u != -1: return self.__u + # get it only once: + for v, t in self.values: + if t == "u": + self.__u = v + break else: - # get it only once: - for v, t in self.values: - if t == 'u': - self.__u = v - break - else: - self.__u = None - return self.__u + self.__u = None + return self.__u def get_m(self): """ @@ -424,38 +460,36 @@ def get_m(self): if self.__m != -1: return self.__m + if "like" in "".join(self.input).lower(): + # material name should be given in another cell. + pass + for v, t in self.values: + if t == "mat": + self.__m = v + break else: - if 'like' in ''.join(self.input).lower(): - # material name should be given in another cell. - pass - for v, t in self.values: - if t == 'mat': - self.__m = v - break - else: - # raise ValueError("Cell does not have material specs") - self.__m = -2 - return self.__m + # raise ValueError("Cell does not have material specs") + self.__m = -2 + return self.__m def get_d(self): """ For cell card return density """ - if self.__d != '': + if self.__d != "": return self.__d if self.get_m() == 0: self.__d = 0 return self.__d - elif self.get_m() == -2: + if self.get_m() == -2: # this is like-but cell - self.__d = -100. - return self.__d - else: - # density entry is hidden in the input and available as the 1-st - # entry in self.hidden dictionary. - self.__d = float(self.hidden['~'][0]) + self.__d = -100.0 return self.__d + # density entry is hidden in the input and available as the 1-st + # entry in self.hidden dictionary. + self.__d = float(self.hidden["~"][0]) + return self.__d def set_d(self, v): """ @@ -464,7 +498,7 @@ def set_d(self, v): It is assumed that get_values() method is called before this. """ if self.get_m() > 0: - self.hidden['~'][0] = v + self.hidden["~"][0] = v self.__d = float(v) def get_f(self, newv=None): @@ -476,61 +510,71 @@ def get_f(self, newv=None): if self.__f != -1 and newv is None: return self.__f + # get it only once: + for i in range(len(self.values)): + v, t = self.values[i] + if t == "fill": + if newv is not None: + v = newv + self.values[i] = (v, t) + self.__f = v + break else: - # get it only once: - for i in range(len(self.values)): - v, t = self.values[i] - if t == 'fill': - if newv is not None: - v = newv - self.values[i] = (v, t) - self.__f = v - break - else: - self.__f = None - return self.__f - - def get_imp(self, vals={}): - """ - Returns importances, if explicitly specified in the cell card. + self.__f = None + return self.__f + + def get_imp( + self, vals: dict[Literal["n", "p", "e"], float] | None = None + ) -> dict[Literal["n", "p", "e"], float]: + """Get this cell importance values. + + Parameters + ---------- + vals + updates for importance values + + Returns + ------- + importances, if explicitly specified in the cell card. """ + if vals is None: + vals = {} if self.ctype != CID.cell: return None if self.__i != -1 and not vals: return self.__i - else: - res = {} - inpt = ' '.join(self.input).lower() - for p in 'npe': - key = 'imp:' + p - - s = inpt.split(key) - if len(s) == 1: - # there is no key in the input line. - continue - else: - n = s[0].count('~') - res[key] = float(self.hidden['~'][n]) - if p in vals: - # change value only if necessary - if res[key] != vals[p]: - res[key] = vals[p] - self.hidden['~'][n] = str(vals[p]) - - # for s in self.hidden.get('~', []): - # sl = s.lower() - # if key in sl: - # val = float(sl.replace(key, '').replace('=', '')) - # res[key] = val - if not res: - res['imp:n'] = 1 - self.__i = res - return self.__i - - def remove_fill(self): - """ - Removes the FILL= keyword of a cell card. + res = {} + inpt = " ".join(self.input).lower() + for p in "npe": + key = "imp:" + p + + s = inpt.split(key) + if len(s) == 1: + # there is no key in the input line. + continue + n = s[0].count("~") + hidden_item = float(self.hidden["~"][n]) + res[key] = hidden_item + # change value only if necessary + if vals and p in vals: + new_value = vals[p] + if hidden_item != new_value: + res[key] = new_value + self.hidden["~"][n] = str(new_value) + + # for s in self.hidden.get('~', []): + # sl = s.lower() + # if key in sl: + # val = float(sl.replace(key, '').replace('=', '')) + # res[key] = val + if not res: + res["imp:n"] = 1 + self.__i = res + return self.__i + + def remove_fill(self) -> None: + """Remove the FILL= keyword and its values from this cell card. This method must be called after get_values(). """ @@ -552,41 +596,48 @@ def remove_fill(self): # replace with spaces all FILL-related tokens vals = [] # new values list. oldv = self.values[:] - state = 'before' + state = "before" while oldv: v, t = oldv.pop(0) - if state == 'before' and t == 'fill': - v = ' ' - state = 'afterU' - elif state == 'afterU' and '(' in t: - v = ' ' - state = 'after(' - elif state == 'after(': - v = ' ' - if ')' in t: - state = 'after' + if state == "before" and t == "fill": + v = " " + state = "afterU" + elif state == "afterU" and "(" in t: + v = " " + state = "after(" + elif state == "after(": + v = " " + if ")" in t: + state = "after" vals.append((v, t)) self.values = vals # Remove FILL from the input for n, i in enumerate(self.input): - if 'fill' in i.lower(): + if "fill" in i.lower(): # This part of input contains the fill keyword. This keyword is # optionally prepended with an asterix and followed by a sign - i = re_fll.sub(' ', i) - self.input[n] = i + _i = re_fll.sub(" ", i) + self.input[n] = _i break - self.print_debug('remove_fill', 'iv') - return + self.print_debug("remove_fill", "iv") - def card(self, wrap=False, comment=True): - """ - Return multi-line string representing the card. + def card(self, wrap: bool = False, comment: bool = True) -> str: + """Present this card as text. + + Parameters + ---------- + wrap + comment + + Returns + ------- + multi-line string representing the card. """ if self.input: # put values back to meaningful parts: - inpt = '\n'.join(self.input) + inpt = "\n".join(self.input) inpt = inpt.format(*[t[0] for t in self.values]) # put back hidden parts: @@ -594,56 +645,53 @@ def card(self, wrap=False, comment=True): for v in vl: inpt = inpt.replace(k, v, 1) - inpt = inpt.split('\n') + inpt = inpt.split("\n") if not comment: - return ' '.join(inpt) + return " ".join(inpt) if wrap: # and self.ctype != CID.title: - indent = ' '*5 + indent = " " * 5 if self.ctype == CID.title: - indent = 'c' + indent - tparts = re.split(r'\{.*?\}', self.template)[1:] + indent = "c" + indent + tparts = re.split(r"\{.*?\}", self.template)[1:] # print 'wrapped inp', repr(self.template) # print 'wrapped spl', repr(tparts) - newt = [''] # new template parts - newi = [] # new input parts - self.print_debug('card wrap=True', '') - for i, t in zip(inpt, tparts): - self.print_debug(' ' + repr(i) + repr(t), '') + newt = [""] # new template parts + newi = [] # new input parts + self.print_debug("card wrap=True", "") + for _i, t in zip(inpt, tparts, strict=False): + i = _i + self.print_debug(" " + repr(i) + repr(t), "") il = [] tl = [t] # while len(i.rstrip()) > 79: while len(i.rstrip()) > 80: # first try to shift to left - if i[:5] == ' '*5: - i = ' '*5 + i.lstrip() + if i[:5] == " " * 5: + i = " " * 5 + i.lstrip() if len(i.rstrip()) > 79: # input i must be wrapped. Find proper place: - for dc in ' :': + for dc in " :": k = i.rstrip().rfind(dc, 0, 75) if k > 6: il.append(i[:k]) - tl.append('\n') + tl.append("\n") i = indent + i[k:] - self.print_debug('card wrap=True' + - repr(il[-1]) + - repr(i), '') + self.print_debug("card wrap=True" + repr(il[-1]) + repr(i), "") break else: # there is no proper place to wrap. - self.print_debug('Cannot wrap line ' + - repr(i), '') - warnings.warn('Cannot wrap card' - ' on line {}'.format(self.pos)) + self.print_debug("Cannot wrap line " + repr(i), "") + warnings.warn(f"Cannot wrap card on line {self.pos}", stacklevel=2) break else: # input i fits to one line. Do nothing. pass newt += tl - newi += il + [i] - tmpl = '{}'.join(newt) + newi += [*il, i] + tmpl = "{}".join(newt) inpt = newi else: tmpl = self.template @@ -658,89 +706,63 @@ def remove_spaces(self): """ Remove extra spaces from meaningful parts. """ - self.print_debug('before remove_spaces', 'i') + self.print_debug("before remove_spaces", "i") if self.ctype in (CID.cell, CID.surface, CID.data): inpt = [] - for i in self.input: - indented = i[:5] == ' '*5 + for _i in self.input: + i = _i + indented = i[:5] == " " * 5 # leave only one sep. space - i = ' '.join(i.split()) + i = " ".join(i.split()) i = i.strip() # spaces before/after some characters are not needed: - for c in '):': - i = i.replace(' ' + c, c) - for c in '(:': - i = i.replace(c + ' ', c) + for c in "):": + i = i.replace(" " + c, c) + for c in "(:": + i = i.replace(c + " ", c) if indented: - i = ' '*5 + i + i = " " * 5 + i inpt.append(i) - self.print_debug(i, '') + self.print_debug(i, "") self.input = inpt - self.print_debug('after remove_spaces', 'i') - return + self.print_debug("after remove_spaces", "i") def apply_map(self, f): """ Replace Ni in self.values by Mi = f(Ni, Ti). """ - self.print_debug('before apply_map', 'vi') + self.print_debug("before apply_map", "vi") # u and fill should be renumberd in the same way, but types # must remain different, to let explicit u=0 # self.values = map(lambda t: (f(t[0], t[1]), t[1]), self.values) newvals = [] for t in self.values: - if t[1] == 'fill': - t1 = 'u' - else: - t1 = t[1] + t1 = "u" if t[1] == "fill" else t[1] # newvals.append((f(t[0], t1), t[1])) - if t1 in f: - newval = f[t1](t[0]) - else: - newval = t[0] + newval = f[t1](t[0]) if t1 in f else t[0] newvals.append((newval, t[1])) self.values = newvals - self.print_debug('after apply_map', 'vi') - return - - -# def _parse_geom(geom): -# """ -# Parse the geometry part of a cell card. -# """ -# raise NotImplementedError() -# t = geom.split() -# vals = [] -# fmts = [] -# -# # cell name -# js = t.pop(0) -# geom = geom.replace(js, tp, 1) -# vals.append((int(js), 'cel')) -# fmts.append(fmt_d(js)) -# -# if 'like' in geom.lower(): -# # this is like-but syntax -# pass -# else: -# # get material and density. -# # Density, if specified in cells card, should be allready hidden -# ms = t.pop(0) -# if int(ms) == 0: -# inpt = inpt.replace(ms, tp+tp , 1) -# else: -# inpt = inpt.replace(ms, tp, 1) -# inpt = inpt.replace('~', '~'+tp, 1) -# vals.append((int(ms), 'mat')) -# fmts.append(fmt_d(ms)) -# -# # placeholder for geometry prefix -# vals.append(('', '#gpr')) -# fmts.append('{}') - - -def _split_cell(input_, self): + self.print_debug("after apply_map", "vi") + + +def get_numbers(scards: list[Card]) -> dict[str, list[int]]: + """Collect type->numbers map. + + Returns + ------- + dictionary with card types as keys and list of numbers as values + """ + r = {} + for c in scards: + for v, t in c.values: + if t not in r: + r[t] = [] + r[t].append(v) + return r + + +def _split_cell(input_, _self): """ Replace integers in the meaningful parts of a cell card with format specifiers, and return a list of replaced values together with their types. @@ -751,37 +773,36 @@ def _split_cell(input_, self): # all of them should land to the card template, therefore, after all # entries are replaced with format specifiers, it can be split back to a # list easily at \n positions. - inpt = '\n'.join(input_) + inpt = "\n".join(input_) vals = [] # list of values fmts = [] # value format. It has digits, thus inserted into inpt later. - tp = '_' # temporary placeholder for format specifiers + tp = "_" # temporary placeholder for format specifiers # Parse part before parameters. This is different for usual and like-but # syntax. As result, all entries are replaced in inpt and i, the index # where parameter's part starts in inpt, is computed. - if 'like ' in inpt.lower(): - + if "like " in inpt.lower(): # Get cell name t = inpt.split() js = t.pop(0) inpt = inpt.replace(js, tp, 1) - vals.append((int(js), 'cel')) + vals.append((int(js), "cel")) fmts.append(fmt_d(js)) # Get reference cell name: t.pop(0) # like js = t.pop(0) inpt = inpt.replace(js, tp, 1) - vals.append((int(js), 'cel')) + vals.append((int(js), "cel")) fmts.append(fmt_d(js)) # compute i -- where first param token starts t.pop(0) # but p0 = t.pop(0) i = inpt.index(p0) - parm = [p0] + t + parm = [p0, *t] else: # cell card has usual format. @@ -791,23 +812,23 @@ def _split_cell(input_, self): # Get cell name js = t.pop(0) inpt = inpt.replace(js, tp, 1) - vals.append((int(js), 'cel')) + vals.append((int(js), "cel")) fmts.append(fmt_d(js)) # get material and density. # Density, if specified in cells card, should be allready hidden ms = t.pop(0) if int(ms) == 0: - inpt = inpt.replace(ms, tp+tp, 1) + inpt = inpt.replace(ms, tp + tp, 1) else: inpt = inpt.replace(ms, tp, 1) - inpt = inpt.replace('~', '~'+tp, 1) - vals.append((int(ms), 'mat')) + inpt = inpt.replace("~", "~" + tp, 1) + vals.append((int(ms), "mat")) fmts.append(fmt_d(ms)) # placeholder for geometry prefix - vals.append(('', '#gpr')) - fmts.append('{}') + vals.append(("", "#gpr")) + fmts.append("{}") # Get geometry and parameters blocks. I assume that geom and param # blocks are separated by at least one space, so there will be an @@ -817,18 +838,18 @@ def _split_cell(input_, self): parm = [] while t: e = t.pop(0) - if e[0].isalpha() or e[0] == '*': - parm = [e] + t + if e[0].isalpha() or e[0] == "*": + parm = [e, *t] break - else: - geom.append(e) + geom.append(e) # print '_split_cell geom', geom, parm # replace integer entries in geom block: - for s in re_int.findall(' '.join(geom)): + for _s in re_int.findall(" ".join(geom)): + s = _s # print 's from re_int', repr(s) # s is a surface or a cell (later only if prefixed by #) - t = 'cel' if s[0] == '#' else 'sur' + t = "cel" if s[0] == "#" else "sur" s = s if s[0].isdigit() else s[1:] f = fmt_d(s) inpt = inpt.replace(s, tp, 1) @@ -840,19 +861,16 @@ def _split_cell(input_, self): fmts.append(f) # geometry suffix - vals.append(('', '#gsu')) - fmts.append('{}') + vals.append(("", "#gsu")) + fmts.append("{}") # insert placeholder for geometry suffix if parm: - inpt = inpt.replace(parm[0], '_' + parm[0], 1) + inpt = inpt.replace(parm[0], "_" + parm[0], 1) # At this point all geom entries are replaced in inpt. The rest should # work only with the parm part of inpt. To ensure this, inpt is splitted # into inpt_geom and inpt_parm: - if parm: - i = inpt.index(parm[0]) - else: - i = len(inpt) + i = inpt.index(parm[0]) if parm else len(inpt) inpt_geom = inpt[:i] inpt_parm = inpt[i:] @@ -866,92 +884,94 @@ def _split_cell(input_, self): # replace values in parameters block. Values are prefixed with = or space(s) # Note that tmp and imp values must be hidden - t = ' '.join(parm).replace('=', ' ').split() # get rid of =. + t = " ".join(parm).replace("=", " ").split() # get rid of =. while t: s = t.pop(0) # print '_split_cell s: ', repr(s) - if s.lower() == 'u': + if s.lower() == "u": vs = t.pop(0) vv = int(vs) vf = fmt_d(vs) - vt = 'u' + vt = "u" inpt_parm = inpt_parm.replace(vs, tp, 1) vals.append((vv, vt)) fmts.append(vf) - elif 'fill' in s.lower(): + elif "fill" in s.lower(): # print '_split_cell: has fill!' # assume that only one integer follows the fill keyword, optionally # with transformation in parentheses. vs = t.pop(0) # if transformation in parentheses follows the universe number # immediately, split this manually: - if '(' in vs: - i = vs.index('(') + if "(" in vs: + i = vs.index("(") ttt = vs[i:] vs = vs[:i] # vs, ttt = vs.split('(') t.insert(0, ttt) vv = int(vs) vf = fmt_d(vs) - vt = 'fill' + vt = "fill" inpt_parm = inpt_parm.replace(vs, tp, 1) vals.append((vv, vt)) fmts.append(vf) # fill value can be followed by transformation in parentheses # Fill value can be optionally followed by transformation number of # transformation parameters in parentheses - if t and '(' in t[0]: + if t and "(" in t[0]: vsl = [] # lists of strings, values, formats and types vvl = [] vfl = [] vtl = [] # add opening parenthesis - vsl.append('(') - vvl.append('(') - vfl.append(fmt_s('(')) - vtl.append('#(') # #-types are internal, don't output in --mode info. - t[0] = t[0].replace('(', '', 1) + vsl.append("(") + vvl.append("(") + vfl.append(fmt_s("(")) + vtl.append("#(") # #-types are internal, don't output in --mode info. + t[0] = t[0].replace("(", "", 1) # add entries in parentheses and the closing parenthis - while vsl[-1] != ')': + while vsl[-1] != ")": vs = t.pop(0) - if ')' in vs: - vs = vs.replace(')', '', 1) + if ")" in vs: + vs = vs.replace(")", "", 1) if vs: vsl.append(vs) vvl.append(vs) vfl.append(fmt_s(vs)) - vtl.append('#tparam') - vsl.append(')') - vvl.append(')') - vfl.append(fmt_s(')')) - vtl.append('#)') + vtl.append("#tparam") + vsl.append(")") + vvl.append(")") + vfl.append(fmt_s(")")) + vtl.append("#)") elif vs: vsl.append(vs) vvl.append(vs) vfl.append(fmt_s(vs)) - vtl.append('#tparam') + vtl.append("#tparam") # check if only one parameter in parenthethes -- it is tr # number, not tr parameter if len(vsl) == 3: vvl[1] = int(vvl[1]) vfl[1] = fmt_d(vsl[1]) - vtl[1] = 'tr' + vtl[1] = "tr" # add all strings, values, formats and types: - for vs, vv, vf, vt in zip(vsl, vvl, vfl, vtl): - inpt_parm = inpt_parm.replace(vs, tp, 1) # TODO: here only parm part of inpt should be modified. + for vs, vv, vf, vt in zip(vsl, vvl, vfl, vtl, strict=False): + inpt_parm = inpt_parm.replace( + vs, tp, 1 + ) # TODO: here only parm part of inpt should be modified. vals.append((vv, vt)) fmts.append(vf) # warn if there is possibility for an array following the fill # keyword: # TODO fill value can be an array - if 'fill' == s.lower() and 'lat' in ''.join(parm).lower(): - print('WARNING: fill keyword followed by an array', end=' ') - print('cannot be parsed') + if s.lower() == "fill" and "lat" in "".join(parm).lower(): + print("WARNING: fill keyword followed by an array", end=" ") + print("cannot be parsed") inpt = inpt_geom + inpt_parm @@ -959,26 +979,26 @@ def _split_cell(input_, self): for f in fmts: inpt = inpt.replace(tp, f, 1) - return inpt.split('\n'), vals + return inpt.split("\n"), vals def _split_surface(input_): """ Similar to _split_cell(), but for surface cards. """ - inpt = '\n'.join(input_) + inpt = "\n".join(input_) t = inpt.split() vals = [] # like in split_cell() fmts = [] - tp = '_' + tp = "_" # get surface name: js = t.pop(0) if not js[0].isdigit(): js = js[1:] inpt = inpt.replace(js, tp, 1) - vals.append((int(js), 'sur')) + vals.append((int(js), "sur")) fmts.append(fmt_d(js)) # get TR or periodic surface: @@ -986,14 +1006,14 @@ def _split_surface(input_): if ns[0].isdigit(): # TR is given inpt = inpt.replace(ns, tp, 1) - vals.append((int(ns), 'tr')) + vals.append((int(ns), "tr")) fmts.append(fmt_d(ns)) st = t.pop(0) - elif ns[0] == '-': + elif ns[0] == "-": # periodic surface ns = ns[1:] inpt = inpt.replace(ns, tp, 1) - vals.append((int(ns), 'sur')) + vals.append((int(ns), "sur")) fmts.append(fmt_d(ns)) st = t.pop(0) elif ns[0].isalpha(): @@ -1008,91 +1028,129 @@ def _split_surface(input_): for f in fmts: inpt = inpt.replace(tp, f, 1) - return inpt.split('\n'), vals, st, scoef + return inpt.split("\n"), vals, st, scoef + + +def _get_int(s: str) -> str: + """Extract contigous digits from a word `s`. + Example + ------- + >>> _get_int("TR1") + '1' + >>> _get_int("100") + '100' + >>> _get_int("m2") + '2' + >>> _get_int("d1s") + '1' -def _get_int(s): - r = '' + Parameters + ---------- + s + string to parse + + Returns + ------- + str: digits value part + """ + r = "" for c in s: if r and c.isalpha(): break - elif c.isdigit(): + if c.isdigit(): r += c return r -def _parse_tr(input_): - """ +def _parse_tr(input_: list[str]) -> tuple[str, list[str], list[tuple[float, str]]]: + """Parse transformation input. + + Note + ---- input_ should be already passed through _split_data() + + Parameters + ---------- + input + text lines to parse + + Returns + ------- + - Rotation units - '*' if in degrees + - + + Example + ------- + >>> _parse_tr(["tr1 0 0 1"]) + ('', ['tr1 {} {} {}'], [(0.0, 'float'), (0.0, 'float'), (1.0, 'float')]) + >>> _parse_tr(["*tr1 0 0 1"]) + ('*', ['*tr1 {} {} {}'], [(0.0, 'float'), (0.0, 'float'), (1.0, 'float')]) + """ - inpt = '\n'.join(input_) + inpt = "\n".join(input_) inp1, inp2 = inpt.split(None, 1) - if inp1.lstrip()[0] == '*': - unit = '*' - else: - unit = '' + unit = "*" if inp1.lstrip()[0] == "*" else "" svals = inp2.split() for s in svals: - inp2 = inp2.replace(s, '{}', 1) + inp2 = inp2.replace(s, "{}", 1) - fvals = [(float(s), 'float') for s in svals] - return unit, (inp1 + ' ' + inp2).split('\n'), fvals + fvals = [(float(s), "float") for s in svals] + return unit, (inp1 + " " + inp2).split("\n"), fvals -def _split_data(input_): - inpt = '\n'.join(input_) +def _split_data(input_: list[str]) -> tuple[list[str], list[tuple[int, str], str | None]]: + inpt = "\n".join(input_) t = inpt.split() vals = [] fmts = [] - tp = '_' + tp = "_" - if 'tr' in t[0][:3].lower(): + if "tr" in t[0][:3].lower(): # TRn card - dtype = 'TRn' + dtype = "TRn" ns = _get_int(t[0]) inpt = inpt.replace(ns, tp, 1) - vals.append((int(ns), 'tr')) + vals.append((int(ns), "tr")) fmts.append(fmt_d(ns)) - elif (t[0][0].lower() == 'm' and - 'mode' not in t[0].lower() and - 'mesh' not in t[0].lower()): + elif t[0][0].lower() == "m" and "mode" not in t[0].lower() and "mesh" not in t[0].lower(): # This is the Mn, MTn or MPNn card ms = _get_int(t[0]) inpt = inpt.replace(ms, tp, 1) - vals.append((int(ms), 'mat')) + vals.append((int(ms), "mat")) fmts.append(fmt_d(ms)) # additional tests to define data card type: if t[0][1].isdigit(): - dtype = 'Mn' - elif t[0][1].lower() == 't': - dtype = 'MTn' - elif t[0][1].lower() == 'p': - dtype = 'MPNn' - elif t[0][0].lower() == 'f' and t[0][1].isdigit(): + dtype = "Mn" + elif t[0][1].lower() == "t": + dtype = "MTn" + elif t[0][1].lower() == "p": + dtype = "MPNn" + elif t[0][0].lower() == "f" and t[0][1].isdigit(): # FN card - dtype = 'Fn' + dtype = "Fn" ns = _get_int(t[0]) # tally number inpt = inpt.replace(ns, tp, 1) - vals.append((int(ns), 'tal')) + vals.append((int(ns), "tal")) fmts.append(fmt_d(ns)) # define type of integers by tally type: nv = int(ns[-1]) if nv in [1, 2]: - typ = 'sur' + typ = "sur" elif nv in [4, 6, 7, 8]: - typ = 'cel' + typ = "cel" else: - typ = '' + typ = "" if typ: # Lattice indices, surrounded by square brakets must allready be # hidden # Special treatment, if tally has 'u=' syntax. - hasu = 'u' in inpt.lower() and '=' in inpt.lower() + hasu = "u" in inpt.lower() and "=" in inpt.lower() # find all integers -- they are cells or surfaces for s in re_int.findall(inpt): ss = s[1:] @@ -1103,19 +1161,19 @@ def _split_data(input_): i1 = inpt.rfind(tp) i2 = inpt.find(ss) part = inpt[i1:i2] - while ' ' in part: - part = part.replace(' ', '') - if part[-2:].lower() == 'u=': - tpe = 'u' + while " " in part: + part = part.replace(" ", "") + if part[-2:].lower() == "u=": + tpe = "u" inpt = inpt.replace(ss, tp, 1) vals.append((int(ss), tpe)) fmts.append(fmt_d(ss)) - elif 'fmesh' == t[0][:5].lower() and t[0][5].isdigit(): + elif t[0][:5].lower() == "fmesh" and t[0][5].isdigit(): # fmesh card - dtype = 'fmesh' + dtype = "fmesh" ns = _get_int(t[0]) # tally number inpt = inpt.replace(ns, tp, 1) - vals.append((int(ns), 'tal')) + vals.append((int(ns), "tal")) fmts.append(fmt_d(ns)) else: dtype = None @@ -1123,7 +1181,7 @@ def _split_data(input_): for f in fmts: inpt = inpt.replace(tp, f, 1) - return inpt.split('\n'), vals, dtype + return inpt.split("\n"), vals, dtype def is_commented(l): @@ -1134,10 +1192,7 @@ def is_commented(l): # remove newline chars at the end of l: l = l.splitlines()[0] - if 'c ' in l[0:6].lstrip().lower(): - res = True - # print 'is_com "c "', - elif 'c' == l.lower(): + if "c " in l[0:6].lstrip().lower() or l.lower() == "c": res = True # print 'is_com "c"', # print 'is_com', res @@ -1148,79 +1203,44 @@ def is_fc_card(l): """ Return true, if line l is tally comment cards, fcN """ - return l.lstrip().lower()[:2] == 'fc' + return l.lstrip().lower()[:2] == "fc" def is_blankline(l): """ Return True, if l is the delimiter blank line. """ - return l.strip() == '' + return l.strip() == "" -if six.PY2: - def get_cards(inp, debug=None, preservetabs=False): - """ - Check first existence of a dump file - If dump exists and it is newwer than the input file, read the dump file - """ - from os import stat - iname = inp - dname = '.{}.~'.format(os.path.basename(inp)) - try: - it = stat(iname).st_mtime - except OSError as e: - raise e - - try: - dt = stat(dname).st_mtime - except OSError: - # print('No dump file exists') - dt = it - 1.0 - if it < dt and debug is None: - # print('Reading from dump') - # dump is youger - dfile = open(dname, 'r') - cl = cPickle.load(dfile) - for c in cl: - yield c - else: - # print('Reading from input') - cl = [] - for c in get_cards_from_input(inp, debug=debug, preservetabs=preservetabs): - yield c - cl.append(c) - if debug is None: - # otherwise the instances of c contain the file object, which - # cannot be dumped. - dfile = open(dname, 'w') - cPickle.dump(cl, dfile) -else: - def get_cards(inp, debug=None, preservetabs=False): - """ - Check first existence of a dump file +def get_cards(inp: str, debug: bool = None, preservetabs: bool = False) -> Iterable[Card]: + """ + Check first existence of a dump file - If dump exists and it is newwer than the input file, read the dump file - """ - iname = inp - for c in get_cards_from_input(inp, debug=debug, preservetabs=preservetabs): - yield c + If dump exists and it is newer than the input file, read the dump file + """ + # TODO @dvp2015: the docsring is not implemented + yield from get_cards_from_input(inp, debug=debug, preservetabs=preservetabs) -def index_(line, chars='$&'): +def index_(line: str, chars: str = "$&") -> int: """ Find the first index of one of the chars in line. """ - r = re.compile('[{}]'.format(chars)) + r = re.compile(f"[{chars}]") m = r.search(line) - if m: - i = m.end() - 1 - else: - i = len(line) - 1 - return i - - -def load_decode_buffer(filename): + return m.end() - 1 if m else len(line) - 1 + # TODO @dvp: the last char is lost here, assumed that this is newline + # but why? + # The following fix, breaks some tests, need deep debugging + # d = m.end() - 1 if m else len(line) - 1 + # last_char = line[d] + # if not (last_char in chars or last_char.isspace()): + # d += 1 + # return d + + +def load_decode_buffer(filename: str | Path) -> StringIO: """ Load and decode the text inside an file to a string buffer. @@ -1228,31 +1248,41 @@ def load_decode_buffer(filename): """ # detect encoding input deck detector = UniversalDetector() - with open(filename, 'rb') as finp: + with Path(filename).open("rb") as finp: for row in finp: detector.feed(row) if detector.done: break detector.close() - inpencoding = detector.result['encoding'] + inpencoding = detector.result["encoding"] # bufferize input deck in memory while decoding # replace unknown characters found with hexadecimal Unicode backslashed escape sequences - with open(filename, mode='rb') as finp: - textbuffer = StringIO(finp.read().decode(inpencoding, errors='backslashreplace')) - - return textbuffer - - -def get_cards_from_input(inp, debug=None, preservetabs=False): - """ - Iterable, return instances of the Card() class representing + with Path(filename).open(mode="rb") as finp: + return StringIO(finp.read().decode(inpencoding, errors="backslashreplace")) + + +def get_cards_from_input( + inp: str, debug: TextIO | None = None, preservetabs: bool = False +) -> Iterable[Card]: + """Load cards from a file. + + Parameters + ---------- + inp + the filename. + debug + optional output stream for debugging + preservetabs + if not, tabs from input text will be removed on reading + + Returns + ------- + Iterable, return instances of the Card class representing cards in the input file. - - inp -- is the filename. """ - def _yield(card, ct, ln): + def _yield(card: list[str], ct: Literal[1, 2, 3, 4, 5], ln: int): return Card(card, ct, ln, debug) def replace_tab(l, cln, preserve=False, ts=8): @@ -1262,13 +1292,12 @@ def replace_tab(l, cln, preserve=False, ts=8): """ if preserve: return l[:] - else: - while '\t' in l: - i = l.index('\t') - ii = (i // ts + 1) * ts - i - # print("c Line {}: tab replaced with {} spaces".format(cln + 1, ii)) - l = l[:i] + ' '*ii + l[i+1:] - return l[:] + while "\t" in l: + i = l.index("\t") + ii = (i // ts + 1) * ts - i + # print("c Line {}: tab replaced with {} spaces".format(cln + 1, ii)) + l = l[:i] + " " * ii + l[i + 1 :] + return l[:] # load input deck file inside a string buffer f = load_decode_buffer(inp) @@ -1285,19 +1314,19 @@ def replace_tab(l, cln, preserve=False, ts=8): cln += 1 # kw = l.lower().split()[0] kw = l.lstrip() - if 'message:' == kw[:8].lower(): + if kw[:8].lower() == "message:": # read message block right here res = [] while not is_blankline(l): res.append(l) l = replace_tab(next(f), cln, preserve=preservetabs) cln += 1 - yield _yield(res, CID.message, cln-1) # message card - yield _yield(l, CID.blankline, cln) # blank line + yield _yield(res, CID.message, cln - 1) # message card + yield _yield(l, CID.blankline, cln) # blank line l = replace_tab(next(f), cln, preserve=preservetabs) cln += 1 ncid = CID.title - elif 'continue' == kw[:8].lower(): + elif kw[:8].lower() == "continue": # input file for continue job. Contains only data block. ncid = CID.data else: @@ -1348,13 +1377,13 @@ def replace_tab(l, cln, preserve=False, ts=8): card = [] if ncid == 6: break - elif l[0:5] == ' ' or cf: + elif l[0:5] == " " or cf: # l is continuation line. if cmnt: card += cmnt # prev. comment lines belong to this card. cmnt = [] card.append(l) - cf = l[:index_(l)].find('&', 0, 81) > -1 + cf = l[: index_(l)].find("&", 0, 81) > -1 elif is_commented(l): # l is a line comment. Where it belongs (to the current card or # to the next one), depends on the next line, therefore, just @@ -1371,7 +1400,7 @@ def replace_tab(l, cln, preserve=False, ts=8): card = [l] # if tally comment card, i.e. started with fc, the & character # does not mean continuation. - cf = not is_fc_card(l) and l[:index_(l)].find('&', 0, 81) > -1 + cf = not is_fc_card(l) and l[: index_(l)].find("&", 0, 81) > -1 if card: yield _yield(card, ncid, cln - len(card) - len(cmnt)) if cmnt: @@ -1385,7 +1414,7 @@ def get_blocks(cards): d = {} cbt = None # current block type - cbc = [] # current block cards + cbc = [] # current block cards for c in cards: if c.ctype == CID.blankline: d[cbt] = cbc @@ -1402,31 +1431,58 @@ def get_blocks(cards): return d -def are_close_vals(x, y, re=1e-6, ra=0.): - """ - Return True if x and y are closer then re or ra. - """ - if abs(x - y) <= ra: - r = True - elif x != 0: - r = abs((x - y)/x) <= re - else: - # y is not equal to x and x is 0 -> y is not 0. - r = abs((x - y)/y) <= re - return r +def are_close_vals(x: Number, y: Number, re: float = 1e-6, ra: float = 0.0) -> bool: + """Check if `x` is close to `y`. + Parameters + ---------- + x + some value + y + the other one + re + relative error + ra + absolute error -def are_close_lists(x, y, re=1e-6, pci=[]): + Returns + ------- + True if x and y are closer then re or ra. """ - Return True if x and y are close but not equal. + return True if abs(x - y) <= ra else abs((x - y) / x) <= re if x != 0 else False + + +def are_close_lists(x: list[Number], y: list[Number], re=1e-6, pci=None) -> bool: + """Check if the two lists of numbers are close with given relative error. + + Use, for instance, to compare coefficients of surfaces for equivalence. + + Parameters + ---------- + x + the first list + y + the second + re + allowed relative errror + pci + (proportional check index) - list of index ranges that + define elements of x and y to be checked for proportionality only + + + Returns + ------- + True if x and y are close but not equal. """ if len(x) != len(y): - res = False - msg = 'Different length' + return False if x == y: return True + if pci is None: + pci = [] + # pci -- list of indices that define elements of x and y to be checked for # proportionality only. if len(pci) == 0: @@ -1439,13 +1495,13 @@ def are_close_lists(x, y, re=1e-6, pci=[]): else: if len(pci) % 2 == 1: # augment with len(x) +1 - pci = tuple(pci) + (len(x) + 1, ) + pci = (*tuple(pci), len(x) + 1) xe = [] ye = [] xp = [] yp = [] i = 0 - for i1, i2 in zip(pci[0::2], pci[1::2]): + for i1, i2 in zip(pci[0::2], pci[1::2], strict=True): xe += x[i:i1] ye += y[i:i1] xp += x[i1:i2] @@ -1453,39 +1509,27 @@ def are_close_lists(x, y, re=1e-6, pci=[]): i = i2 # normalize yp - xpn = sum([e**2 for e in xp]) - ypn = sum([e**2 for e in yp]) - if xpn > 0 and ypn > 0: - yp = [e*xpn/ypn for e in yp] - - msg = [] - res = [] - for xl, yl in zip([xe, xp], [ye, yp]): + + # @dvp2015 changed (that was an error in norm presentation): + # xpn = sum([e**2 for e in xp]) + # ypn = sum([e**2 for e in yp]) + # xpn = max(abs(e) for e in xp) + # ypn = max(abs(e) for e in yp) + # if xpn > 0 and ypn > 0: + # yp = [e * xpn / ypn for e in yp] + # to: + if xp and yp: + xpn = max(abs(e) for e in xp) # Let's use LP1 norm for this + ypn = max(abs(e) for e in yp) + if xpn > 0 and ypn > 0: + r = xpn / ypn + yp = [e * r for e in yp] + + for xl, yl in zip([xe, xp], [ye, yp], strict=False): # compare xl and yl without normalization if xl == yl: - res.append(True) - msg.append('exact match') - else: - n = 0 - for xx, yy in zip(xl, yl): - r = are_close_vals(xx, yy, re) - if not r: - m = 'diff at {}'.format(n) - break - else: - m = 'all elements are close or equal' - r = True - res.append(r) - msg.append(m) - - if not res[-1]: - result = False - break - - else: - result = True - return result - - -if __name__ == '__main__': - pass + continue + for xx, yy in zip(xl, yl, strict=False): + if not are_close_vals(xx, yy, re): + return False + return True diff --git a/numjuggler/ri_notation.py b/src/numjuggler/ri_notation.py similarity index 96% rename from numjuggler/ri_notation.py rename to src/numjuggler/ri_notation.py index 586734c..2304a5c 100644 --- a/numjuggler/ri_notation.py +++ b/src/numjuggler/ri_notation.py @@ -5,8 +5,7 @@ are not possible or seldom. """ - -from __future__ import print_function +from __future__ import annotations def shorten(list_, rmin=2, imin=2): @@ -59,7 +58,7 @@ def expand(list_): they are expanded. """ es = None - # this value not actually used. Set only to avoid checker warning about undefined n + # this value not actually used. Set only to avoid checker warning about undefined n n = -1 for e in list_: if es is not None: @@ -128,4 +127,4 @@ def test_(tl, rmin, imin, name): for imin in [1, 2, 3, 4]: for rmin in [1, 2, 3, 4]: test_(tr + ti, rmin, imin, - 'CustomList imin={}, rmin={}'.format(imin, rmin)) + f'CustomList imin={imin}, rmin={rmin}') diff --git a/numjuggler/shortener.py b/src/numjuggler/shortener.py similarity index 69% rename from numjuggler/shortener.py rename to src/numjuggler/shortener.py index 94669cc..b6c493d 100644 --- a/numjuggler/shortener.py +++ b/src/numjuggler/shortener.py @@ -1,3 +1,5 @@ +from __future__ import annotations + def f(l): @@ -14,7 +16,7 @@ def f(l): else: # r-series stops here. yield es - yield '{}r'.format(iR) + yield f'{iR}r' iR = 0 es = e elif iD != 0: @@ -26,20 +28,19 @@ def f(l): else: # i-series stops here. yield es - yield '{}i'.format(iI) + yield f'{iI}i' yield ep iI = 0 iD = 0 es = e + # there is no active series, and es -- previous element. + elif e == es: + # r-series starts here + iR = 1 else: - # there is no active series, and es -- previous element. - if e == es: - # r-series starts here - iR = 1 - else: - # i-series starts here - iI = 0 - iD = e - es + # i-series starts here + iI = 0 + iD = e - es if __name__ == '__main__': pass diff --git a/numjuggler/splitter.py b/src/numjuggler/splitter.py similarity index 64% rename from numjuggler/splitter.py rename to src/numjuggler/splitter.py index 8c6eca1..5fbf4d9 100644 --- a/numjuggler/splitter.py +++ b/src/numjuggler/splitter.py @@ -10,69 +10,71 @@ The keyword part can contain floats, alpha-numeric keywords, equal signs and parentheses. -Everywhere the repitition syntax is possible, that is Ni, Nr, etc. +Everywhere the repitition syntax is possible, that is Ni, Nr, etc. """ # List of cell parameter keywords -LoCL = ['imp', - 'vol', - 'pwt', - 'ext', - 'fcl', - 'wwn', - 'dxc', - 'nonu', - 'pd', - 'tmp', - 'u', - '*fill', - 'fill', - 'lat'] +from __future__ import annotations + +LoCL = [ + "imp", + "vol", + "pwt", + "ext", + "fcl", + "wwn", + "dxc", + "nonu", + "pd", + "tmp", + "u", + "*fill", + "fill", + "lat", +] # List of shorthand features -LoSH = ['r', - 'ilog', - 'i', - 'm', - 'j'] +LoSH = ["r", "ilog", "i", "m", "j"] + def split_by_state(inpt): - tl = inpt.split() # list of tokens - vl = [] # list of (v, t) -- value and its type + tl = inpt.split() # list of tokens + vl = [] # list of (v, t) -- value and its type # Read cell name and material specs c = tl.pop(0) m = tl.pop(0) - vl += [(c, 'cell'), (m, 'mat')] + vl += [(c, "cell"), (m, "mat")] if int(m) > 0: d = tl.pop(0) - vl += [(d, 'rho')] + vl += [(d, "rho")] - state = 'geom' + state = "geom" while tl: n = tl[0] - if state == 'geom': + if state == "geom": # check if param starts: for kw in LoCL: if kw in n: - state = 'param' + state = "param" break - - elif state == 'param': + elif state == "param": + msg = "Missed branch" + raise NotImplementedError(msg) def _cut(s, subs): """ Cut subs from the begining of s. """ - return s[s.find(subs) + len(subs): ] + return s[s.find(subs) + len(subs) :] def split_head(inpt): """ Return [Cellname, material, density] and the rest of the inpt. - The argument ``inpt`` is a string representing the meaningful parts of a cell card that does not + The argument ``inpt`` is a string representing the meaningful parts of a cell card that does not use the ``LIKE n BUT`` syntax. """ @@ -83,7 +85,7 @@ def split_head(inpt): res = (c, m) if int(m) > 0: d = t.pop(0) - res += (d, ) + res += (d,) # remove c, m and d from inpt: for e in res: @@ -102,24 +104,23 @@ def split_geometry(inpt): # geometry description goes until the first keyword, that must be alpha-numeric. # Find where geometry ends, taking into account repetition characters i and r: for i in range(1, len(inpt)): - if inpt[i].isalpha() and not inpt[i-1].isdigit(): + if inpt[i].isalpha() and not inpt[i - 1].isdigit(): break geom = inpt[:i] rest = inpt[i:] # add spaces around parentheses, colons, and #-s. - for c in '()#:': - geom = geom.replace(c, ' ' + c + ' ') - + for c in "()#:": + geom = geom.replace(c, " " + c + " ") -if __name__ == '__main__': - s = {} - s[0] = '1 0 1 -5' - s[1] = '2 5 -10.3 (3:4)' - s[2] = '2 5 -10.3 \n (3:4)' +# if __name__ == '__main__': +# s = {} +# s[0] = '1 0 1 -5' +# s[1] = '2 5 -10.3 (3:4)' +# s[2] = '2 5 -10.3 \n (3:4)' - for l in s.values(): - print repr(l) - print split_head(l) - print '--'*30 +# for l in s.values(): +# print repr(l) +# print split_head(l) +# print '--'*30 diff --git a/numjuggler/string_cells.py b/src/numjuggler/string_cells.py similarity index 91% rename from numjuggler/string_cells.py rename to src/numjuggler/string_cells.py index 26bf0ab..2d0ca48 100644 --- a/numjuggler/string_cells.py +++ b/src/numjuggler/string_cells.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import re -from numjuggler import numbering as mn -from numjuggler import parser as mp +from numjuggler import parser as mp ######################################### # define patterns to be found in string # @@ -9,11 +10,11 @@ # used in card_split -celmat=re.compile(r"(?P^ *\d+) +(?P(\d+|like))",re.I) # identify first two entries on cell card (cell name, material) -grlnumber=re.compile(r"[-+]?(\d+\.\d+|\.\d+|\d+\.?)(e[+-]\d+)?",re.I) # identify a general number form signed integer, float or exponential -param=re.compile(r"((^|\n {5})[\(\):\-\+\d+\.\# ]*)([\*a-z])",re.I) # identity begining of the paramter part of the cell card -likebut=re.compile(r"but",re.I) # identify likebut card -trans=re.compile(r"trcl|fill= *\d+[ c\$\n]*\(,",re.I) # identify tranformed card +celmat=re.compile(r"(?P^ *\d+) +(?P(\d+|like))",re.IGNORECASE) # identify first two entries on cell card (cell name, material) +grlnumber=re.compile(r"[-+]?(\d+\.\d+|\.\d+|\d+\.?)(e[+-]\d+)?",re.IGNORECASE) # identify a general number form signed integer, float or exponential +param=re.compile(r"((^|\n {5})[\(\):\-\+\d+\.\# ]*)([\*a-z])",re.IGNORECASE) # identity begining of the paramter part of the cell card +likebut=re.compile(r"but",re.IGNORECASE) # identify likebut card +trans=re.compile(r"trcl|fill= *\d+[ c\$\n]*\(,",re.IGNORECASE) # identify tranformed card # user in get_stat function reword=re.compile(r"(\d+|\(|\)|:|\#)") # word identification in cell line @@ -22,8 +23,8 @@ # used in Complementary operator function number=re.compile(r"(?P[-+]?\d+)") # signed (+-) (or not) numbers # leftp=re.compile(r"^ *(?P[-\d\(\#])",re.M) # identify first valid character -leftp=re.compile(r"(?P[-+\d\(\#])",re.I) # identify first valid character -rightp=re.compile(r"(?P[ c\$\n]*$)",re.I) # identify last valid character +leftp=re.compile(r"(?P[-+\d\(\#])",re.IGNORECASE) # identify first valid character +rightp=re.compile(r"(?P[ c\$\n]*$)",re.IGNORECASE) # identify last valid character #interblk=re.compile(r"(?P\d)(?P(( +| *(\$)?\n(C\n)* *)[-+]?\d))") # two numbers separated by blank (or newline or comments) #intercls=re.compile(r"(?P\))(?P(( *| *(\$)?\n(C\n)* *)[-+]?\d))") # closed parenthesis followed by number #interopn=re.compile(r"(?P\d)(?P(( *| *(\$)?\n(C\n)* *)\())") # number followed by opened parenthesis @@ -37,18 +38,18 @@ # used for remove redundant parenthesis function mostinner=re.compile(r"\([^\(^\)]*\)") # identify most inner parentheses bracketsemi=re.compile(r"[\]\[;]") # square bracket or semicolon -blnkline=re.compile(r"^ *\n",re.M) # identify blank line -contline=re.compile(r"\n {0,4}(?P[^c^ ])",re.I) # identify character other than 'C' in fisrt 5 columns +blnkline=re.compile(r"^ *\n",re.MULTILINE) # identify blank line +contline=re.compile(r"\n {0,4}(?P[^c^ ])",re.IGNORECASE) # identify character other than 'C' in fisrt 5 columns comdollar=re.compile(r"\n(?P *)\$") # identify dollar on 'blank line' startgeom=re.compile(r"(?P^ *)(?P[\-\+\d])") # identify beginning of the geomtric part -endgeom=re.compile(r"(?P\d)(?P *((\n *)?\$|\nc)?(\n *)?$)",re.I) # identify end of the geomtric part +endgeom=re.compile(r"(?P\d)(?P *((\n *)?\$|\nc)?(\n *)?$)",re.IGNORECASE) # identify end of the geomtric part #endgeom=re.compile(r"(?P\d)(?P *(\$|\nc)?(\n *)?$)",re.I) # identify end of the geomtric part # other rehash=re.compile(r"# *(\d+|\()") # find beginning of complementary operator (both cell and surf) parent=re.compile(r"[\(|\)]") # position of open and close parenthesis (get_hashcell) -gline=re.compile(r"(^ ?[\(\):\-\+\d+\.\# ]+|\n {5}[\(\):\-\+\d+\.\# ]+)",re.I) # valid geometric part of the line (remove/restore_comments) -comments=re.compile(r"((\n *)?\$|\n *c)",re.I) # begining of comment part (remove/restore_comments) +gline=re.compile(r"(^ ?[\(\):\-\+\d+\.\# ]+|\n {5}[\(\):\-\+\d+\.\# ]+)",re.IGNORECASE) # valid geometric part of the line (remove/restore_comments) +comments=re.compile(r"((\n *)?\$|\n *c)",re.IGNORECASE) # begining of comment part (remove/restore_comments) #comments=re.compile(r"\$|\n *c",re.I) # begining of comment part (remove/restore_comments) @@ -95,8 +96,7 @@ def redundant(m,geom): if leftOK and rightOK : return True - else: - return False + return False # function used in Regular expresion sub function # function user in complementary function @@ -107,8 +107,7 @@ def chgsign(m): return num[1:] if num[0] == '+': return '-'+num[1:] - else: - return '-'+num + return '-'+num # function used in Regular expersion sub function # function user in complementary function @@ -117,8 +116,7 @@ def chgsign(m): def repl_inter_union(m): if m.group(0) == ':' : return ')(' - else : - return ':' + return ':' # function used in Regular expersion sub function # function user in remove_redundant function @@ -127,10 +125,9 @@ def reverse_repl(m): symb=m.group(0) if symb == '[' : return '(' - elif symb == ']' : + if symb == ']' : return ')' - else : - return ':' + return ':' ############################################################ def complementary(ccell) : @@ -169,7 +166,7 @@ def complementary(ccell) : return ccell.str ############################################################ -class cline(): +class cline: def __init__(self,line): self.str=line @@ -192,7 +189,6 @@ def remove_comments(self): celltab[i] = c.group() self.str=''.join(celltab) - return def restore_comments(self): """ Restore the text of the comment.""" @@ -212,7 +208,6 @@ def restore_comments(self): j += 1 self.str = ''.join(celltab) - return def remove_redundant(self,remove_com=True,remopt='nochg'): @@ -292,7 +287,7 @@ def countP(self): rp=self.str.count(')') return (lp,rp) ############################################################ -class cell_card_string(): +class cell_card_string: def __init__(self,card): self.stat={ 'word' : None ,\ @@ -301,7 +296,6 @@ def __init__(self,card): 'hash' : None } self.__card_split__(card) - return def __card_split__(self,cardin): """ Split the card string in three parts : @@ -359,7 +353,6 @@ def __card_split__(self,cardin): self.geom = cline(cellcard.str) self.parm = cline('') - return def get_stat(self,remove_com=True): """ Count and return the number of words and hashes on the line.""" @@ -475,7 +468,7 @@ def remove(card,cname): logtab.sort flog = open(logfile,'w') for cell in logtab: - flog.write(' Cell {:>9} :\n'.format(cell[0])) + flog.write(f' Cell {cell[0]:>9} :\n') cc = False for h in cell[1]: if (h[0] == 'surf'): @@ -485,7 +478,7 @@ def remove(card,cname): if cc: for i,h in enumerate(cell[1]): if (h[0] == 'surf'): - flog.write(' {:>2}: {}\n'.format(i+1,h[1]) ) + flog.write(f' {i+1:>2}: {h[1]}\n' ) cc = False for h in cell[1]: if (h[0] == 'cell'): @@ -495,7 +488,7 @@ def remove(card,cname): if cc: for i,h in enumerate(cell[1]): if (h[0] == 'cell'): - flog.write(' {:>2}: {:>9}\n'.format(i+1,h[1]) ) + flog.write(f' {i+1:>2}: {h[1]:>9}\n' ) flog.write('\n---------------------------------------------------\n') flog.close() diff --git a/src/numjuggler/utils/__init__.py b/src/numjuggler/utils/__init__.py new file mode 100644 index 0000000..d300327 --- /dev/null +++ b/src/numjuggler/utils/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from ._io import resolve_fname_or_stream +from .partial_formatter import PartialFormatter + +__all__ = ["PartialFormatter", "resolve_fname_or_stream"] diff --git a/src/numjuggler/utils/_io.py b/src/numjuggler/utils/_io.py new file mode 100644 index 0000000..3420ebd --- /dev/null +++ b/src/numjuggler/utils/_io.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import Generator, TextIO, TYPE_CHECKING + + +import sys + +from contextlib import contextmanager +from pathlib import Path + +if TYPE_CHECKING: + from os import PathLike + + +@contextmanager +def resolve_fname_or_stream( + fname_or_stream: PathLike | str | TextIO | None, mode: str = "r" +) -> Generator[TextIO, None, None]: + """Open stream by name or pass as if it's already opened. + + Parameters + ---------- + fname_or_stream + file name or handle or Path to open, if None use std stream + mode + opening mode (as in Path) + + Returns + ------- + Context with opened stream + """ + + is_input = mode == "r" + if fname_or_stream is None: + if is_input: + yield sys.stdin + else: + yield sys.stdout + elif (is_input and hasattr(fname_or_stream, "read")) or ( + not is_input and hasattr(fname_or_stream, "write") + ): + yield fname_or_stream + else: + with Path(fname_or_stream).open(mode=mode) as fid: + yield fid diff --git a/src/numjuggler/utils/partial_formatter.py b/src/numjuggler/utils/partial_formatter.py new file mode 100644 index 0000000..3363be9 --- /dev/null +++ b/src/numjuggler/utils/partial_formatter.py @@ -0,0 +1,32 @@ +"""PartialFormatter module""" + +from __future__ import annotations + +import string + + +def _make_label(item: str) -> str: + return "{" + item + "}" if item else "" + + +class _SafeDict(dict): + def __getitem__(self, item: str) -> str: + return super().__getitem__(item) or "" + + def __missing__(self, key): + return _make_label(key) + + +class PartialFormatter(string.Formatter): + """Formatter for incomplete fillers specification. + + For cases when not all of the fillers in a template + are provided. Standard string.formatter throws KeyError + in this case. + + Useful for formatting in several steps. + """ + + def format(self, format_string, /, *args, **kwargs): + """Safely formats even if not fillers are provided in the kwargs.""" + return self.vformat(format_string, args, _SafeDict(kwargs)) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..12a21a6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +import pytest + +from pathlib import Path + + +@pytest.fixture +def cd_tmpdir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Temporarily switch to temp directory.""" + monkeypatch.chdir(tmp_path) + return tmp_path diff --git a/tests/data/continue b/tests/data/continue new file mode 100644 index 0000000..193c52d --- /dev/null +++ b/tests/data/continue @@ -0,0 +1,3 @@ +continue +ctme 15 +print 160 \ No newline at end of file diff --git a/tests/data/simple_cubes_with_multiline_tr.mcnp b/tests/data/simple_cubes_with_multiline_tr.mcnp new file mode 100644 index 0000000..5057b10 --- /dev/null +++ b/tests/data/simple_cubes_with_multiline_tr.mcnp @@ -0,0 +1,51 @@ +testing integration scripts on simple cubic cells +c +c given two envelopes and fillers insert new envelop and filler intersecting +c the original fillers +c +1 0 -1 : 2 : -3 : 4 : -5 : 6 imp:n=0 + $ outer space +2 0 1 -2 3 -7 5 -6 imp:n=1 fill=1 + $ envelop #1 +3 0 1 -2 7 -4 5 -6 imp:n=1 fill=2 + $ envelop #2 +4 1 1.0 20 -21 22 -23 24 -25 imp:n=1 u=1 + $ filler #1 body +5 0 -20 : 21 : -22 : 23 : -24 : 25 imp:n=1 u=1 + $ filler #1 outer space +6 2 1.0 30 -31 32 -33 34 -35 imp:n=1 u=2 + $ filler #2 body +7 0 -30 : 31 : -32 : 33 :-34 : 35 imp:n=1 u=2 + $ filler #2 outer space + +c envelopes +1 px -50 +2 px 50 +3 py -50 +4 py 50 +5 pz -50 +6 pz 50 +7 py 0 +c filler #1 +20 px -25 +21 px 25 +22 py -25 +23 py 0 +24 pz -25 +25 pz 25 +c filler #2 +30 px -35 +31 px 35 +32 py 0 +33 py 35 +34 pz -35 +35 pz 35 + +m1 1001.31c 1.0 +m2 2004.31c 1.0 +c +c Comment may contain entries with braces {31c} +c +tr1 + 100 0 0 + \ No newline at end of file diff --git a/travis_tests/cdens/inp.i b/tests/data/travis_tests/cdens/inp.i similarity index 100% rename from travis_tests/cdens/inp.i rename to tests/data/travis_tests/cdens/inp.i diff --git a/travis_tests/cdens/inp.map1.ref b/tests/data/travis_tests/cdens/inp.map1.ref similarity index 100% rename from travis_tests/cdens/inp.map1.ref rename to tests/data/travis_tests/cdens/inp.map1.ref diff --git a/travis_tests/cdens/inp.map2.ref b/tests/data/travis_tests/cdens/inp.map2.ref similarity index 100% rename from travis_tests/cdens/inp.map2.ref rename to tests/data/travis_tests/cdens/inp.map2.ref diff --git a/travis_tests/cdens/inp.map3.ref b/tests/data/travis_tests/cdens/inp.map3.ref similarity index 100% rename from travis_tests/cdens/inp.map3.ref rename to tests/data/travis_tests/cdens/inp.map3.ref diff --git a/travis_tests/cdens/inp.map4.ref b/tests/data/travis_tests/cdens/inp.map4.ref similarity index 100% rename from travis_tests/cdens/inp.map4.ref rename to tests/data/travis_tests/cdens/inp.map4.ref diff --git a/travis_tests/cdens/inp.map5.ref b/tests/data/travis_tests/cdens/inp.map5.ref similarity index 100% rename from travis_tests/cdens/inp.map5.ref rename to tests/data/travis_tests/cdens/inp.map5.ref diff --git a/travis_tests/cdens/inp.map6.ref b/tests/data/travis_tests/cdens/inp.map6.ref similarity index 100% rename from travis_tests/cdens/inp.map6.ref rename to tests/data/travis_tests/cdens/inp.map6.ref diff --git a/travis_tests/cdens/map1 b/tests/data/travis_tests/cdens/map1 similarity index 100% rename from travis_tests/cdens/map1 rename to tests/data/travis_tests/cdens/map1 diff --git a/travis_tests/cdens/map2 b/tests/data/travis_tests/cdens/map2 similarity index 100% rename from travis_tests/cdens/map2 rename to tests/data/travis_tests/cdens/map2 diff --git a/travis_tests/cdens/map3 b/tests/data/travis_tests/cdens/map3 similarity index 100% rename from travis_tests/cdens/map3 rename to tests/data/travis_tests/cdens/map3 diff --git a/travis_tests/cdens/map4 b/tests/data/travis_tests/cdens/map4 similarity index 100% rename from travis_tests/cdens/map4 rename to tests/data/travis_tests/cdens/map4 diff --git a/travis_tests/cdens/map5 b/tests/data/travis_tests/cdens/map5 similarity index 100% rename from travis_tests/cdens/map5 rename to tests/data/travis_tests/cdens/map5 diff --git a/travis_tests/cdens/map6 b/tests/data/travis_tests/cdens/map6 similarity index 100% rename from travis_tests/cdens/map6 rename to tests/data/travis_tests/cdens/map6 diff --git a/travis_tests/merge/inp1.inp b/tests/data/travis_tests/merge/inp1.inp similarity index 100% rename from travis_tests/merge/inp1.inp rename to tests/data/travis_tests/merge/inp1.inp diff --git a/travis_tests/merge/inp2.inp b/tests/data/travis_tests/merge/inp2.inp similarity index 99% rename from travis_tests/merge/inp2.inp rename to tests/data/travis_tests/merge/inp2.inp index 3a934ec..2197933 100644 --- a/travis_tests/merge/inp2.inp +++ b/tests/data/travis_tests/merge/inp2.inp @@ -9,4 +9,3 @@ c surfaces c data m10 1001 1. - diff --git a/travis_tests/merge/merged.inp.ref b/tests/data/travis_tests/merge/merged.inp.ref similarity index 100% rename from travis_tests/merge/merged.inp.ref rename to tests/data/travis_tests/merge/merged.inp.ref diff --git a/travis_tests/remh/nested_complement.i b/tests/data/travis_tests/remh/nested_complement.i similarity index 100% rename from travis_tests/remh/nested_complement.i rename to tests/data/travis_tests/remh/nested_complement.i diff --git a/travis_tests/remh/nested_complement.ref b/tests/data/travis_tests/remh/nested_complement.ref similarity index 100% rename from travis_tests/remh/nested_complement.ref rename to tests/data/travis_tests/remh/nested_complement.ref diff --git a/travis_tests/renum/i1.i b/tests/data/travis_tests/renum/i1.i similarity index 100% rename from travis_tests/renum/i1.i rename to tests/data/travis_tests/renum/i1.i diff --git a/travis_tests/renum/i1.ref b/tests/data/travis_tests/renum/i1.ref similarity index 100% rename from travis_tests/renum/i1.ref rename to tests/data/travis_tests/renum/i1.ref diff --git a/travis_tests/renum/i2.i b/tests/data/travis_tests/renum/i2.i similarity index 100% rename from travis_tests/renum/i2.i rename to tests/data/travis_tests/renum/i2.i diff --git a/travis_tests/renum/i2.ref b/tests/data/travis_tests/renum/i2.ref similarity index 100% rename from travis_tests/renum/i2.ref rename to tests/data/travis_tests/renum/i2.ref diff --git a/travis_tests/renum/i3.i b/tests/data/travis_tests/renum/i3.i similarity index 100% rename from travis_tests/renum/i3.i rename to tests/data/travis_tests/renum/i3.i diff --git a/travis_tests/renum/i3.ref b/tests/data/travis_tests/renum/i3.ref similarity index 100% rename from travis_tests/renum/i3.ref rename to tests/data/travis_tests/renum/i3.ref diff --git a/tests/data/with_message.mcnp b/tests/data/with_message.mcnp new file mode 100644 index 0000000..627a352 --- /dev/null +++ b/tests/data/with_message.mcnp @@ -0,0 +1,51 @@ +message: ixr + +testing integration scripts on simple cubic cells +c +c given two envelopes and fillers insert new envelop and filler intersecting +c the original fillers +c +1 0 -1 : 2 : -3 : 4 : -5 : 6 imp:n=0 + $ outer space +2 0 1 -2 3 -7 5 -6 imp:n=1 fill=1 + $ envelop #1 +3 0 1 -2 7 -4 5 -6 imp:n=1 fill=2 + $ envelop #2 +4 1 1.0 20 -21 22 -23 24 -25 imp:n=1 u=1 + $ filler #1 body +5 0 -20 : 21 : -22 : 23 : -24 : 25 imp:n=1 u=1 + $ filler #1 outer space +6 2 1.0 30 -31 32 -33 34 -35 imp:n=1 u=2 + $ filler #2 body +7 0 -30 : 31 : -32 : 33 :-34 : 35 imp:n=1 u=2 + $ filler #2 outer space + +c envelopes +1 px -50 +2 px 50 +3 py -50 +4 py 50 +5 pz -50 +6 pz 50 +7 py 0 +c filler #1 +20 px -25 +21 px 25 +22 py -25 +23 py 0 +24 pz -25 +25 pz 25 +c filler #2 +30 px -35 +31 px 35 +32 py 0 +33 py 35 +34 pz -35 +35 pz 35 + +m1 1001.31c 1.0 +m2 2004.31c 1.0 +c +c Comment may contain entries with braces {31c} +c +mode n diff --git a/tests/test_likefunc.py b/tests/test_likefunc.py index a5a0a66..0767e5e 100644 --- a/tests/test_likefunc.py +++ b/tests/test_likefunc.py @@ -1,43 +1,44 @@ +from io import StringIO + import pytest -from numjuggler.utils.io import cd_temporarily -from six import StringIO -import numjuggler.likefunc as lf +import numjuggler.likefunc as lf -@pytest.mark.parametrize("data, log, present, absent, expected_present_value, expected_absent_value, expected_text", [ - ( - """ +@pytest.mark.parametrize( + "data, log, present, absent, expected_present_value, expected_absent_value, expected_text", + [ + ( + """ c 1: 12 c 2: 14 """, - False, - 1, 3, - 12, 3, - "", - ), - ( - """ + False, + 1, + 3, + 12, + 3, + "", + ), + ( + """ c 1: 12 c 2: 14 """, - True, - 1, 3, - 12, 3, - "cel 12: 1\ncel 3: 3\n", - ), -]) + True, + 1, + 3, + 12, + 3, + "cel 12: 1\ncel 3: 3\n", + ), + ], +) def test_LikeFunction( - data, - log, - present, - absent, - expected_present_value, - expected_absent_value, - expected_text + data, log, present, absent, expected_present_value, expected_absent_value, expected_text ): - input = StringIO(data) - maps = lf.read_map_file(input, log) + inp = StringIO(data) + maps = lf.read_map_file(inp, log) actual = StringIO() for k in maps: like_function = maps[k] diff --git a/tests/test_main.py b/tests/test_main.py index 666b528..10967e0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,21 +1,21 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, division, nested_scopes +from __future__ import annotations from pathlib import Path +import shutil import pytest import six -from numjuggler.utils.io import cd_temporarily from numjuggler.main import main -test_data_path = Path('./data') +HERE = Path(__file__).parent +test_data_path = HERE / "data" assert test_data_path.exists(), "Cannot access test data files" def load_line_heading_numbers(lines): res = [] for line in lines: - line = six.ensure_str(line, encoding='utf8') + line = six.ensure_str(line, encoding="utf8") if line and str.isdigit(line[0]): if six.PY2: card_no = int(line.split()[0]) @@ -25,25 +25,34 @@ def load_line_heading_numbers(lines): return res -@pytest.mark.parametrize("inp,command,expected", [ - ( - 'simple_cubes.mcnp', - "-c 10", - "11 12 13 14 15 16 17 1 2 3 4 5 6 7 20 21 22 23 24 25 30 31 32 33 34 35" - ), - ( - 'simple_cubes.mcnp', - "-c 20 -s 10", - "21 22 23 24 25 26 27 11 12 13 14 15 16 17 30 31 32 33 34 35 40 41 42 43 44 45" - ), -]) -def test_test_main(tmpdir, capsys, inp, command, expected): +@pytest.mark.parametrize( + "inp,command,expected", + [ + ( + "simple_cubes.mcnp", + "-c 10", + "11 12 13 14 15 16 17 1 2 3 4 5 6 7 20 21 22 23 24 25 30 31 32 33 34 35", + ), + ( + "simple_cubes.mcnp", + "-c 20 -s 10", + "21 22 23 24 25 26 27 11 12 13 14 15 16 17 30 31 32 33 34 35 40 41 42 43 44 45", + ), + pytest.param( + "simple_cubes_with_multiline_tr.mcnp", + "-c 20 -s 10", + "21 22 23 24 25 26 27 11 12 13 14 15 16 17 30 31 32 33 34 35 40 41 42 43 44 45", + marks=pytest.mark.xfail(reason="Failed on multiline TR specification"), + ), + ], +) +def test_rename(cd_tmpdir, capsys, inp, command, expected): source = test_data_path / inp + wrk_file = shutil.copy(source, cd_tmpdir) command = command.split() - command.append(str(source.absolute())) - with cd_temporarily(tmpdir): - main(command) - out, err = capsys.readouterr() - actual_numbers = load_line_heading_numbers(out.split('\n')) + command.append(str(wrk_file)) + main(command) + out, _ = capsys.readouterr() + actual_numbers = load_line_heading_numbers(out.split("\n")) expected_numbers = list(f for f in map(int, expected.split())) assert expected_numbers == actual_numbers, "Output of numjuggler is wrong" diff --git a/tests/test_numbering.py b/tests/test_numbering.py new file mode 100644 index 0000000..a047759 --- /dev/null +++ b/tests/test_numbering.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import pytest + +from numjuggler.numbering import _Range, LikeFunction, read_map_file + + +def test_range_class(): + r = _Range(1) + assert 1 in r + assert 2 not in r + r = _Range(1, 3) + assert 1 in r + assert 2 in r + assert 3 in r + assert 4 not in r + r = _Range(3, 1) + assert 1 in r + assert 2 in r + assert 3 in r + assert 0 not in r + r = _Range("a", "z") + assert "a" in r + assert "z" in r + assert "ab" in r + assert "_" not in r + + +# @pytest.mark.xfail(reason="obsolete collections.Callable is used") +@pytest.mark.parametrize("pdict, n, expected", [({"c": [5, [(10, 20, 10)]]}, 1, 1+5)]) +def test_like_function(pdict, n, expected): + lf = LikeFunction(pdict) + assert lf(n, "c") == expected diff --git a/tests/test_parser.py b/tests/test_parser.py index e5bf434..51b128c 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,18 +1,354 @@ -from numjuggler.parser import Card +from io import StringIO +from pathlib import Path +from textwrap import dedent +from typing import Iterable +import pytest -class TestCardParser: - def test_vol_param(self): - definition = [ - "600177 400 -1.00000e+00 $ WATER_LEFT\n", - " 600003 -600421 601230 -600013 600011\n", - " Vol=1.335972e+01\n", - " imp:n=1.0 imp:p=1.0 U=5972 \n", - ] +from numjuggler.parser import ( + Card, + CID, + _split_data, + are_close_lists, + get_cards_from_input, + load_decode_buffer, +) - cell2 = Card(definition, ctype=3, pos=1) - cell2.get_values() +HERE = Path(__file__).parent +DATA = HERE / "data" +assert DATA.is_dir() - cell2._set_value_by_type("u", 30) - assert "U=30" in cell2.card() - assert "Vol=1.335972e+01" in cell2.card() + +@pytest.fixture +def card_600177() -> Card: + definition = [ + "600177 400 -1.00000e+00 $ WATER_LEFT\n", + " 600003 -600421 601230 -600013 600011\n", + " Vol=1.335972e+01\n", + " imp:n=1.0 imp:p=1.0 U=5972 \n", + ] + card = Card(definition, ctype=3, pos=1, debug=StringIO()) + card.get_values() # dvp: as in the old test, why not in constructor? + return card + + +@pytest.mark.parametrize( + "value, expected_name", + [ + (-1, "comment"), + (-2, "blankline"), + (1, "message"), + (2, "title"), + (3, "cell"), + (4, "surface"), + (5, "data"), + ], +) +def test_cid(value, expected_name): + assert CID.get_name(value) == expected_name + + +def test_cid_bad_path(): + with pytest.raises(ValueError, match="No CID names with value 100"): + CID.get_name(100) + + +def test_vol_param(card_600177: Card): + assert "Vol=1.335972e+01" in card_600177.card() + + +def test_get_and_set_value(card_600177: Card): + assert card_600177._get_value_by_type("u") == 5972 + assert card_600177._get_value_by_type("_not_existing") is None + card_600177._set_value_by_type("u", 30) + assert "U=30" in card_600177.card() + assert card_600177._get_value_by_type("u") == 30 + + +def test_print_debug(card_600177: Card): + debug_text = card_600177.debug.getvalue() + assert "Line" in debug_text + + +def test_card_get_inpt_with_bad_character(): + log = StringIO() + card = Card(["1\t0 1"], ctype=3, pos=1, debug=log) + card.get_input(check_bad_chars=True) + assert "get_input: bad char" in log.getvalue() + card = Card(["1\t0 1"], ctype=3, pos=1, debug=None) + with pytest.raises(ValueError, match="Bad character"): + card.get_input(check_bad_chars=True) + + +def test_suffix_and_prefix_properties(): + card = Card(["1 0 1\n"], ctype=3, pos=1) + card.get_values() + assert card.geom_prefix == "" + assert card.geom_suffix == "" + card.geom_prefix = "pfx" + card.geom_suffix = "sfx" + assert card.geom_prefix == "pfx" + assert card.geom_suffix == "sfx" + + +def test_fc_card(): + log = StringIO() + card = Card(["fc4 xxx\n"], ctype=5, pos=1, debug=log) + card.get_values() + card.get_input() + log_result = log.getvalue() + assert "xxx" in log_result + + +def test_card_get_refcells(): + card = Card(["1 0 1\n"], ctype=3, pos=1) + card.get_values() + # card.get_input() + actual = card.get_refcells() + assert actual == {1} + card = Card(["1 0 1 #2\n"], ctype=3, pos=1) + card.get_values() + # card.get_input() + actual = card.get_refcells() + assert actual == {1, 2} + + +def test_card_get_geom(): + card = Card(["1 0 1\n"], ctype=3, pos=1) + card.get_values() + geom = card.get_geom() + assert geom.strip() == "1" + + +def test_card_get_u(): + card = Card(["1 0 1\n"], ctype=3, pos=1) + card.get_values() + universe = card.get_u() + assert universe is None + card = Card( + [ + "1 0 1\n", + " u=200\n", + ], + ctype=3, + pos=1, + ) + card.get_values() + universe = card.get_u() + assert universe == 200 + + +def test_card_get_f(): + card = Card(["1 0 1\n"], ctype=3, pos=1) + card.get_values() + assert card.get_f() is None + card = Card( + [ + "1 0 1\n", + " fill=200\n", + ], + ctype=3, + pos=1, + ) + card.get_values() + assert card.get_f() == 200 + assert card.get_f(300) == 300 + assert card.get_f() == 300 + assert card._get_value_by_type("fill") == 300 + + +@pytest.mark.parametrize( + "card,vals,expected,msg", + [ + ( + Card(["1 0 1\n"], 3, 1), + None, + {"imp:n": 1}, + "expect `imp:n=1`, if importance is not specified", + ), + ( + Card(["1 0 1\n", " imp:n=2\n"], 3, 1), + {"n": 3.0}, + {"imp:n": 3.0}, + "expect `imp:n=3` with updated value", + ), + ( + Card(["1 0 1\n", " imp:n=2\n"], 3, 1), + None, + {"imp:n": 2}, + "expect `imp:n=2`", + ), + (Card(["F4 1\n"], 5, 1), None, None, "expect None for non cell card"), + ], +) +def test_card_get_imp(card, vals, expected, msg): + card.get_values() + actual = card.get_imp(vals) + assert actual == expected, msg + actual2 = card.get_imp() + assert actual2 is actual, "Should return previously cached value" + + +@pytest.mark.parametrize( + "card", + [ + Card(["1 0 1\n"], 3, 1), + Card(["1 0 1\n", " fill=100\n"], 3, 1), + Card(["1 0 1\n", " fill 100\n"], 3, 1), + Card(["1 0 1\n", " fill 100 (1)\n"], 3, 1), + Card(["1 0 1\n", " fill 100 (10 10 10)\n"], 3, 1), + ], +) +def test_card_remove_fill(card): + card.get_values() + card.remove_fill() + card.get_f() is None + + +@pytest.mark.parametrize( + "x,y,re,pci,expected", + [ + ([1, 2], [1, 2], 0.0, None, True), + ([1, 2], [1, 2, 3], 0.0, None, False), + ([1, 2], [1, 3], 0.0, None, False), + ([1, 2], [1.1, 2], 0.2, None, True), + ([1, 2], [1.1, 2], 0.1, None, False), + ([1, 2, 3, 100], [2, 4, 6, 100], 0.1, (0, 3), True), + ([1, 2, 3], [2, 4, 6], 0.1, (0,), True), + ([0.0, 0.0], [0.0, 1e-7], 1e-6, (0, 2), False), # comparing to zero is to be absolute + ([0.0, 1e-7], [0.0, 0.0], 1e-6, (0, 2), False), + ([0.0, 0.0], [0.0, 0.0], 1e-6, (0, 2), True), + ([0.0, 1e-7], [0.0, -1e-7], 1e-7, (0, 2), False), + ([0.0, 1e-7], [0.0, -1e-7], 2, (0, 2), True), + ([], [], 2, None, True), + ], +) +def test_are_close_lists(x, y, re, pci, expected): + actual = are_close_lists(x, y, re, pci) + assert actual == expected + + +@pytest.mark.parametrize( + "card,expected", + [ + (Card(["1 0 1 \n"], 3, 1), "1 0 1 \n"), + (Card(["1 0 1: 2\n"], 3, 1), "1 0 1:2 \n"), + (Card(["1 0 ( 1 : 2 ) (3 : 4)\n"], 3, 1), "1 0 (1:2) (3:4) \n"), + # TODO @dvp2015: why we need these trailing spaces in expected? + ], +) +def test_remove_spaces(card, expected): + card.get_values() + card.remove_spaces() + assert card.card() == expected + + +def test_card_with_like(): + card = Card(["2 LIKE 1 BUT TRCL 20\n"], 3, 1) + card.get_values() + assert card.ctype == 3 + + +def test_card_with_repetitions(): + card = Card(["F4 1 3i 5\n"], 5, 1) + card.get_values() + assert card.ctype == 5 + + +@pytest.mark.parametrize( + "card,wrap,expected", + [ + ( + Card( + [ + "1 0 100000000000 100000000001 100000000002 100000000003 100000000004" + " 100000000005 100000000006 100000000007 100000000008 \n" + ], + 3, + 1, + ), + False, + ( + "1 0 100000000000 100000000001 100000000002 100000000003 100000000004" + " 100000000005 100000000006 100000000007 100000000008 \n" + ), + ), + ( + Card( + [ + "1 0 100000000000 100000000001 100000000002 100000000003 100000000004" + " 100000000005 100000000006 100000000007 100000000008 \n" + ], + 3, + 1, + ), + True, + dedent(""" + 1 0 100000000000 100000000001 100000000002 100000000003 100000000004 + 100000000005 100000000006 100000000007 100000000008 + """)[1:-1] + + " \n", + ), + ], +) +def test_card_wrap(card, wrap, expected): + card.get_values() + actual = card.card(wrap) + assert actual == expected + + +@pytest.mark.parametrize( + "encoding", + [ + "utf8", + pytest.param( + "cp1251", marks=pytest.mark.xfail(reason="encoding auto detection fails on short texts") + ), + pytest.param( + "ascii", marks=pytest.mark.xfail(reason="acsii encoding corrupts any non english text") + ), + ], +) +def test_load_decode_buffer(cd_tmpdir, encoding): + text = "Something with Юникод valid for cp1251" + path = Path("test.txt") + path.write_text(text, encoding=encoding, errors="backslashreplace") + actual = load_decode_buffer(path).getvalue() + assert actual == text + + +@pytest.mark.parametrize( + "inp, expected", + [ # Note: split_data doesn't need \n at the end of lines + (["tr1 0 0 2"], (["tr{:<1} 0 0 2"], [(1, "tr")], "TRn")), + (["m1 00101 1"], (["m{:<1} 00101 1"], [(1, "mat")], "Mn")), + (["f4 1"], (["f{:<1} {:<1}"], [(4, "tal"), (1, "cel")], "Fn")), + (["fmesh1004"], (["fmesh{:<4}"], [(1004, "tal")], "fmesh")), + ( + ["fmesh1004", " orig 10 10 10"], + (["fmesh{:<4}", " orig 10 10 10"], [(1004, "tal")], "fmesh"), + ), + ], +) +def test_split_data(inp, expected): + actual = _split_data(inp) + assert actual == expected + + +def with_message_validator(cards: Iterable[Card]): + first_card: Card = next(cards) + assert first_card.ctype == CID.message + + +def continue_validator(cards: Iterable[Card]): + first_card: Card = next(cards) + assert first_card.ctype == CID.data + + +@pytest.mark.parametrize( + "fname, validator", + [("with_message.mcnp", with_message_validator), ("continue", continue_validator)], +) +def test_get_cards_from_input(fname, validator): + actual = get_cards_from_input(DATA / fname) + validator(actual) diff --git a/tests/test_travis.py b/tests/test_travis.py new file mode 100644 index 0000000..ca40650 --- /dev/null +++ b/tests/test_travis.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import difflib +from io import StringIO +import sys + +from pathlib import Path +import shutil +from typing import Iterable, Sequence + +import pytest + +from numjuggler.main import main + +HERE = Path(__file__).parent.absolute() +data = HERE / "data/travis_tests" +assert data.exists(), "Cannot access test data 'travis' files" + + +@pytest.mark.parametrize( + "mode,options,inp", + [ + ( + "renum", + "-c 10 -s 5 -m 100", + "i1", + ), + ( + "renum", + "-c i -s i -m i", + "i2", + ), + ( + "renum", + "-u -5942", + "i3", + ), + pytest.param( + "remh", + "", + "nested_complement", + marks=pytest.mark.xfail( + sys.platform == "win32", reason="Need more efforts on parser testing" + ), + ), + ], +) +def test_mode_options_inp(cd_tmpdir, capsys, mode, options, inp): + subdir = data / mode + source = subdir / (inp + ".i") + wrk_file = shutil.copy(source, cd_tmpdir) + command = ["--mode", mode, *options.split(), wrk_file] + main(command) + out, _ = capsys.readouterr() + ref_path = subdir / (inp + ".ref") + _assert_str_path_equal(out, ref_path) + + +cdense_data = Path(data / "cdens") + + +@pytest.mark.parametrize("inp", ["inp"]) +@pytest.mark.parametrize("map_", [f"map{x}" for x in range(1, 7)]) +def test_cdens(cd_tmpdir, capsys, inp, map_): + source = cdense_data / (inp + ".i") + wrk_file = shutil.copy(source, cd_tmpdir) + wrk_map = shutil.copy(cdense_data / map_, cd_tmpdir) + command = ["--mode", "cdens", "--map", wrk_map, wrk_file] + main(command) + out, _ = capsys.readouterr() + ref_path = cdense_data / f"{inp}.{map_}.ref" + _assert_str_path_equal(out, ref_path) + + +merge_data = Path(data / "merge") + + +# TODO @dvp2015: numjuggler parser on Windows leaves before the closing quote `inp\r"` + + +@pytest.mark.parametrize( + "inp,merged", + [ + ( + "inp", + "merged", + ) + ], +) +def test_merge(cd_tmpdir, capsys, inp, merged): + inp2_path = merge_data / (inp + "2.inp") + inp1_path = merge_data / (inp + "1.inp") + wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) + wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) + command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] + main(command) + out, _ = capsys.readouterr() + ref_path = merge_data / f"{merged}.{inp}.ref" + _assert_str_path_equal(out, ref_path) + + +def assert_lines_equal(msg_prefix: str, lines_a: list[str], lines_b: list[str]) -> None: + diff = list(difflib.Differ().compare(lines_a, lines_b)) + assert len(diff) == len(lines_a) == len(lines_b), msg_prefix + ":\n" + "".join(diff) + + +@pytest.mark.parametrize( + "a, b", + [ + ( + ["abc\n", "def\n"], + ["abc\n", "def\n"], + ), + ( + ["abc\n", "def"], + ["abc\n", "def"], + ), + ], +) +def test_assert_lines_equal(a, b): + assert_lines_equal("xxx", a, b) + + +@pytest.mark.parametrize( + "a, b", + [ + ( + ["abc\n", "def\n"], + ["cab\n", "def\n"], + ), + ( + ["abc\n", "def"], + ["abc\n"], + ), + ], +) +def test_assert_lines_equal_when_not_equal(a, b): + with pytest.raises(AssertionError, match="xxx"): + assert_lines_equal("xxx", a, b) + + +def _assert_str_path_equal(out: str, ref_path: Path) -> None: + with ref_path.open(encoding="utf8") as f: + actual = StringIO(out).readlines() + expected = f.readlines() + if sys.platform == "win32": + actual = [s.replace("\r", "") for s in actual] + expected = [s.replace("\r", "") for s in expected] + assert_lines_equal(ref_path.name, actual, expected) diff --git a/numjuggler/utils/__init__.py b/tests/utils/__init__.py similarity index 100% rename from numjuggler/utils/__init__.py rename to tests/utils/__init__.py diff --git a/tests/utils/test_io.py b/tests/utils/test_io.py new file mode 100644 index 0000000..56e075c --- /dev/null +++ b/tests/utils/test_io.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +from numjuggler.utils import resolve_fname_or_stream + + +def test_resolve_fname_or_stream(cd_tmpdir): + fname = "some.txt" + path = Path(fname) + text = "some text" + assert not path.exists() + with resolve_fname_or_stream(fname, mode="w") as fid: + fid.write(text) + assert path.exists(), "Should open a file, if file name is given as string" + actual = path.read_text() + assert actual == text, f"Expected content: {text}, not {actual}" + with path.open() as fid: + with resolve_fname_or_stream(fid, mode="w") as fid_passed: + assert fid is fid_passed, "Should pass file object as is" + with resolve_fname_or_stream(path) as fid: + assert fid.read() == text, "Should open a file, if file is given as Path" + with resolve_fname_or_stream(path, mode="w") as fid: + fid.write(text) + assert path.exists(), "Should create a file, if file name is given as Path" + + +def test_stdout(capsys): + text = "for std" + with resolve_fname_or_stream(None, mode="w") as fid: + assert fid is sys.stdout + fid.write(text) + out, _ = capsys.readouterr() + assert out == text + + +def test_stdin(capsys): + with resolve_fname_or_stream(None) as fid: + assert fid is sys.stdin diff --git a/tests/utils/test_partial_formatter.py b/tests/utils/test_partial_formatter.py new file mode 100644 index 0000000..cfce3ed --- /dev/null +++ b/tests/utils/test_partial_formatter.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import pytest + +from numjuggler.utils import PartialFormatter + + +@pytest.mark.parametrize( + "fmt, fillers, expected", + [ + ("{a},{b}", {"a": 1, "b": 2}, "1,2"), + ("{a},{b}", {"a": 1}, "1,{b}"), + ("{a},{b}", {"a": 1, "b": 2, "c": 3}, "1,2"), + ], +) +def test_partial_formatter(fmt, fillers, expected): + assert PartialFormatter().format(fmt, **fillers) == expected + + +@pytest.mark.parametrize( + "fmt, fillers, expected", + [ + ("{a},{b}", {"a": 1, "b": 2}, "1,2"), + ("{a},{b}", {"a": 1, "b": 2, "c": 3}, "1,2"), + ], +) +def test_string_formatter(fmt, fillers, expected): + assert fmt.format(fmt, **fillers) == expected + + +@pytest.mark.parametrize( + "fmt, fillers, expected", + [ + ("{a},{b}", {"a": 1}, "1,{b}"), + ], +) +def test_string_formatter_bad_path(fmt, fillers, expected): + with pytest.raises(KeyError, match="'b'"): + fmt.format(fmt, **fillers) == expected + + +@pytest.mark.parametrize( + "fmt, args, fillers, expected", + [ + ("{},{a},{},{b}", ["x", "y"], {"a": 1, "b": 2}, "x,1,y,2"), + ("{},{},{a},{b}", ["x", "y"], {"a": 1}, "x,y,1,{b}"), + ("{a},{},{b},{}", ["x", "y"], {"a": 1, "b": 2, "c": 3}, "1,x,2,y"), + ], +) +def test_partial_formatter_with_args(fmt, args, fillers, expected): + assert PartialFormatter().format(fmt, *args, **fillers) == expected diff --git a/tools/install/install-just b/tools/install/install-just new file mode 100755 index 0000000..ca4b7d4 --- /dev/null +++ b/tools/install/install-just @@ -0,0 +1,12 @@ +#!/bin/bash + +command -v just > /dev/null && echo "$(just --version) is already installed" && exit 0 + + +if [ ! command -v cargo ]; then + echo "'just' is a Rust application, install rustup in advance" + exit 1 +fi + +cargo install just +just --version diff --git a/tools/install/install-rustup b/tools/install/install-rustup new file mode 100755 index 0000000..f41fc3f --- /dev/null +++ b/tools/install/install-rustup @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Rust see: https://www.rust-lang.org/ +# Rust install, see: https://www.rust-lang.org/tools/install +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +rustup update stable + diff --git a/tools/install/install-uv b/tools/install/install-uv new file mode 100755 index 0000000..1264fb0 --- /dev/null +++ b/tools/install/install-uv @@ -0,0 +1,11 @@ +#!/bin/bash + +# +# Install uv, fast Rust based Python project and environment manager +# See https://github.com/astral-sh/uv +# +command -v uv && echo "$(uv --version) is already installed" && exit 0 + +curl -LsSf https://astral.sh/uv/install.sh | sh + +# vim: set ts=4 sw=4 tw=92 ft=sh ss=0 et ai : diff --git a/show_graph.sh b/tools/show_graph.sh similarity index 100% rename from show_graph.sh rename to tools/show_graph.sh diff --git a/travis_run_all.sh b/travis_run_all.sh deleted file mode 100755 index e34e9dc..0000000 --- a/travis_run_all.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# Assuming that numjuggler is installed properly -# and tests are in travis_tests(see .travis.yml) - -# Check numjuggler version -numjuggler --version - -odir=$(pwd) - -cd $odir/travis_tests/renum -i=i1 -o="-c 10 -s 5 -m 100" -numjuggler $o $i.i > $i.res && diff -w $i.ref $i.res > $i.diff || exit 1 - -i=i2 -o="-c i -s i -m i" -numjuggler $o $i.i > $i.res && diff -w $i.ref $i.res > $i.diff || exit 1 - -i=i3 -o="-u -5942" -numjuggler $o $i.i > $i.res && diff -w $i.ref $i.res > $i.diff || exit 1 - - -cd $odir/travis_tests/remh -i=nested_complement -o="--mode remh" -numjuggler $o $i.i > $i.res && diff -w $i.ref $i.res > $i.diff || exit 1 - -cd $odir/travis_tests/cdens -i=inp.i -o="--mode cdens" -for m in $(ls map?); do - echo $m - numjuggler $o --map $m inp.i > inp.$m && diff -w inp.$m.ref inp.$m > $m.diff || exit 1 -done - -cd $odir/travis_tests/merge -numjuggler --mode merge -m inp2.inp inp1.inp > merged.inp && diff -w merged.inp.ref merged.inp > merged.diff || exit 1 - diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f4a3863 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1315 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/52/505c207f334d51e937cbaa27ff95776e16e2d120e13cbe491cd7b3a70b50/chardet-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", size = 870747, upload-time = "2026-04-13T21:32:56.916Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/d3c79495dee4831b8bebca2790e72cb90f0c5849c940570a7c7e5b70b952/chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", size = 853210, upload-time = "2026-04-13T21:32:58.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/99/f6a822ad1bde25a4c38dc3e770485e78e0893dfd871cd6e18ed3ea3a795e/chardet-7.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", size = 873625, upload-time = "2026-04-13T21:32:59.606Z" }, + { url = "https://files.pythonhosted.org/packages/b1/10/31932775c94a86814f76b41c4a772b52abfb0e6125324f32c6da1196c297/chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", size = 883436, upload-time = "2026-04-13T21:33:01.351Z" }, + { url = "https://files.pythonhosted.org/packages/6c/63/0f43e3acf2c436fdb32a0f904aeb03a2904d2126eed34a042a194d235926/chardet-7.4.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", size = 876589, upload-time = "2026-04-13T21:33:02.636Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a6/e9b8f8a3e99602792b01fa7d0a731737615ab56d8bfd0b52935a0ef88b85/chardet-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", size = 941866, upload-time = "2026-04-13T21:33:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe-public-wildcard-imports" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/b4/bab08ba73fece60828a0d35b252fd3ec2399a4afd901a82e837d39a0483c/griffe_public_wildcard_imports-0.3.1.tar.gz", hash = "sha256:bca005c76ff1a4b2f143e64d8445758e0a13f1ad439c41102045316ce2fa905e", size = 27607, upload-time = "2026-02-21T09:38:47.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/a7/9b17661c0e4d3406e637406bce5a026669ae7f232db4aac15400dd7ca999/griffe_public_wildcard_imports-0.3.1-py3-none-any.whl", hash = "sha256:877ab5d290b24978cce50233bf1cc23b6d1022f3d85a8e601ffa7efd871e610b", size = 6887, upload-time = "2026-02-20T11:25:00.717Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2d/bdf1aee3f4f7b34148b0f62298b62f03415160cb2707f09503c99a0a7cd5/mkdocs_include_markdown_plugin-7.2.2.tar.gz", hash = "sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9", size = 25415, upload-time = "2026-03-29T15:15:14.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/a5/f6b2f0aa805dbda52f6265e9aff1450c8643195442facf29d475bdeba15d/mkdocs_include_markdown_plugin-7.2.2-py3-none-any.whl", hash = "sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1", size = 29528, upload-time = "2026-03-29T15:15:13.079Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numjuggler" +source = { editable = "." } +dependencies = [ + { name = "chardet" }, + { name = "numpy" }, + { name = "six" }, +] + +[package.dev-dependencies] +dev = [ + { name = "griffe-public-wildcard-imports" }, + { name = "mkdocs" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pre-commit" }, + { name = "pygments" }, + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "ty" }, + { name = "xdoctest" }, +] +docs = [ + { name = "griffe-public-wildcard-imports" }, + { name = "mkdocs" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pygments" }, +] +pylint = [ + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, +] +style = [ + { name = "pre-commit" }, + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, + { name = "ruff" }, + { name = "ty" }, +] +test = [ + { name = "pygments" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "xdoctest" }, +] + +[package.metadata] +requires-dist = [ + { name = "chardet", specifier = ">=7.4.3" }, + { name = "numpy", specifier = ">=2.4.4" }, + { name = "six", specifier = ">=1.17.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "griffe-public-wildcard-imports" }, + { name = "mkdocs" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29" }, + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pygments" }, + { name = "pygments", specifier = ">=2.20.0" }, + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.15.11" }, + { name = "ty", specifier = ">=0.0.31" }, + { name = "xdoctest", specifier = ">=1.3.2" }, +] +docs = [ + { name = "griffe-public-wildcard-imports" }, + { name = "mkdocs" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29" }, + { name = "pygments" }, +] +pylint = [ + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, +] +style = [ + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, + { name = "ruff", specifier = ">=0.15.11" }, + { name = "ty", specifier = ">=0.0.31" }, +] +test = [ + { name = "pygments", specifier = ">=2.20.0" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "xdoctest", specifier = ">=1.3.2" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, +] + +[[package]] +name = "pylint-per-file-ignores" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pylint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/bc/6d40a3596f91ef23fca7b89983b78bf4b3686323fd98e44e53ae365b1880/pylint_per_file_ignores-3.2.1.tar.gz", hash = "sha256:0a89f3cdc6fa09244a3f5624ad977ac9b026f0b25b2adb48c97c080da8d858f9", size = 81844, upload-time = "2026-04-03T19:35:37.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/68/2b0cc27b549fd788caae254752910fe7222ac47c71627c60926895fe8960/pylint_per_file_ignores-3.2.1-py3-none-any.whl", hash = "sha256:aaac8b118791e742ccf7baaf42346978f6cd0440a9090d4087fc8ff26e4a31f2", size = 5699, upload-time = "2026-04-03T19:35:36.524Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "ty" +version = "0.0.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/cc/5ea5d3a72216c8c2bf77d83066dd4f3553532d0aacc03d4a8397dd9845e1/ty-0.0.31.tar.gz", hash = "sha256:4a4094292d9671caf3b510c7edf36991acd9c962bb5d97205374ffed9f541c45", size = 5516619, upload-time = "2026-04-15T15:47:59.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/10/ea805cbbd75d5d50792551a2b383de8521eeab0c44f38c73e12819ced65e/ty-0.0.31-py3-none-linux_armv6l.whl", hash = "sha256:761651dc17ad7bc0abfc1b04b3f0e84df263ed435d34f29760b3da739ab02d35", size = 10834749, upload-time = "2026-04-15T15:48:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4c/fabf951850401d24d36b21bced088a366c6827e1c37dab4523afff84c4b2/ty-0.0.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c529922395a07231c27488f0290651e05d27d149f7e0aa807678f1f7e9c58a5e", size = 10626012, upload-time = "2026-04-15T15:48:22.554Z" }, + { url = "https://files.pythonhosted.org/packages/04/b0/4a5aff88d2544f19514a59c8f693d63144aa7307fe2ee5df608333ab5460/ty-0.0.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f345df2b87d747859e72c2cbc9be607ea1bbc8bc93dd32fa3d03ea091cb4fee", size = 10075790, upload-time = "2026-04-15T15:47:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/d5/73/9d4dcad12cd4e85274014f2c0510ef93f590b2a1e5148de3a9f276098dad/ty-0.0.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b207eddcfbafd376132689d3435b14efcb531289cb59cd961c6a611133bd54", size = 10590286, upload-time = "2026-04-15T15:48:06.222Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/fe40adde18692359ded174ae7ddbfac056e876eb0f43b65be74fde7f6072/ty-0.0.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:663778b220f357067488ce68bfc52335ccbd161549776f70dcbde6bbde82f77a", size = 10623824, upload-time = "2026-04-15T15:48:12.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e8/0ffa2e09b548e6daa9ebc368d68b767dc2405ca4cbeadb7ede0e2cb21059/ty-0.0.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3506cfe87dfade0fb2960dd4fffd4fd8089003587b3445c0a1a295c9d83764fb", size = 11156864, upload-time = "2026-04-15T15:48:08.473Z" }, + { url = "https://files.pythonhosted.org/packages/08/e9/fd44c2075115d569593ee9473d7e2a38b750fd7e783421c95eb528c15df5/ty-0.0.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b3f3d8492f08e81916026354c1d1599e9ddfa1241804141a74d5662fc710085", size = 11696401, upload-time = "2026-04-15T15:48:17.355Z" }, + { url = "https://files.pythonhosted.org/packages/4e/50/35aad8eadf964d23e2a4faa5b38a206aa85c78833c8ce335dddd2c34ba63/ty-0.0.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a97de32ee6a619393a4c495e056a1c547de7877510f3152e61345c71d774d2d0", size = 11374903, upload-time = "2026-04-15T15:47:55.893Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/01eccd25d23f5aaa7f7ff1a87b5b215469f6b202cf689a1812b71c1e7f6b/ty-0.0.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c906354ce441e342646582bc9b8f48a676f79f3d061e25de15ff870e015ca14e", size = 11206624, upload-time = "2026-04-15T15:47:51.778Z" }, + { url = "https://files.pythonhosted.org/packages/f4/70/baad2914cb097453f127a221f8addb2b41926098059cd773c75e6a662fc4/ty-0.0.31-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:275bb7c82afcbf89fe2dbef1b2692f2bc98451f1ee2c8eb809ddd91317822388", size = 10575089, upload-time = "2026-04-15T15:47:49.448Z" }, + { url = "https://files.pythonhosted.org/packages/83/12/bae3a7bba2e785eb72ce00f9da70eedcb8c5e8299efecbd16e6e436abd82/ty-0.0.31-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:405da247027c6efd1e264886b6ac4a86ab3a4f09200b02e33630efe85f119e53", size = 10642315, upload-time = "2026-04-15T15:48:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/93/9e/cad04d5d839bc60355cea98c7e09d724ea65f47184def0fae8b90dc54591/ty-0.0.31-py3-none-musllinux_1_2_i686.whl", hash = "sha256:54d9835608eed196853d6643f645c50ce83bcc7fe546cdb3e210c1bcf7c58c09", size = 10834473, upload-time = "2026-04-15T15:48:02.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/84112d280182d37690d3d2b4018b2667e42bc281585e607015635310016a/ty-0.0.31-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ee11be9b07e8c0c6b455ff075a0abe4f194de9476f57624db98eec9df618355", size = 11315785, upload-time = "2026-04-15T15:48:10.754Z" }, + { url = "https://files.pythonhosted.org/packages/50/9f/ac42dc223d7e0950e97a1854567a8b3e7fe09ad7375adbf91bfb43290482/ty-0.0.31-py3-none-win32.whl", hash = "sha256:7286587aacf3eef0956062d6492b893b02f82b0f22c5e230008e13ff0d216a8b", size = 10187657, upload-time = "2026-04-15T15:48:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/57ba7ea7ecb2f4751644ba91756e2be70e33ef5952c0c41a256a0e4c2437/ty-0.0.31-py3-none-win_amd64.whl", hash = "sha256:81134e25d2a2562ab372f24de8f9bd05034d27d30377a5d7540f259791c6234c", size = 11205258, upload-time = "2026-04-15T15:47:53.759Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/bca669095ccf0a400af941fdf741578d4c2d6719f1b7f10e6dbec10aa862/ty-0.0.31-py3-none-win_arm64.whl", hash = "sha256:e9cb15fad26545c6a608f40f227af3a5513cb376998ca6feddd47ca7d93ffafa", size = 10590392, upload-time = "2026-04-15T15:47:57.968Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "xdoctest" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/df/1b5751e3546967a4884cff6ea743b1e457d3f7c94fc35913f149a85734fc/xdoctest-1.3.2.tar.gz", hash = "sha256:bf6078c4fc0d60aafd8753fccdc435c95f26a640508e606d188d96f48359f0aa", size = 209923, upload-time = "2026-03-27T01:13:25.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/1d/6b9cf46122f2f07eab95c4af29ab5e29bd6674147a0b1e9a6d3929870af1/xdoctest-1.3.2-py3-none-any.whl", hash = "sha256:052118c8efb2b4cfb54485d328915b9e7b44da37c64b0998ca6aa21193dcb601", size = 146469, upload-time = "2026-03-27T01:13:23.417Z" }, +]