From bda624b0e913df9cfdb6b79618d8de74e5430560 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 00:12:55 +0300 Subject: [PATCH 01/54] test: fixed test dependencies and execution --- pyproject.toml | 5 +++++ tests/test_main.py | 37 ++++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7477710..bc9055e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,4 +7,9 @@ [tool.setuptools_scm] write_to = 'numjuggler/version.py' +[dependency-groups] +test = [ + "pytest>=9.0.3", +] + diff --git a/tests/test_main.py b/tests/test_main.py index 666b528..5066d0b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, division, nested_scopes +from __future__ import annotations from pathlib import Path @@ -8,14 +7,15 @@ 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,18 +25,21 @@ 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" - ), -]) +@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): source = test_data_path / inp command = command.split() @@ -44,6 +47,6 @@ def test_test_main(tmpdir, capsys, inp, command, expected): with cd_temporarily(tmpdir): main(command) out, err = capsys.readouterr() - actual_numbers = load_line_heading_numbers(out.split('\n')) + 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" From 4d9d7844d61ae000b69ddef8c726c793264b2a72 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 00:23:18 +0300 Subject: [PATCH 02/54] build: remove redundant Path import handling --- numjuggler/utils/io.py | 11 ++++++++--- numjuggler/utils/resource.py | 7 ------- pyproject.toml | 1 + 3 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 numjuggler/utils/resource.py diff --git a/numjuggler/utils/io.py b/numjuggler/utils/io.py index 20e6d13..d98fd73 100644 --- a/numjuggler/utils/io.py +++ b/numjuggler/utils/io.py @@ -1,8 +1,8 @@ import os import sys -from .resource import Path from contextlib import contextmanager +from pathlib import Path @contextmanager @@ -17,13 +17,18 @@ def cd_temporarily(cd_to): @contextmanager def resolve_fname_or_stream(fname_or_stream, mode="r"): - is_input = 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"): + 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: 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/pyproject.toml b/pyproject.toml index bc9055e..29abda8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ [dependency-groups] test = [ "pytest>=9.0.3", + "pytest-cov>=7.1.0", ] From 148fb7ac0545c95bbd7a73014361d2a8d6ee9e09 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 00:33:25 +0300 Subject: [PATCH 03/54] style: hide implementation details --- numjuggler/__init__.py | 3 +-- numjuggler/parser.py | 2 +- numjuggler/utils/__init__.py | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/numjuggler/__init__.py b/numjuggler/__init__.py index defed47..92c63ef 100644 --- a/numjuggler/__init__.py +++ b/numjuggler/__init__.py @@ -4,11 +4,10 @@ Package provides script mcnp.juggler. See its help for description. """ -from numjuggler.utils.PartialFormatter import PartialFormatter 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/numjuggler/parser.py b/numjuggler/parser.py index 7e950e2..832e55b 100644 --- a/numjuggler/parser.py +++ b/numjuggler/parser.py @@ -12,7 +12,7 @@ import os from chardet import UniversalDetector from io import StringIO -from numjuggler import PartialFormatter +from numjuggler.utils import PartialFormatter try: # This clause define the fallback for cPickle, which is an accelerated diff --git a/numjuggler/utils/__init__.py b/numjuggler/utils/__init__.py index e69de29..1fdb023 100644 --- a/numjuggler/utils/__init__.py +++ b/numjuggler/utils/__init__.py @@ -0,0 +1,3 @@ +from .PartialFormatter import PartialFormatter + +__all__ = ["PartialFormatter"] From 1cb8ffe63b4c3c47b45f8ab93eb4eb4c4de1a478 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 12:14:30 +0300 Subject: [PATCH 04/54] build: modernize project configuration - Stop support of Python2 in favor of new libraries - Stop using deprecates setup.* files - Add style checks - Workaroun on missed branch in ancient splitter module --- .coveragerc | 33 --- .envrc | 1 + justfile | 202 +++++++++++++++ numjuggler/splitter.py | 89 ++++--- pylintrc.toml | 555 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 81 +++++- pytest.ini | 14 -- pytest.toml | 47 ++++ ruff.toml | 120 +++++++++ setup.cfg | 2 - setup.py | 67 ----- 11 files changed, 1045 insertions(+), 166 deletions(-) delete mode 100644 .coveragerc create mode 100644 .envrc create mode 100644 justfile create mode 100644 pylintrc.toml delete mode 100644 pytest.ini create mode 100644 pytest.toml create mode 100644 ruff.toml delete mode 100644 setup.cfg delete mode 100644 setup.py 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/justfile b/justfile new file mode 100644 index 0000000..39d42a7 --- /dev/null +++ b/justfile @@ -0,0 +1,202 @@ +# 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 := true + +default_python := "3.13" +TITLE := `uv version` +VERSION := `uv version --short` + +log := "warn" + +export JUST_LOG := log + +@_default: + just --list + +# 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" + "docs/_build" + "htmlcov" + ) + for d in "${dirs_to_clean[@]}"; do + find . -type d -wholename "$d" -exec rm -rf {} + + done + coverage erase + + +# 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": # this check python version on moving to the python-3.14 + uvx pyupgrade {{args}} # presumably, code is updated by ruff, just to check sometimes + +# 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 python -m xdoctest --silent -c all src 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: 'lint'] +@ruff: + ruff check --fix src tests + ruff format src tests + +# Run pre-commit on all files +[group: 'lint'] +@pre-commit: + uv run --no-dev --group pre-commit 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: 'lint'] +@pylint: + uv run --no-dev --group lint pylint --recursive=y --output-format colorized src tests + +[group: 'lint'] +@pyright: + uv run --no-dev --group pyright pyright src tests + +# Lint with ty +[group: 'lint'] +@ty: + uvx ty check + +# Check rst-texts +[group: 'docs'] +@rstcheck: + uv run --no-dev --group docs rstcheck --recursive *.rst docs + +# build documentation +[group: 'docs'] +@docs-build: rstcheck + uv run --no-dev --group docs sphinx-build docs/source docs/_build + +# browse and edit documentation with auto build +[group: 'docs'] +@docs: + uv run --no-dev --group docs --group docs sphinx-autobuild --open-browser docs/source docs/_build diff --git a/numjuggler/splitter.py b/numjuggler/splitter.py index 8c6eca1..2fca700 100644 --- a/numjuggler/splitter.py +++ b/numjuggler/splitter.py @@ -10,69 +10,69 @@ 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'] +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 +83,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 +102,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/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 29abda8..9cd1a07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,15 +2,86 @@ # 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" +version = "0.6.1.dev2" +description = "MCNP input file renumbering tool" +readme = "README.rst" +keywords = ["abundance", "element", "nuclide"] +license = { text = "GPLv3" } +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", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "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 = 'numjuggler/version.py' [dependency-groups] -test = [ - "pytest>=9.0.3", - "pytest-cov>=7.1.0", +dev = [{ include-group = "style" }, { include-group = "test" }] +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 = ["pytest>=9.0.3", "pytest-cov>=7.1.0"] +[tool.coverage] +run.branch = true +run.omit = [ "**/__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 = 100 +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..ebd0546 --- /dev/null +++ b/pytest.toml @@ -0,0 +1,47 @@ +[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", +] +doctest_optionflags = [ + "ELLIPSIS", + "NORMALIZE_WHITESPACE", + "IGNORE_EXCEPTION_DETAIL", + "ALLOW_UNICODE", + "ALLOW_BYTES", + "NUMBER", +] +testpaths = ["tests", "numjuggler", "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/ruff.toml b/ruff.toml new file mode 100644 index 0000000..5cd43d0 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,120 @@ +target-version = "py312" +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", + "venv", + "wrk", +] + +[lint] +select = ["ALL"] +ignore = [ + "ANN", + "COM812", + "D", + "D203", + "D211", + "D213", + "FRA", + "Q" +] + +[lint.per-file-ignores] +"benchmarks/*" = ["S101"] +"tests/*" = [ + "ANN", + "D100", + "D101", + "D102", + "D103", + "D104", + "PLR2004", + "S101", +] +"tools/*" = ["T201", "INP001", "S603", "S607"] + +[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/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', - ], -) From 9f9c6eea19ab540f788d54c1986b9123351cdc15 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 16:09:27 +0300 Subject: [PATCH 05/54] build: switch to src-layout --- justfile | 88 +++++++++---------- pyproject.toml | 43 +++++---- pytest.toml | 2 +- {numjuggler => src/numjuggler}/__init__.py | 0 {numjuggler => src/numjuggler}/__main__.py | 0 {numjuggler => src/numjuggler}/fmesh.py | 0 {numjuggler => src/numjuggler}/likefunc.py | 0 {numjuggler => src/numjuggler}/main.py | 0 {numjuggler => src/numjuggler}/mapparsers.py | 0 {numjuggler => src/numjuggler}/nogq.py | 0 {numjuggler => src/numjuggler}/nogq2.py | 0 {numjuggler => src/numjuggler}/numbering.py | 0 {numjuggler => src/numjuggler}/parser.py | 0 {numjuggler => src/numjuggler}/readme.rst | 0 {numjuggler => src/numjuggler}/ri_notation.py | 0 {numjuggler => src/numjuggler}/shortener.py | 0 {numjuggler => src/numjuggler}/splitter.py | 0 .../numjuggler}/string_cells.py | 0 {numjuggler => src/numjuggler}/trial_map.txt | 0 .../numjuggler}/utils/PartialFormatter.py | 0 .../numjuggler}/utils/__init__.py | 0 {numjuggler => src/numjuggler}/utils/io.py | 0 22 files changed, 70 insertions(+), 63 deletions(-) rename {numjuggler => src/numjuggler}/__init__.py (100%) rename {numjuggler => src/numjuggler}/__main__.py (100%) rename {numjuggler => src/numjuggler}/fmesh.py (100%) rename {numjuggler => src/numjuggler}/likefunc.py (100%) rename {numjuggler => src/numjuggler}/main.py (100%) rename {numjuggler => src/numjuggler}/mapparsers.py (100%) rename {numjuggler => src/numjuggler}/nogq.py (100%) rename {numjuggler => src/numjuggler}/nogq2.py (100%) rename {numjuggler => src/numjuggler}/numbering.py (100%) rename {numjuggler => src/numjuggler}/parser.py (100%) rename {numjuggler => src/numjuggler}/readme.rst (100%) rename {numjuggler => src/numjuggler}/ri_notation.py (100%) rename {numjuggler => src/numjuggler}/shortener.py (100%) rename {numjuggler => src/numjuggler}/splitter.py (100%) rename {numjuggler => src/numjuggler}/string_cells.py (100%) rename {numjuggler => src/numjuggler}/trial_map.txt (100%) rename {numjuggler => src/numjuggler}/utils/PartialFormatter.py (100%) rename {numjuggler => src/numjuggler}/utils/__init__.py (100%) rename {numjuggler => src/numjuggler}/utils/io.py (100%) diff --git a/justfile b/justfile index 39d42a7..6984317 100644 --- a/justfile +++ b/justfile @@ -17,9 +17,9 @@ alias c := check set dotenv-load := true default_python := "3.13" -TITLE := `uv version` -VERSION := `uv version --short` - +# TITLE := `uv version` +TITLE := `uv run --with setuptools_scm python -m setuptools_scm` +# VERSION := `uv version --short` log := "warn" export JUST_LOG := log @@ -82,14 +82,14 @@ export JUST_LOG := log @check: pre-commit test # Check style includeing mypy and pylint and test -[group: 'dev'] -@check-full: check mypy pylint pyright +# [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 +# # 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'] @@ -113,7 +113,7 @@ export JUST_LOG := log # run pyupgrade [group: 'dev'] @pyupgrade *args="--py314-plus": # this check python version on moving to the python-3.14 - uvx pyupgrade {{args}} # presumably, code is updated by ruff, just to check sometimes + uvx pyupgrade {{args}} # presumably, code is updated by ruff, just to check occasionally # test up to the first fail [group: 'test'] @@ -135,10 +135,10 @@ export JUST_LOG := log @test *args: uv run --no-dev --group test pytest {{args}} -# run documentation tests -[group: 'test'] -@xdoctest *args: - uv run --no-dev --group test python -m xdoctest --silent -c all src tools {{args}} +# # run documentation tests +# [group: 'test'] +# @xdoctest *args: +# uv run --no-dev --group test python -m xdoctest --silent -c all src tools {{args}} # create coverage data [group: 'test'] @@ -152,51 +152,51 @@ export JUST_LOG := log 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}} +# [group: 'test'] +# typeguard *args: +# @uv run --no-dev --group test --group typeguard pytest --typeguard-packages=src {{args}} # ruff check and format -[group: 'lint'] +[group: 'style'] @ruff: ruff check --fix src tests ruff format src tests # Run pre-commit on all files -[group: 'lint'] +[group: 'style'] @pre-commit: uv run --no-dev --group pre-commit 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: 'lint'] +# @mypy: +# uv run --no-dev --group mypy mypy src tests docs/source/conf.py -[group: 'lint'] +[group: 'style'] @pylint: uv run --no-dev --group lint pylint --recursive=y --output-format colorized src tests -[group: 'lint'] -@pyright: - uv run --no-dev --group pyright pyright src tests +# [group: 'lint'] +# @pyright: +# uv run --no-dev --group pyright pyright src tests # Lint with ty -[group: 'lint'] +[group: 'style'] @ty: - uvx ty check - -# Check rst-texts -[group: 'docs'] -@rstcheck: - uv run --no-dev --group docs rstcheck --recursive *.rst docs - -# build documentation -[group: 'docs'] -@docs-build: rstcheck - uv run --no-dev --group docs sphinx-build docs/source docs/_build - -# browse and edit documentation with auto build -[group: 'docs'] -@docs: - uv run --no-dev --group docs --group docs sphinx-autobuild --open-browser docs/source docs/_build + ty check + +# # Check rst-texts +# [group: 'docs'] +# @rstcheck: +# uv run --no-dev --group docs rstcheck --recursive *.rst docs +# +# # build documentation +# [group: 'docs'] +# @docs-build: rstcheck +# uv run --no-dev --group docs sphinx-build docs/source docs/_build +# +# # browse and edit documentation with auto build +# [group: 'docs'] +# @docs: +# uv run --no-dev --group docs --group docs sphinx-autobuild --open-browser docs/source docs/_build diff --git a/pyproject.toml b/pyproject.toml index 9cd1a07..c8c86ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,12 @@ -# [tool.towncrier] -# directory = 'changes' - [build-system] requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4'] [project] name = "numjuggler" -version = "0.6.1.dev2" +dynamic = ["version"] description = "MCNP input file renumbering tool" readme = "README.rst" -keywords = ["abundance", "element", "nuclide"] +keywords = ["MCNP", "ITER", "PARSER", "RENUMBER"] license = { text = "GPLv3" } authors = [{ name = "A.Travleev", email = "anton.travleev@gmail.com" }] requires-python = ">=3.11" @@ -29,11 +26,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Physics", "Topic :: Utilities", ] -dependencies = [ - "chardet>=7.4.3", - "numpy>=2.4.4", - "six>=1.17.0", -] +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/" @@ -42,10 +35,25 @@ 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 = "style" }, { include-group = "test" }] +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" }, @@ -59,10 +67,10 @@ test = ["pytest>=9.0.3", "pytest-cov>=7.1.0"] [tool.coverage] run.branch = true -run.omit = [ "**/__init__.py", "*_tab.py", "tools/*.py" ] +run.omit = ["**/__init__.py", "*_tab.py", "tools/*.py"] run.parallel = true -run.source = [ "src" ] -paths.source = [ "src" ] +run.source = ["src"] +paths.source = ["src"] # Regexes for lines to exclude from consideration report.exclude_lines = [ # Don't complain about missing debug-only code: @@ -78,10 +86,9 @@ report.exclude_lines = [ "raise AssertionError", "raise NotImplementedError", ] -report.fail_under = 100 +report.fail_under = 27 report.ignore_errors = true -report.omit = [ "**/__init__.py", "**/types.py", "*_tab.py" ] +report.omit = ["**/__init__.py", "**/types.py", "*_tab.py"] report.show_missing = true report.skip_covered = true report.sort = "Cover" - diff --git a/pytest.toml b/pytest.toml index ebd0546..81906a8 100644 --- a/pytest.toml +++ b/pytest.toml @@ -34,7 +34,7 @@ doctest_optionflags = [ "ALLOW_BYTES", "NUMBER", ] -testpaths = ["tests", "numjuggler", "docs"] +testpaths = ["tests", "src", "docs"] markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"] filterwarnings = [ "error", diff --git a/numjuggler/__init__.py b/src/numjuggler/__init__.py similarity index 100% rename from numjuggler/__init__.py rename to src/numjuggler/__init__.py diff --git a/numjuggler/__main__.py b/src/numjuggler/__main__.py similarity index 100% rename from numjuggler/__main__.py rename to src/numjuggler/__main__.py diff --git a/numjuggler/fmesh.py b/src/numjuggler/fmesh.py similarity index 100% rename from numjuggler/fmesh.py rename to src/numjuggler/fmesh.py diff --git a/numjuggler/likefunc.py b/src/numjuggler/likefunc.py similarity index 100% rename from numjuggler/likefunc.py rename to src/numjuggler/likefunc.py diff --git a/numjuggler/main.py b/src/numjuggler/main.py similarity index 100% rename from numjuggler/main.py rename to src/numjuggler/main.py diff --git a/numjuggler/mapparsers.py b/src/numjuggler/mapparsers.py similarity index 100% rename from numjuggler/mapparsers.py rename to src/numjuggler/mapparsers.py diff --git a/numjuggler/nogq.py b/src/numjuggler/nogq.py similarity index 100% rename from numjuggler/nogq.py rename to src/numjuggler/nogq.py diff --git a/numjuggler/nogq2.py b/src/numjuggler/nogq2.py similarity index 100% rename from numjuggler/nogq2.py rename to src/numjuggler/nogq2.py diff --git a/numjuggler/numbering.py b/src/numjuggler/numbering.py similarity index 100% rename from numjuggler/numbering.py rename to src/numjuggler/numbering.py diff --git a/numjuggler/parser.py b/src/numjuggler/parser.py similarity index 100% rename from numjuggler/parser.py rename to src/numjuggler/parser.py diff --git a/numjuggler/readme.rst b/src/numjuggler/readme.rst similarity index 100% rename from numjuggler/readme.rst rename to src/numjuggler/readme.rst diff --git a/numjuggler/ri_notation.py b/src/numjuggler/ri_notation.py similarity index 100% rename from numjuggler/ri_notation.py rename to src/numjuggler/ri_notation.py diff --git a/numjuggler/shortener.py b/src/numjuggler/shortener.py similarity index 100% rename from numjuggler/shortener.py rename to src/numjuggler/shortener.py diff --git a/numjuggler/splitter.py b/src/numjuggler/splitter.py similarity index 100% rename from numjuggler/splitter.py rename to src/numjuggler/splitter.py diff --git a/numjuggler/string_cells.py b/src/numjuggler/string_cells.py similarity index 100% rename from numjuggler/string_cells.py rename to src/numjuggler/string_cells.py diff --git a/numjuggler/trial_map.txt b/src/numjuggler/trial_map.txt similarity index 100% rename from numjuggler/trial_map.txt rename to src/numjuggler/trial_map.txt diff --git a/numjuggler/utils/PartialFormatter.py b/src/numjuggler/utils/PartialFormatter.py similarity index 100% rename from numjuggler/utils/PartialFormatter.py rename to src/numjuggler/utils/PartialFormatter.py diff --git a/numjuggler/utils/__init__.py b/src/numjuggler/utils/__init__.py similarity index 100% rename from numjuggler/utils/__init__.py rename to src/numjuggler/utils/__init__.py diff --git a/numjuggler/utils/io.py b/src/numjuggler/utils/io.py similarity index 100% rename from numjuggler/utils/io.py rename to src/numjuggler/utils/io.py From 42fc07b52f0eb4c978944b09601934ba92553e94 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 16:15:41 +0300 Subject: [PATCH 06/54] build: add uv.lock to VCS --- .gitattributes | 24 +- .gitignore | 3 +- uv.lock | 1283 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1301 insertions(+), 9 deletions(-) create mode 100644 uv.lock 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/.gitignore b/.gitignore index 4ee4e3c..22a3349 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 @@ -32,3 +32,4 @@ travis_tests/*.res # vs code settings.json + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6ecb186 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1283 @@ +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 = "ruff" }, + { name = "ty" }, +] +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 = "pytest" }, + { name = "pytest-cov" }, +] + +[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 = "pylint" }, + { name = "pylint-per-file-ignores" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.11" }, + { name = "ty", specifier = ">=0.0.31" }, +] +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 = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, +] + +[[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 = "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" }, +] From 554ff17dedc3d6ac440d89e906775ba8c85c8bcb Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 18:41:05 +0300 Subject: [PATCH 07/54] test: decrease allowed coverage level to 10% ... this is extremly low for production quality package will be increased with time --- justfile | 4 ++++ pyproject.toml | 4 ++-- uv.lock | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 6984317..7ae2fa1 100644 --- a/justfile +++ b/justfile @@ -27,6 +27,10 @@ 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: diff --git a/pyproject.toml b/pyproject.toml index c8c86ce..6b10bf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ style = [ "ty>=0.0.31", ] pylint = ["pylint", "pylint-per-file-ignores"] -test = ["pytest>=9.0.3", "pytest-cov>=7.1.0"] +test = ["pytest>=9.0.3", "pytest-cov>=7.1.0", "pytest-mock>=3.15.1"] [tool.coverage] run.branch = true @@ -86,7 +86,7 @@ report.exclude_lines = [ "raise AssertionError", "raise NotImplementedError", ] -report.fail_under = 27 +report.fail_under = 10 # TODO @dvp: should be at least 80% report.ignore_errors = true report.omit = ["**/__init__.py", "**/types.py", "*_tab.py"] report.show_missing = true diff --git a/uv.lock b/uv.lock index 6ecb186..7c9d3a5 100644 --- a/uv.lock +++ b/uv.lock @@ -695,6 +695,7 @@ dev = [ { name = "pylint-per-file-ignores" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "pytest-mock" }, { name = "ruff" }, { name = "ty" }, ] @@ -720,6 +721,7 @@ style = [ test = [ { name = "pytest" }, { name = "pytest-cov" }, + { name = "pytest-mock" }, ] [package.metadata] @@ -742,6 +744,7 @@ dev = [ { 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" }, ] @@ -767,6 +770,7 @@ style = [ test = [ { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, ] [[package]] @@ -991,6 +995,18 @@ 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" From a2e27b516677862cf681a6a398b5d4e0888d5037 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 19:44:51 +0300 Subject: [PATCH 08/54] build: ignore docs/site --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 22a3349..dec2dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ htmlcov/ travis_tests/*.diff travis_tests/*.res +# docs +docs/site + # vs code settings.json From b2984c967d985aa4262caea0cf7923bd98f7b02f Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 20:19:10 +0300 Subject: [PATCH 09/54] docs: fix ignoring folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dec2dc9..307c581 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ travis_tests/*.diff travis_tests/*.res # docs -docs/site +site # vs code settings.json From 6a2c7abca6e6c06a472fd2df6e028e8e3c9b9ab4 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 20:42:53 +0300 Subject: [PATCH 10/54] style: ruff auto fix --- ruff.toml | 2 +- src/numjuggler/__init__.py | 1 + src/numjuggler/__main__.py | 2 + src/numjuggler/fmesh.py | 4 +- src/numjuggler/likefunc.py | 53 +++--- src/numjuggler/main.py | 142 +++++++-------- src/numjuggler/mapparsers.py | 5 +- src/numjuggler/nogq.py | 14 +- src/numjuggler/nogq2.py | 79 ++++----- src/numjuggler/numbering.py | 57 +++--- src/numjuggler/parser.py | 214 ++++++++++------------- src/numjuggler/ri_notation.py | 7 +- src/numjuggler/shortener.py | 21 +-- src/numjuggler/splitter.py | 2 + src/numjuggler/string_cells.py | 57 +++--- src/numjuggler/utils/PartialFormatter.py | 15 +- src/numjuggler/utils/__init__.py | 2 + src/numjuggler/utils/io.py | 12 +- 18 files changed, 323 insertions(+), 366 deletions(-) diff --git a/ruff.toml b/ruff.toml index 5cd43d0..8569f1c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -42,7 +42,7 @@ ignore = [ "D203", "D211", "D213", - "FRA", + # "FRA", "Q" ] diff --git a/src/numjuggler/__init__.py b/src/numjuggler/__init__.py index 92c63ef..54d5e9f 100644 --- a/src/numjuggler/__init__.py +++ b/src/numjuggler/__init__.py @@ -4,6 +4,7 @@ Package provides script mcnp.juggler. See its help for description. """ +from __future__ import annotations try: from .version import version diff --git a/src/numjuggler/__main__.py b/src/numjuggler/__main__.py index 5d6a810..ace94d4 100644 --- a/src/numjuggler/__main__.py +++ b/src/numjuggler/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .main import main main() diff --git a/src/numjuggler/fmesh.py b/src/numjuggler/fmesh.py index ce1e288..42142dd 100644 --- a/src/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/src/numjuggler/likefunc.py b/src/numjuggler/likefunc.py index 8f6bc15..f9e43dc 100644 --- a/src/numjuggler/likefunc.py +++ b/src/numjuggler/likefunc.py @@ -4,8 +4,8 @@ New implementation of mapping, where one can specify different functions for different ranges and separate values. """ +from __future__ import annotations -from __future__ import print_function from collections import OrderedDict from numjuggler.utils.io import resolve_fname_or_stream @@ -25,18 +25,18 @@ def const_func(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 +67,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,7 +88,7 @@ def __str__(self): res.extend(self._str()) - res.append('other -> {}'.format(self.default._mydoc)) + res.append(f'other -> {self.default._mydoc}') return '\n'.join(res) def write_log_as_map(self, t, fname_or_stream=None): @@ -97,7 +96,7 @@ def write_log_as_map(self, t, fname_or_stream=None): 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): @@ -108,12 +107,11 @@ class LikeFunction(LikeFunctionBase): 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()): @@ -124,7 +122,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 @@ -135,7 +133,7 @@ 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 +145,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 +170,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,11 +182,11 @@ 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. """ @@ -203,19 +198,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)) @@ -247,7 +239,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: @@ -300,15 +292,14 @@ def _get_map_ranges(s): for t in tl: 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): diff --git a/src/numjuggler/main.py b/src/numjuggler/main.py index fd5a8b2..8a38e7d 100644 --- a/src/numjuggler/main.py +++ b/src/numjuggler/main.py @@ -1,22 +1,26 @@ #!/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 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 @@ -110,17 +114,17 @@ def processing(args, cards, debuglog): 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) + 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) @@ -146,11 +150,11 @@ def processing(args, cards, debuglog): 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)) + flog.write(f' {cname:>9s} : unbalanced\n') elif ( args.opt == 'nochg' and cardstr.geom.removedp[0] == 0) : - flog.write(' {:>9s} : nochg\n'.format(cname)) + flog.write(f' {cname:>9s} : nochg\n') else: - flog.write(' {:>9s} : {:>5}\n'.format(cname,cardstr.geom.removedp[0])) + 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) @@ -224,11 +228,11 @@ def processing(args, cards, debuglog): # SD card. Requires tally number and number of cells 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) + print(file=outstr) elif args.mode == 'addgeom': # add stuff to geometry definition of cells. # Get info from the --map file: @@ -294,7 +298,7 @@ def processing(args, cards, debuglog): if mb: for c in mb: print(c.card(), end='', file=outstr) - print('', file=outstr) + print(file=outstr) # Title: if args.t == "0": # default one. Use title of the main input @@ -308,14 +312,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 # 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]: + if blk2.get(t): # First check if blk2 actually contains any cards: flg = False for c in blk2[t]: @@ -329,7 +333,7 @@ def processing(args, cards, debuglog): 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) + print(file=outstr) elif args.mode == 'uexp': if args.u == "0": N = " u=0 " @@ -374,12 +378,12 @@ def cfunc(n): 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) # create file with blank line delimiter if k in (mp.CID.cell, mp.CID.surface): - fout = open(args.inp + '.{}z'.format(k), 'w') + fout = open(args.inp + f'.{k}z', 'w') print(' ', file=fout) fout.close() elif args.mode == 'matan': @@ -437,7 +441,7 @@ def cfunc(n): c.get_values() 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(): @@ -463,7 +467,7 @@ def cfunc(n): 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()): @@ -553,15 +557,12 @@ def cfunc(n): 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: + 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) - pass else: # check that cell card does not depend on one of cset: if c.get_refcells(): @@ -585,12 +586,12 @@ def cfunc(n): 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(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. @@ -611,7 +612,7 @@ def cfunc(n): for n in clst1[1:]: g = d[n].get_geom() g = ' '.join(g.splitlines()) - new_card.geom_suffix += '({}) '.format(g) + new_card.geom_suffix += f'({g}) ' # Print out the new file for c in cards: if c.ctype == mp.CID.cell: @@ -627,8 +628,7 @@ def cfunc(n): ar = ag * Pi / 180. # 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: @@ -699,7 +699,7 @@ def e1(v): 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) if c.ctype == mp.CID.title: @@ -731,7 +731,7 @@ def e1(v): cset = set(le) 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: @@ -830,13 +830,13 @@ def e1(v): 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) 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) @@ -859,8 +859,7 @@ def e1(v): a2, g, kk = nogq.get_k(p) if cflag: crd = (crd[:-1] + - '$ a^2={:12.6e} c={:12.6e}\n'.format(a2, - g + a2)) + 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 @@ -881,8 +880,7 @@ def e1(v): '\n') else: crd = '' - crd += '{} {} c/z {:15.8e} 0 {:15.8e}\n'.format( - c.name, trn + trn0, x0, R) + 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) if trd and c.ctype == mp.CID.blankline: @@ -909,7 +907,7 @@ def e1(v): 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() @@ -986,7 +984,7 @@ def e1(v): 'Line', 'all', 'unique', - '>{}'.format(Nmax)), file=outstr) + f'>{Nmax}'), file=outstr) sc = 0 # cell counter sa = 0 # all surfaces counter su = 0 # unique surface counter @@ -1013,7 +1011,7 @@ def e1(v): su += u ma = max(ma, a) mu = max(mu, u) - print('', file=outstr) + 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': @@ -1061,14 +1059,12 @@ def check(v, s): 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(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 @@ -1101,7 +1097,7 @@ def check(v, s): 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 @@ -1129,10 +1125,10 @@ def check(v, s): 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(file=outstr) print(len(l), file=outstr) else: uref = int(args.u) @@ -1268,13 +1264,13 @@ def print_planar(params, d=1e-5, u='0'): 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) + 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, '0 {} 0'.format(v), dx*dz), file=outstr) + 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, '0 0 {}'.format(v), dx*dy), file=outstr) + 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) @@ -1288,7 +1284,7 @@ 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': @@ -1338,9 +1334,9 @@ def print_spherical(s, r): 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(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) @@ -1353,15 +1349,13 @@ def print_spherical(s, r): 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]) + 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'] + n1, v1, n2, v2 = surfaces['s'] if print_sdef: print_spherical(n2, v2) if args.c != '0': @@ -1428,7 +1422,7 @@ def print_spherical(s, r): dn = getattr(args, t[0]) if dn == 'i': maps[t] = imaps[t] - maps[t].doc = 'Indexing function for {}'.format(t) + 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 != '') @@ -1436,7 +1430,7 @@ def print_spherical(s, r): # 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) @@ -1450,7 +1444,7 @@ 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)) + version=f'%(prog)s {version}') p.add_argument('inp', help='MCNP input file') p.add_argument('-c', help=help_c, type=str, @@ -1483,7 +1477,7 @@ def main(args=sys.argv[1:]): 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)', + 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, @@ -1505,11 +1499,11 @@ def main(args=sys.argv[1:]): 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)) + hlp = os.path.join(dir1, f'help/{harg.h}.rst') + 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]) diff --git a/src/numjuggler/mapparsers.py b/src/numjuggler/mapparsers.py index 5efb844..7c3272c 100644 --- a/src/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/src/numjuggler/nogq.py b/src/numjuggler/nogq.py index af9519b..89687c2 100644 --- a/src/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/src/numjuggler/nogq2.py b/src/numjuggler/nogq2.py index 8a47193..fb75cea 100644 --- a/src/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/src/numjuggler/numbering.py b/src/numjuggler/numbering.py index 2f62efd..5770dfd 100644 --- a/src/numjuggler/numbering.py +++ b/src/numjuggler/numbering.py @@ -1,13 +1,13 @@ """ Functions to renumber cells, surfaces, etc. in MCNP input file. """ -from __future__ import print_function +from __future__ import annotations -import warnings import collections +import warnings -class _Range(object): +class _Range: """ Represents a range or a point. """ @@ -19,21 +19,18 @@ 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): +class LikeFunction: """ Class of callables that take two arguments, a number (integer) and a type (char or string): @@ -75,28 +72,24 @@ def __init__(self, pdict, log=False): self.__lf = log # flag to log or not. self.__ld = {} # here log is written, if log. - return @staticmethod def __applyD(f, n): if isinstance(f, collections.Callable): return f(n) - else: - return f + return f @staticmethod def __applyL(f, n): if isinstance(f, collections.Callable): return f(n) - else: - return n + int(f) + return n + int(f) def __get_mapping(self, t): 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): dn0, param = self.__get_mapping(t) @@ -105,7 +98,7 @@ def __call__(self, n, t): 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,15 +116,14 @@ 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('Non-injective mapping. ' + '({}, {}) and ({}, {}) ' + f'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(f'WARNING: material {n} replaced with {nnew}.') print('Add cell density to the resulting input file.') return nnew @@ -151,7 +143,7 @@ def write_log_as_map(self, fname): for n in sorted(d[t].keys()): nnew = d[t][n] if nnew != n: - print('{} {:>6d}: {:>6d}'.format(t, nnew, n), file=f) + print(f'{t} {nnew:>6d}: {n:>6d}', file=f) def get_numbers(scards): @@ -237,7 +229,7 @@ def read_map_file(fname): 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: @@ -289,7 +281,7 @@ def _parse_map_line(l): 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() @@ -312,15 +304,14 @@ def _get_map_ranges(s): for t in tl: 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 v1 is not None: + yield v1, v1 + v1 = int(t) if __name__ == '__main__': diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index 832e55b..e6c7ccd 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -1,17 +1,19 @@ -# -*- coding: utf-8 -*- """ Functions for parsing MCNP input files. """ +from __future__ import annotations -from __future__ import print_function - +import os import re import warnings + +from io import StringIO + import six -import os + from chardet import UniversalDetector -from io import StringIO + from numjuggler.utils import PartialFormatter try: @@ -47,7 +49,7 @@ # 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 +59,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: @@ -93,15 +95,14 @@ def get_name(cls, cid): for k, v in list(cls.__dict__.items()): if '__' not in k and v == cid: return k - else: - print('No attribute with name', cid) - raise ValueError() + print('No attribute with name', cid) + raise ValueError CID = __CIDClass() -class Card(object): +class Card: """ Representation of a card. """ @@ -164,7 +165,6 @@ def __init__(self, lines, ctype, pos, debug=None): # 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): """ @@ -205,9 +205,7 @@ def geom_suffix(self, value): 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) + 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: @@ -231,7 +229,7 @@ def get_input(self, check_bad_chars=False): self.print_debug('get_input: bad char in input cards', '') else: - raise ValueError('Bad character in input file. ' + + raise ValueError('Bad character in input file. ' 'Run with --debug option.') if self.ctype in (CID.comment, CID.blankline): @@ -280,7 +278,6 @@ def get_input(self, check_bad_chars=False): # TODO: dtype and name of the card can be defined already here. self.print_debug('get_input', 'ti') - return def _protect_nums(self): """ @@ -333,7 +330,6 @@ def _protect_nums(self): self.hidden = d self.print_debug('_protect_nums', 'ih') - return def get_values(self): """ @@ -366,7 +362,6 @@ def get_values(self): self.values = vt self.print_debug('get_values', 'iv') - return def get_refcells(self): """ @@ -376,13 +371,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): """ @@ -405,15 +399,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,18 +417,17 @@ 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): """ @@ -447,15 +439,14 @@ def get_d(self): 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]) - 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): """ @@ -476,19 +467,18 @@ 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 + self.__f = None + return self.__f def get_imp(self, vals={}): """ @@ -499,34 +489,32 @@ def get_imp(self, vals={}): 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 + 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('~') + 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): """ @@ -578,7 +566,6 @@ def remove_fill(self): break self.print_debug('remove_fill', 'iv') - return def card(self, wrap=False, comment=True): """ @@ -635,7 +622,7 @@ def card(self, wrap=False, comment=True): self.print_debug('Cannot wrap line ' + repr(i), '') warnings.warn('Cannot wrap card' - ' on line {}'.format(self.pos)) + f' on line {self.pos}') break else: # input i fits to one line. Do nothing. @@ -677,7 +664,6 @@ def remove_spaces(self): self.print_debug(i, '') self.input = inpt self.print_debug('after remove_spaces', 'i') - return def apply_map(self, f): """ @@ -702,7 +688,6 @@ def apply_map(self, f): newvals.append((newval, t[1])) self.values = newvals self.print_debug('after apply_map', 'vi') - return # def _parse_geom(geom): @@ -820,8 +805,7 @@ def _split_cell(input_, self): 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: @@ -949,7 +933,7 @@ def _split_cell(input_, self): # 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(): + if s.lower() == 'fill' and 'lat' in ''.join(parm).lower(): print('WARNING: fill keyword followed by an array', end=' ') print('cannot be parsed') @@ -1016,7 +1000,7 @@ def _get_int(s): for c in s: if r and c.isalpha(): break - elif c.isdigit(): + if c.isdigit(): r += c return r @@ -1110,7 +1094,7 @@ def _split_data(input_): 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' ns = _get_int(t[0]) # tally number @@ -1134,10 +1118,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 @@ -1166,7 +1147,7 @@ def get_cards(inp, debug=None, preservetabs=False): """ from os import stat iname = inp - dname = '.{}.~'.format(os.path.basename(inp)) + dname = f'.{os.path.basename(inp)}.~' try: it = stat(iname).st_mtime except OSError as e: @@ -1180,7 +1161,7 @@ def get_cards(inp, debug=None, preservetabs=False): if it < dt and debug is None: # print('Reading from dump') # dump is youger - dfile = open(dname, 'r') + dfile = open(dname) cl = cPickle.load(dfile) for c in cl: yield c @@ -1211,7 +1192,7 @@ def index_(line, chars='$&'): """ 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 @@ -1262,13 +1243,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,7 +1265,7 @@ 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): @@ -1297,7 +1277,7 @@ def replace_tab(l, cln, preserve=False, ts=8): 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: @@ -1470,7 +1450,7 @@ def are_close_lists(x, y, re=1e-6, pci=[]): for xx, yy in zip(xl, yl): r = are_close_vals(xx, yy, re) if not r: - m = 'diff at {}'.format(n) + m = f'diff at {n}' break else: m = 'all elements are close or equal' diff --git a/src/numjuggler/ri_notation.py b/src/numjuggler/ri_notation.py index 586734c..2304a5c 100644 --- a/src/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/src/numjuggler/shortener.py b/src/numjuggler/shortener.py index 94669cc..b6c493d 100644 --- a/src/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/src/numjuggler/splitter.py b/src/numjuggler/splitter.py index 2fca700..5fbf4d9 100644 --- a/src/numjuggler/splitter.py +++ b/src/numjuggler/splitter.py @@ -14,6 +14,8 @@ """ # List of cell parameter keywords +from __future__ import annotations + LoCL = [ "imp", "vol", diff --git a/src/numjuggler/string_cells.py b/src/numjuggler/string_cells.py index 26bf0ab..2d0ca48 100644 --- a/src/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/PartialFormatter.py b/src/numjuggler/utils/PartialFormatter.py index 5f977de..3a293b1 100644 --- a/src/numjuggler/utils/PartialFormatter.py +++ b/src/numjuggler/utils/PartialFormatter.py @@ -1,20 +1,20 @@ -from __future__ import print_function, absolute_import, division +from __future__ import annotations -import six import string +import six + def make_label(item): - if 0 < len(item): + if len(item) > 0: return '{' + item + '}' - else: - return item + return item class SafeDict(dict): def __getitem__(self, item): - return super(SafeDict, self).__getitem__(item) or '' + return super().__getitem__(item) or '' def __missing__(self, key): return make_label(key) @@ -98,5 +98,4 @@ def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto if six.PY2: return result - else: - return result, auto_arg_index + return result, auto_arg_index diff --git a/src/numjuggler/utils/__init__.py b/src/numjuggler/utils/__init__.py index 1fdb023..6b7dafb 100644 --- a/src/numjuggler/utils/__init__.py +++ b/src/numjuggler/utils/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .PartialFormatter import PartialFormatter __all__ = ["PartialFormatter"] diff --git a/src/numjuggler/utils/io.py b/src/numjuggler/utils/io.py index d98fd73..a2146e1 100644 --- a/src/numjuggler/utils/io.py +++ b/src/numjuggler/utils/io.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys @@ -24,12 +26,12 @@ def resolve_fname_or_stream(fname_or_stream, mode="r"): else: yield sys.stdout elif ( - is_input - and hasattr(fname_or_stream, "read") - or not is_input - and hasattr(fname_or_stream, "write") + (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: + with Path(fname_or_stream).open(mode=mode) as fid: yield fid From 7ac7d11688cafa0bb1dd27085b4a10c8bf1736ca Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 21:35:51 +0300 Subject: [PATCH 11/54] style: fix some ruff warnings --- ruff.toml | 32 +- src/numjuggler/parser.py | 620 ++++++++++++++++++--------------------- 2 files changed, 298 insertions(+), 354 deletions(-) diff --git a/ruff.toml b/ruff.toml index 8569f1c..80d00b4 100644 --- a/ruff.toml +++ b/ruff.toml @@ -37,27 +37,30 @@ exclude = [ select = ["ALL"] ignore = [ "ANN", + "ARG", + "C901", "COM812", "D", "D203", "D211", "D213", + "EM", + "E741", + "ERA", + "FBT", + "FIX", # "FRA", - "Q" + "Q", + "PLR", + "PLW", + "T201", + "TD", + "TRY", ] [lint.per-file-ignores] "benchmarks/*" = ["S101"] -"tests/*" = [ - "ANN", - "D100", - "D101", - "D102", - "D103", - "D104", - "PLR2004", - "S101", -] +"tests/*" = ["ANN", "D100", "D101", "D102", "D103", "D104", "PLR2004", "S101"] "tools/*" = ["T201", "INP001", "S603", "S607"] [lint.mccabe] @@ -65,7 +68,7 @@ ignore = [ max-complexity = 15 [lint.flake8-annotations] -mypy-init-return = true # skip return type for __init__() methods +mypy-init-return = true # skip return type for __init__() methods [lint.flake8-pytest-style] parametrize-names-type = "csv" @@ -74,10 +77,7 @@ parametrize-names-type = "csv" strict = true [lint.pep8-naming] -ignore-names = [ - "*eV*", - "*He*", -] +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. diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index e6c7ccd..770b477 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -1,10 +1,11 @@ - """ Functions for parsing MCNP input files. """ + from __future__ import annotations import os +from pathlib import Path import re import warnings @@ -26,30 +27,33 @@ 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 '{' + f':<{len(s)}' + '}' + return "{" + f":<{len(s)}" + "}" fmt_d = fmt_gen @@ -77,6 +81,7 @@ class __CIDClass: This class is to describe the 1-st level of card types. """ + # no-information cards comment = -1 blankline = -2 @@ -93,9 +98,9 @@ def get_name(cls, cid): 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 - print('No attribute with name', cid) + print("No attribute with name", cid) raise ValueError @@ -106,6 +111,7 @@ class Card: """ Representation of a card. """ + def __init__(self, lines, ctype, pos, debug=None): # Original lines, as read from the input file @@ -132,7 +138,7 @@ def __init__(self, lines, ctype, pos, debug=None): # 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 = "" # List of strings represenging meaningful parts of the card. The # original multi-line string card is obtained as @@ -156,11 +162,11 @@ def __init__(self, lines, ctype, pos, debug=None): 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. @@ -170,82 +176,79 @@ 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(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) + 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=False): """ 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: @@ -255,7 +258,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: @@ -273,11 +276,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') + self.print_debug("get_input", "ti") def _protect_nums(self): """ @@ -285,51 +288,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: + 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') + self.print_debug("_protect_nums", "ih") def get_values(self): """ @@ -348,7 +353,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 @@ -361,7 +366,7 @@ def get_values(self): self.input = inpt self.values = vt - self.print_debug('get_values', 'iv') + self.print_debug("get_values", "iv") def get_refcells(self): """ @@ -373,7 +378,7 @@ def get_refcells(self): return self.__cr s = set() for v, t in self.values: - if t == 'cel': + if t == "cel": s.add(v) self.__cr = s return self.__cr @@ -384,9 +389,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 @@ -401,7 +406,7 @@ def get_u(self): return self.__u # get it only once: for v, t in self.values: - if t == 'u': + if t == "u": self.__u = v break else: @@ -417,11 +422,11 @@ def get_m(self): if self.__m != -1: return self.__m - if 'like' in ''.join(self.input).lower(): + 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': + if t == "mat": self.__m = v break else: @@ -433,7 +438,7 @@ def get_d(self): """ For cell card return density """ - if self.__d != '': + if self.__d != "": return self.__d if self.get_m() == 0: @@ -441,11 +446,11 @@ def get_d(self): return self.__d if self.get_m() == -2: # this is like-but cell - self.__d = -100. + 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]) + self.__d = float(self.hidden["~"][0]) return self.__d def set_d(self, v): @@ -455,7 +460,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): @@ -470,7 +475,7 @@ def get_f(self, newv=None): # get it only once: for i in range(len(self.values)): v, t = self.values[i] - if t == 'fill': + if t == "fill": if newv is not None: v = newv self.values[i] = (v, t) @@ -480,31 +485,32 @@ def get_f(self, newv=None): self.__f = None return self.__f - def get_imp(self, vals={}): + def get_imp(self, vals=None): """ 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 res = {} - inpt = ' '.join(self.input).lower() - for p in 'npe': - key = 'imp:' + p + 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('~') - 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]) + n = s[0].count("~") + res[key] = float(self.hidden["~"][n]) + # change value only if necessary + if p in vals and res[key] != vals[p]: + res[key] = vals[p] + self.hidden["~"][n] = str(vals[p]) # for s in self.hidden.get('~', []): # sl = s.lower() @@ -512,7 +518,7 @@ def get_imp(self, vals={}): # val = float(sl.replace(key, '').replace('=', '')) # res[key] = val if not res: - res['imp:n'] = 1 + res["imp:n"] = 1 self.__i = res return self.__i @@ -540,32 +546,32 @@ 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') + self.print_debug("remove_fill", "iv") def card(self, wrap=False, comment=True): """ @@ -573,7 +579,7 @@ def card(self, wrap=False, comment=True): """ 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: @@ -581,56 +587,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' - f' on line {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 @@ -645,49 +648,44 @@ 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') + 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') + self.print_debug("after apply_map", "vi") # def _parse_geom(geom): @@ -725,7 +723,7 @@ def apply_map(self, f): # fmts.append('{}') -def _split_cell(input_, self): +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. @@ -736,37 +734,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. @@ -776,23 +773,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 @@ -802,17 +799,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 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) @@ -824,19 +822,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:] @@ -850,92 +845,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 s.lower() == 'fill' 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 @@ -943,26 +940,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: @@ -970,14 +967,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(): @@ -992,11 +989,11 @@ 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): - r = '' + r = "" for c in s: if r and c.isalpha(): break @@ -1009,74 +1006,69 @@ def _parse_tr(input_): """ input_ should be already passed through _split_data() """ - 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_) + 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:] @@ -1087,19 +1079,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 t[0][:5].lower() == 'fmesh' 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 @@ -1107,7 +1099,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): @@ -1118,7 +1110,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() or l.lower() == 'c': + if "c " in l[0:6].lstrip().lower() or l.lower() == "c": res = True # print 'is_com "c"', # print 'is_com', res @@ -1129,76 +1121,32 @@ 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 = f'.{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) - 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, 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 - """ - iname = inp - for c in get_cards_from_input(inp, debug=debug, preservetabs=preservetabs): - yield c + If dump exists and it is newwer than the input file, read the dump file + """ + yield from get_cards_from_input(inp, debug=debug, preservetabs=preservetabs) -def index_(line, chars='$&'): +def index_(line, chars="$&"): """ Find the first index of one of the chars in line. """ - r = re.compile(f'[{chars}]') + r = re.compile(f"[{chars}]") m = r.search(line) - if m: - i = m.end() - 1 - else: - i = len(line) - 1 - return i + return m.end() - 1 if m else len(line) - 1 def load_decode_buffer(filename): @@ -1209,20 +1157,18 @@ 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 + with Path(filename).open(mode="rb") as finp: + return StringIO(finp.read().decode(inpencoding, errors="backslashreplace")) def get_cards_from_input(inp, debug=None, preservetabs=False): @@ -1243,11 +1189,11 @@ def replace_tab(l, cln, preserve=False, ts=8): """ if preserve: return l[:] - while '\t' in l: - i = l.index('\t') + 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:] + l = l[:i] + " " * ii + l[i + 1 :] return l[:] # load input deck file inside a string buffer @@ -1265,19 +1211,19 @@ def replace_tab(l, cln, preserve=False, ts=8): cln += 1 # kw = l.lower().split()[0] kw = l.lstrip() - if kw[:8].lower() == 'message:': + 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 kw[:8].lower() == 'continue': + elif kw[:8].lower() == "continue": # input file for continue job. Contains only data block. ncid = CID.data else: @@ -1328,13 +1274,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 @@ -1351,7 +1297,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: @@ -1365,7 +1311,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 @@ -1382,27 +1328,29 @@ def get_blocks(cards): return d -def are_close_vals(x, y, re=1e-6, ra=0.): +def are_close_vals(x, y, re=1e-6, ra=0.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 + 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 + r = abs((x - y) / y) <= re return r -def are_close_lists(x, y, re=1e-6, pci=[]): +def are_close_lists(x, y, re=1e-6, pci=None): """ Return True if x and y are close but not equal. """ + if pci is None: + pci = [] if len(x) != len(y): res = False - msg = 'Different length' + msg = "Different length" if x == y: return True @@ -1419,13 +1367,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=False): xe += x[i:i1] ye += y[i:i1] xp += x[i1:i2] @@ -1436,24 +1384,24 @@ def are_close_lists(x, y, re=1e-6, pci=[]): 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] + yp = [e * xpn / ypn for e in yp] msg = [] res = [] - for xl, yl in zip([xe, xp], [ye, 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') + msg.append("exact match") else: n = 0 - for xx, yy in zip(xl, yl): + for xx, yy in zip(xl, yl, strict=False): r = are_close_vals(xx, yy, re) if not r: - m = f'diff at {n}' + m = f"diff at {n}" break else: - m = 'all elements are close or equal' + m = "all elements are close or equal" r = True res.append(r) msg.append(m) @@ -1465,7 +1413,3 @@ def are_close_lists(x, y, re=1e-6, pci=[]): else: result = True return result - - -if __name__ == '__main__': - pass From 65c7d32428f03342510ab300a4649228835ba49c Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 19 Apr 2026 22:07:49 +0300 Subject: [PATCH 12/54] style: ignore present ruff errors --- ruff.toml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/ruff.toml b/ruff.toml index 80d00b4..f957754 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py312" +target-version = "py313" line-length = 100 src = ["src", "tests"] exclude = [ @@ -35,33 +35,50 @@ exclude = [ [lint] select = ["ALL"] -ignore = [ +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", - # "FRA", + "FURB", + "I", + "ICN", "Q", + "N", + "PERF", + "PLC", "PLR", "PLW", + "PT", + "PTH", + "RET", + "RUF", + "S", + "SIM", + "SLF", "T201", "TD", "TRY", + "UP", ] [lint.per-file-ignores] -"benchmarks/*" = ["S101"] "tests/*" = ["ANN", "D100", "D101", "D102", "D103", "D104", "PLR2004", "S101"] -"tools/*" = ["T201", "INP001", "S603", "S607"] [lint.mccabe] # Unlike Flake8, default to a complexity level of 10. From 8b10ac46df7acb21ff9949646399e414281f5ec1 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 00:06:18 +0300 Subject: [PATCH 13/54] test: add test for multiline transformation specification --- tests/test_likefunc.py | 4 ++-- tests/test_main.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/test_likefunc.py b/tests/test_likefunc.py index a5a0a66..670a17c 100644 --- a/tests/test_likefunc.py +++ b/tests/test_likefunc.py @@ -36,8 +36,8 @@ def test_LikeFunction( 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 5066d0b..10967e0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,10 +1,10 @@ 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 HERE = Path(__file__).parent @@ -38,15 +38,21 @@ def load_line_heading_numbers(lines): "-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_test_main(tmpdir, capsys, inp, command, expected): +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() + 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" From 8549bf3ada06cd96a572a878aac036c0edd51e52 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 00:17:54 +0300 Subject: [PATCH 14/54] test: add conftest.py and fix justfile --- .gitignore | 1 + justfile | 6 +-- tests/conftest.py | 10 ++++ .../data/simple_cubes_with_multiline_tr.mcnp | 50 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/data/simple_cubes_with_multiline_tr.mcnp diff --git a/.gitignore b/.gitignore index 307c581..0765511 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ src/numjuggler/version.py # Environments .cache/ .idea/ +.vscode/ # Dev.scripts results numjuggler.dot diff --git a/justfile b/justfile index 7ae2fa1..82e4927 100644 --- a/justfile +++ b/justfile @@ -170,7 +170,7 @@ export JUST_LOG := log # Run pre-commit on all files [group: 'style'] @pre-commit: - uv run --no-dev --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files + uv run --no-dev --group style pre-commit run --show-diff-on-failure --color=always --all-files # Run mypy # [group: 'lint'] @@ -179,7 +179,7 @@ export JUST_LOG := log [group: 'style'] @pylint: - uv run --no-dev --group lint pylint --recursive=y --output-format colorized src tests + uv run --no-dev --group style pylint --recursive=y --output-format colorized src tests # [group: 'lint'] # @pyright: @@ -188,7 +188,7 @@ export JUST_LOG := log # Lint with ty [group: 'style'] @ty: - ty check + uv run --no-dev --group style ty check # # Check rst-texts # [group: 'docs'] 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/simple_cubes_with_multiline_tr.mcnp b/tests/data/simple_cubes_with_multiline_tr.mcnp new file mode 100644 index 0000000..9d19c2a --- /dev/null +++ b/tests/data/simple_cubes_with_multiline_tr.mcnp @@ -0,0 +1,50 @@ +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 From 4c7939b0162408fa1d00ad3af495d471e0249e64 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 01:08:12 +0300 Subject: [PATCH 15/54] ci: adopt GHA test --- .github/workflows/python-app.yml | 61 +++++++++++++++++++------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 2694bdb..d1b63b1 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -4,37 +4,50 @@ name: Python application on: + workflow_dispatch: + push: - branches: [ "master" ] + branches: ["master", "devel", "test/deps"] pull_request: - branches: [ "master" ] + branches: ["master", "devel"] 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.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 --group test pytest --cov --cov-report term-missing:skip-covered --cov-report xml From 605fb3d0450d9c2c983ea0b5c5eca9bf7fcffef4 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 01:13:12 +0300 Subject: [PATCH 16/54] ci: add Python 3.11 to GHA tests --- .github/workflows/python-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d1b63b1..a37c2c5 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] include: - os: windows-latest python-version: "3.13" # Windows runner is too slow @@ -50,4 +50,4 @@ jobs: - name: ✅ Run tests run: | - uv run --group test pytest --cov --cov-report term-missing:skip-covered --cov-report xml + uv run --no-dev --group test pytest --cov --cov-report term-missing:skip-covered --cov-report xml From 687fdefbcfc812f89e6ff33557b6b7b392d7bf73 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 01:14:14 +0300 Subject: [PATCH 17/54] ci: remove fork branch from GHA test triggers --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a37c2c5..a13cba2 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: push: - branches: ["master", "devel", "test/deps"] + branches: ["master", "devel"] pull_request: branches: ["master", "devel"] From bf00fd19b023cc4ee94119ed058da5855156b687 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 01:15:23 +0300 Subject: [PATCH 18/54] ci: add concurrency guard --- .github/workflows/python-app.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a13cba2..276af40 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -11,6 +11,10 @@ on: pull_request: 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 From 0f5836b8a026de7d93ce512db7e9a4875ba15692 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 09:20:09 +0300 Subject: [PATCH 19/54] docs: fix documentaion building and autoserve use commmands: just docs - for auto serving the docs page just docs-build - to build the docs --- justfile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/justfile b/justfile index 82e4927..7a546d8 100644 --- a/justfile +++ b/justfile @@ -195,12 +195,12 @@ export JUST_LOG := log # @rstcheck: # uv run --no-dev --group docs rstcheck --recursive *.rst docs # -# # build documentation -# [group: 'docs'] -# @docs-build: rstcheck -# uv run --no-dev --group docs sphinx-build docs/source docs/_build -# -# # browse and edit documentation with auto build -# [group: 'docs'] -# @docs: -# uv run --no-dev --group docs --group docs sphinx-autobuild --open-browser docs/source docs/_build +# 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 From fe354f099785d130abfdf7cf2da2da26ad88933a Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 09:57:26 +0300 Subject: [PATCH 20/54] build: add 'just' installation script --- tools/install/install-just | 12 ++++++++++++ tools/install/install-rustup | 7 +++++++ 2 files changed, 19 insertions(+) create mode 100755 tools/install/install-just create mode 100755 tools/install/install-rustup 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 + From 2271d8db07e4b0820f65cc0e5f4292e5d0ff2bfd Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 10:01:06 +0300 Subject: [PATCH 21/54] build: add uv installation script --- tools/install/install-uv | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 tools/install/install-uv 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 : From f7a06c31c5ef1103a397d890f38ff823d977cf54 Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 15:16:12 +0300 Subject: [PATCH 22/54] build: remove unused files --- .pylintrc | 407 ------------------------------------------------------ 1 file changed, 407 deletions(-) delete mode 100644 .pylintrc 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 From 30c1bddc135a5ed7c4fb92314e457567a62d3aaa Mon Sep 17 00:00:00 2001 From: dvp Date: Mon, 20 Apr 2026 15:17:40 +0300 Subject: [PATCH 23/54] build: remove unused files --- pytest_coverage | 4 ---- run_coverage | 6 ------ 2 files changed, 10 deletions(-) delete mode 100755 pytest_coverage delete mode 100755 run_coverage 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/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 - From 62f5d02f30a7133fa0141ab43b314eb5ef9e6901 Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 18:51:12 +0300 Subject: [PATCH 24/54] test: reimplemented travis tests in pytest module --- install.sh | 16 ++-- pyproject.toml | 7 +- src/numjuggler/readme.rst | 4 - src/numjuggler/trial_map.txt | 6 -- .../data/travis_tests}/cdens/inp.i | 0 .../data/travis_tests}/cdens/inp.map1.ref | 0 .../data/travis_tests}/cdens/inp.map2.ref | 0 .../data/travis_tests}/cdens/inp.map3.ref | 0 .../data/travis_tests}/cdens/inp.map4.ref | 0 .../data/travis_tests}/cdens/inp.map5.ref | 0 .../data/travis_tests}/cdens/inp.map6.ref | 0 .../data/travis_tests}/cdens/map1 | 0 .../data/travis_tests}/cdens/map2 | 0 .../data/travis_tests}/cdens/map3 | 0 .../data/travis_tests}/cdens/map4 | 0 .../data/travis_tests}/cdens/map5 | 0 .../data/travis_tests}/cdens/map6 | 0 .../data/travis_tests}/merge/inp1.inp | 0 .../data/travis_tests}/merge/inp2.inp | 0 .../data/travis_tests}/merge/merged.inp.ref | 0 .../travis_tests}/remh/nested_complement.i | 0 .../travis_tests}/remh/nested_complement.ref | 0 .../data/travis_tests}/renum/i1.i | 0 .../data/travis_tests}/renum/i1.ref | 0 .../data/travis_tests}/renum/i2.i | 0 .../data/travis_tests}/renum/i2.ref | 0 .../data/travis_tests}/renum/i3.i | 0 .../data/travis_tests}/renum/i3.ref | 0 tests/test_travis.py | 81 +++++++++++++++++++ .../numjuggler.clusters | 0 show_graph.sh => tools/show_graph.sh | 0 travis_run_all.sh | 40 --------- 32 files changed, 90 insertions(+), 64 deletions(-) delete mode 100644 src/numjuggler/readme.rst delete mode 100644 src/numjuggler/trial_map.txt rename {travis_tests => tests/data/travis_tests}/cdens/inp.i (100%) rename {travis_tests => tests/data/travis_tests}/cdens/inp.map1.ref (100%) rename {travis_tests => tests/data/travis_tests}/cdens/inp.map2.ref (100%) rename {travis_tests => tests/data/travis_tests}/cdens/inp.map3.ref (100%) rename {travis_tests => tests/data/travis_tests}/cdens/inp.map4.ref (100%) rename {travis_tests => tests/data/travis_tests}/cdens/inp.map5.ref (100%) rename {travis_tests => tests/data/travis_tests}/cdens/inp.map6.ref (100%) rename {travis_tests => tests/data/travis_tests}/cdens/map1 (100%) rename {travis_tests => tests/data/travis_tests}/cdens/map2 (100%) rename {travis_tests => tests/data/travis_tests}/cdens/map3 (100%) rename {travis_tests => tests/data/travis_tests}/cdens/map4 (100%) rename {travis_tests => tests/data/travis_tests}/cdens/map5 (100%) rename {travis_tests => tests/data/travis_tests}/cdens/map6 (100%) rename {travis_tests => tests/data/travis_tests}/merge/inp1.inp (100%) rename {travis_tests => tests/data/travis_tests}/merge/inp2.inp (100%) rename {travis_tests => tests/data/travis_tests}/merge/merged.inp.ref (100%) rename {travis_tests => tests/data/travis_tests}/remh/nested_complement.i (100%) rename {travis_tests => tests/data/travis_tests}/remh/nested_complement.ref (100%) rename {travis_tests => tests/data/travis_tests}/renum/i1.i (100%) rename {travis_tests => tests/data/travis_tests}/renum/i1.ref (100%) rename {travis_tests => tests/data/travis_tests}/renum/i2.i (100%) rename {travis_tests => tests/data/travis_tests}/renum/i2.ref (100%) rename {travis_tests => tests/data/travis_tests}/renum/i3.i (100%) rename {travis_tests => tests/data/travis_tests}/renum/i3.ref (100%) create mode 100644 tests/test_travis.py rename numjuggler.clusters => tools/numjuggler.clusters (100%) rename show_graph.sh => tools/show_graph.sh (100%) delete mode 100755 travis_run_all.sh 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/pyproject.toml b/pyproject.toml index 6b10bf1..9e3cd91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,9 @@ requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4'] name = "numjuggler" dynamic = ["version"] description = "MCNP input file renumbering tool" -readme = "README.rst" +readme = "README.md" keywords = ["MCNP", "ITER", "PARSER", "RENUMBER"] -license = { text = "GPLv3" } +license = "GPL-3.0-or-later" authors = [{ name = "A.Travleev", email = "anton.travleev@gmail.com" }] requires-python = ">=3.11" classifiers = [ @@ -16,7 +16,6 @@ classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: Eduation", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", @@ -86,7 +85,7 @@ report.exclude_lines = [ "raise AssertionError", "raise NotImplementedError", ] -report.fail_under = 10 # TODO @dvp: should be at least 80% +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 diff --git a/src/numjuggler/readme.rst b/src/numjuggler/readme.rst deleted file mode 100644 index 60b167b..0000000 --- a/src/numjuggler/readme.rst +++ /dev/null @@ -1,4 +0,0 @@ -Help messages are written in `main.py`_. - - -.. _`main.py`: main.py diff --git a/src/numjuggler/trial_map.txt b/src/numjuggler/trial_map.txt deleted file mode 100644 index dd01e49..0000000 --- a/src/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/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 100% rename from travis_tests/merge/inp2.inp rename to tests/data/travis_tests/merge/inp2.inp 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/test_travis.py b/tests/test_travis.py new file mode 100644 index 0000000..1cb2437 --- /dev/null +++ b/tests/test_travis.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from pathlib import Path +import shutil + +import pytest + +from numjuggler.main import main + + +HERE = Path(__file__).parent +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", + ), + ( + "remh", + "", + "nested_complement", + ), + ], +) +def test_travis(cd_tmpdir, capsys, mode, options, inp): + subdir = data / mode + source = subdir / (inp + ".i") + wrk_file = shutil.copy(source, cd_tmpdir) + expected = (subdir / (inp + ".ref")).read_text() + command = ["--mode", mode, *options.split(), wrk_file] + main(command) + out, _ = capsys.readouterr() + assert out == expected + + +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) + expected = (cdense_data / f"{inp}.{map_}.ref").read_text() + command = ["--mode", "cdens", "--map", wrk_map, wrk_file] + main(command) + out, _ = capsys.readouterr() + assert out == expected + + +merge_data = Path(data / "merge") + + +@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) + expected = (merge_data / f"{merged}.{inp}.ref").read_text() + command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] + main(command) + out, _ = capsys.readouterr() + assert out == expected diff --git a/numjuggler.clusters b/tools/numjuggler.clusters similarity index 100% rename from numjuggler.clusters rename to tools/numjuggler.clusters 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 - From ec4f904360fbeeabd7ae180205777ea61ecba1d0 Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 19:07:48 +0300 Subject: [PATCH 25/54] test: fix windows reading --- tests/test_travis.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 1cb2437..f8d583b 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -1,5 +1,7 @@ from __future__ import annotations +import platform + from pathlib import Path import shutil @@ -42,13 +44,20 @@ def test_travis(cd_tmpdir, capsys, mode, options, inp): subdir = data / mode source = subdir / (inp + ".i") wrk_file = shutil.copy(source, cd_tmpdir) - expected = (subdir / (inp + ".ref")).read_text() + expected = fix_cr(subdir / (inp + ".ref")) command = ["--mode", mode, *options.split(), wrk_file] main(command) out, _ = capsys.readouterr() assert out == expected +def fix_cr(path): + expected = path.read_text() + if platform.system() == "Windows": + expected = expected.replace("\r", "") + return expected + + cdense_data = Path(data / "cdens") @@ -58,7 +67,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) - expected = (cdense_data / f"{inp}.{map_}.ref").read_text() + expected = fix_cr(cdense_data / f"{inp}.{map_}.ref") command = ["--mode", "cdens", "--map", wrk_map, wrk_file] main(command) out, _ = capsys.readouterr() @@ -74,7 +83,7 @@ def test_merge(cd_tmpdir, capsys, inp, merged): inp1_path = merge_data / (inp + "1.inp") wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) - expected = (merge_data / f"{merged}.{inp}.ref").read_text() + expected = fix_cr(merge_data / f"{merged}.{inp}.ref") command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] main(command) out, _ = capsys.readouterr() From 4f24cf499b9986c46d3140c4d71ea497257dbe01 Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 20:32:04 +0300 Subject: [PATCH 26/54] test: explicitly use utf8 on Windows --- src/numjuggler/utils/io.py | 7 ++---- tests/test_likefunc.py | 48 +++++++++++++++++++------------------- tests/test_travis.py | 21 +++++++---------- 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/numjuggler/utils/io.py b/src/numjuggler/utils/io.py index a2146e1..e38e07b 100644 --- a/src/numjuggler/utils/io.py +++ b/src/numjuggler/utils/io.py @@ -25,11 +25,8 @@ def resolve_fname_or_stream(fname_or_stream, mode="r"): 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")) + elif (is_input and hasattr(fname_or_stream, "read")) or ( + not is_input and hasattr(fname_or_stream, "write") ): yield fname_or_stream else: diff --git a/tests/test_likefunc.py b/tests/test_likefunc.py index 670a17c..a22c799 100644 --- a/tests/test_likefunc.py +++ b/tests/test_likefunc.py @@ -4,37 +4,37 @@ 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 ): inp = StringIO(data) maps = lf.read_map_file(inp, log) diff --git a/tests/test_travis.py b/tests/test_travis.py index f8d583b..631e519 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -1,7 +1,5 @@ from __future__ import annotations -import platform - from pathlib import Path import shutil @@ -10,7 +8,7 @@ from numjuggler.main import main -HERE = Path(__file__).parent +HERE = Path(__file__).parent.absolute() data = HERE / "data/travis_tests" assert data.exists(), "Cannot access test data 'travis' files" @@ -44,20 +42,13 @@ def test_travis(cd_tmpdir, capsys, mode, options, inp): subdir = data / mode source = subdir / (inp + ".i") wrk_file = shutil.copy(source, cd_tmpdir) - expected = fix_cr(subdir / (inp + ".ref")) + expected = _load(subdir / (inp + ".ref")) command = ["--mode", mode, *options.split(), wrk_file] main(command) out, _ = capsys.readouterr() assert out == expected -def fix_cr(path): - expected = path.read_text() - if platform.system() == "Windows": - expected = expected.replace("\r", "") - return expected - - cdense_data = Path(data / "cdens") @@ -67,7 +58,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) - expected = fix_cr(cdense_data / f"{inp}.{map_}.ref") + expected = _load(cdense_data / f"{inp}.{map_}.ref") command = ["--mode", "cdens", "--map", wrk_map, wrk_file] main(command) out, _ = capsys.readouterr() @@ -83,8 +74,12 @@ def test_merge(cd_tmpdir, capsys, inp, merged): inp1_path = merge_data / (inp + "1.inp") wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) - expected = fix_cr(merge_data / f"{merged}.{inp}.ref") + expected = _load(merge_data / f"{merged}.{inp}.ref") command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] main(command) out, _ = capsys.readouterr() assert out == expected + + +def _load(path: Path): + return path.read_text(encoding="utf8") From af51a3bdece4a88ce091e9c5b7afb742a9733eb8 Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 20:39:04 +0300 Subject: [PATCH 27/54] build: fix git warnings and remove obsolete files --- .gitignore | 5 ++--- tools/numjuggler.clusters | 0 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 tools/numjuggler.clusters diff --git a/.gitignore b/.gitignore index 0765511..fffa015 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,7 @@ travis_tests/*.diff travis_tests/*.res # docs -site +site +.docs-build -# vs code -settings.json diff --git a/tools/numjuggler.clusters b/tools/numjuggler.clusters deleted file mode 100644 index e69de29..0000000 From debc12497923ac1e6293bd9716946f6478a55beb Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 21:20:23 +0300 Subject: [PATCH 28/54] test: fixin ... --- tests/test_travis.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 631e519..b2d4f5d 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -42,7 +42,7 @@ def test_travis(cd_tmpdir, capsys, mode, options, inp): subdir = data / mode source = subdir / (inp + ".i") wrk_file = shutil.copy(source, cd_tmpdir) - expected = _load(subdir / (inp + ".ref")) + expected = _load_as_binary(subdir / (inp + ".ref")) command = ["--mode", mode, *options.split(), wrk_file] main(command) out, _ = capsys.readouterr() @@ -58,7 +58,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) - expected = _load(cdense_data / f"{inp}.{map_}.ref") + expected = _load_as_binary(cdense_data / f"{inp}.{map_}.ref") command = ["--mode", "cdens", "--map", wrk_map, wrk_file] main(command) out, _ = capsys.readouterr() @@ -74,12 +74,14 @@ def test_merge(cd_tmpdir, capsys, inp, merged): inp1_path = merge_data / (inp + "1.inp") wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) - expected = _load(merge_data / f"{merged}.{inp}.ref") + expected = _load_as_binary(merge_data / f"{merged}.{inp}.ref") command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] main(command) out, _ = capsys.readouterr() assert out == expected -def _load(path: Path): - return path.read_text(encoding="utf8") +def _load_as_binary(path: Path) -> str: + """Ensure there's no characters on Windows""" + with path.open("rb") as fid: + return fid.read().decode("utf8") From 3686da160c07605da3b589fbb14ad187185a8af1 Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 22:06:28 +0300 Subject: [PATCH 29/54] test: fixin ... 2 --- tests/test_travis.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index b2d4f5d..7ae7dd8 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -1,5 +1,8 @@ from __future__ import annotations +import sys +import sysconfig + from pathlib import Path import shutil @@ -7,7 +10,8 @@ from numjuggler.main import main - +WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform() +MACOS = sys.platform.startswith("darwin") HERE = Path(__file__).parent.absolute() data = HERE / "data/travis_tests" assert data.exists(), "Cannot access test data 'travis' files" @@ -42,7 +46,7 @@ def test_travis(cd_tmpdir, capsys, mode, options, inp): subdir = data / mode source = subdir / (inp + ".i") wrk_file = shutil.copy(source, cd_tmpdir) - expected = _load_as_binary(subdir / (inp + ".ref")) + expected = load_without_cr_chars(subdir / (inp + ".ref")) command = ["--mode", mode, *options.split(), wrk_file] main(command) out, _ = capsys.readouterr() @@ -58,7 +62,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) - expected = _load_as_binary(cdense_data / f"{inp}.{map_}.ref") + expected = load_without_cr_chars(cdense_data / f"{inp}.{map_}.ref") command = ["--mode", "cdens", "--map", wrk_map, wrk_file] main(command) out, _ = capsys.readouterr() @@ -74,14 +78,17 @@ def test_merge(cd_tmpdir, capsys, inp, merged): inp1_path = merge_data / (inp + "1.inp") wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) - expected = _load_as_binary(merge_data / f"{merged}.{inp}.ref") + expected = load_without_cr_chars(merge_data / f"{merged}.{inp}.ref") command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] main(command) out, _ = capsys.readouterr() assert out == expected -def _load_as_binary(path: Path) -> str: +def load_without_cr_chars(path: Path) -> str: """Ensure there's no characters on Windows""" - with path.open("rb") as fid: - return fid.read().decode("utf8") + if WIN: + with path.open("rb") as fid: + text = fid.read().decode("utf8") + return text.replace("\r", "") + return path.read_text() From e0250cdea43bbbf11888d647a18c091d9a09a25c Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 23:25:34 +0300 Subject: [PATCH 30/54] test: fixin ... 3 --- tests/test_travis.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 7ae7dd8..d832e9b 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -1,5 +1,6 @@ from __future__ import annotations +import difflib import sys import sysconfig @@ -10,8 +11,6 @@ from numjuggler.main import main -WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform() -MACOS = sys.platform.startswith("darwin") HERE = Path(__file__).parent.absolute() data = HERE / "data/travis_tests" assert data.exists(), "Cannot access test data 'travis' files" @@ -46,11 +45,11 @@ def test_travis(cd_tmpdir, capsys, mode, options, inp): subdir = data / mode source = subdir / (inp + ".i") wrk_file = shutil.copy(source, cd_tmpdir) - expected = load_without_cr_chars(subdir / (inp + ".ref")) + ref_path = subdir / (inp + ".ref") command = ["--mode", mode, *options.split(), wrk_file] main(command) out, _ = capsys.readouterr() - assert out == expected + _assert_equal(out, ref_path) cdense_data = Path(data / "cdens") @@ -62,11 +61,11 @@ 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) - expected = load_without_cr_chars(cdense_data / f"{inp}.{map_}.ref") + ref_path = cdense_data / f"{inp}.{map_}.ref" command = ["--mode", "cdens", "--map", wrk_map, wrk_file] main(command) out, _ = capsys.readouterr() - assert out == expected + _assert_equal(out, ref_path) merge_data = Path(data / "merge") @@ -78,17 +77,17 @@ def test_merge(cd_tmpdir, capsys, inp, merged): inp1_path = merge_data / (inp + "1.inp") wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) - expected = load_without_cr_chars(merge_data / f"{merged}.{inp}.ref") + ref_path = merge_data / f"{merged}.{inp}.ref" command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] main(command) out, _ = capsys.readouterr() - assert out == expected + _assert_equal(out, ref_path) -def load_without_cr_chars(path: Path) -> str: - """Ensure there's no characters on Windows""" - if WIN: - with path.open("rb") as fid: - text = fid.read().decode("utf8") - return text.replace("\r", "") - return path.read_text() +def _assert_equal(out: str, ref_path: Path) -> None: + name = ref_path.name + with ref_path.open(encoding="utf8") as f: + for i, (o, r) in enumerate(zip(out.split("\n"), f.readlines())): + assert o.strip() == r.strip(), ( + f"{name}:{i + 1} {'\n'.join(difflib.Differ().compare([o], [r]))}" + ) From 833935b651c0f35cefd5c2cde175f03c8d5fd3ce Mon Sep 17 00:00:00 2001 From: dvp Date: Tue, 21 Apr 2026 23:45:28 +0300 Subject: [PATCH 31/54] test: fixin ... 4 --- tests/data/travis_tests/merge/inp2.inp | 1 - tests/test_travis.py | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/data/travis_tests/merge/inp2.inp b/tests/data/travis_tests/merge/inp2.inp index 3a934ec..2197933 100644 --- a/tests/data/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/tests/test_travis.py b/tests/test_travis.py index d832e9b..12c79d3 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -86,8 +86,10 @@ def test_merge(cd_tmpdir, capsys, inp, merged): def _assert_equal(out: str, ref_path: Path) -> None: name = ref_path.name + + def report(o: str, r: str) -> str: + return "\n".join(difflib.Differ().compare([o], [r])) + with ref_path.open(encoding="utf8") as f: for i, (o, r) in enumerate(zip(out.split("\n"), f.readlines())): - assert o.strip() == r.strip(), ( - f"{name}:{i + 1} {'\n'.join(difflib.Differ().compare([o], [r]))}" - ) + assert o.strip() == r.strip(), f"{name}:{i + 1} {report(o, r)}" From 6a8b69aea7a9dd66f21b8edd3d6e864d505a16e2 Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 00:04:07 +0300 Subject: [PATCH 32/54] test: fixing ... 5 --- tests/test_travis.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 12c79d3..b09eb43 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -41,14 +41,14 @@ ), ], ) -def test_travis(cd_tmpdir, capsys, mode, options, inp): +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) - ref_path = subdir / (inp + ".ref") command = ["--mode", mode, *options.split(), wrk_file] main(command) out, _ = capsys.readouterr() + ref_path = subdir / (inp + ".ref") _assert_equal(out, ref_path) @@ -61,10 +61,10 @@ 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) - ref_path = cdense_data / f"{inp}.{map_}.ref" command = ["--mode", "cdens", "--map", wrk_map, wrk_file] main(command) out, _ = capsys.readouterr() + ref_path = cdense_data / f"{inp}.{map_}.ref" _assert_equal(out, ref_path) @@ -77,10 +77,10 @@ def test_merge(cd_tmpdir, capsys, inp, merged): inp1_path = merge_data / (inp + "1.inp") wrk_inp2 = shutil.copy(inp2_path, cd_tmpdir) wrk_inp1 = shutil.copy(inp1_path, cd_tmpdir) - ref_path = merge_data / f"{merged}.{inp}.ref" command = ["--mode", "merge", "-m", wrk_inp2, wrk_inp1] main(command) out, _ = capsys.readouterr() + ref_path = merge_data / f"{merged}.{inp}.ref" _assert_equal(out, ref_path) @@ -91,5 +91,5 @@ def report(o: str, r: str) -> str: return "\n".join(difflib.Differ().compare([o], [r])) with ref_path.open(encoding="utf8") as f: - for i, (o, r) in enumerate(zip(out.split("\n"), f.readlines())): + for i, (o, r) in enumerate(zip(out.split("\n"), filter(lambda s: s, f.readlines()))): assert o.strip() == r.strip(), f"{name}:{i + 1} {report(o, r)}" From 8e0da5141d54eb8047cffa6fead44a2666f654e5 Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 00:35:14 +0300 Subject: [PATCH 33/54] test: fixing ... 6 made xfail: need low level testing of parser module --- tests/test_travis.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index b09eb43..9c88770 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -2,7 +2,6 @@ import difflib import sys -import sysconfig from pathlib import Path import shutil @@ -71,7 +70,20 @@ def test_cdens(cd_tmpdir, capsys, inp, map_): merge_data = Path(data / "merge") -@pytest.mark.parametrize("inp,merged", [("inp", "merged")]) +@pytest.mark.parametrize( + "inp,merged", + [ + pytest.param( + "inp", + "merged", + marks=pytest.mark.xfail( + sys.platform == "win32", + reason='...inp1.inp\\r"', + # numjuggler parser leaves character at the end of title before double quote + ), + ) + ], +) def test_merge(cd_tmpdir, capsys, inp, merged): inp2_path = merge_data / (inp + "2.inp") inp1_path = merge_data / (inp + "1.inp") From 715f8d5540895d2db61324c2a5b6843c7ce449a1 Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 00:41:47 +0300 Subject: [PATCH 34/54] test: fixing ... 7 made xfail: need low level testing of parser module --- tests/test_travis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 9c88770..5421547 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -33,10 +33,13 @@ "-u -5942", "i3", ), - ( + pytest.param( "remh", "", "nested_complement", + marks=pytest.mark.xfail( + sys.platform == "win32", reason="Need more efforts on parser testing" + ), ), ], ) From ba0a39ef40a9f253991eca611e4d911dff09a420 Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 01:52:02 +0300 Subject: [PATCH 35/54] style: add pyreverse & symilar --- .gitignore | 6 +- justfile | 170 ++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 3 files changed, 94 insertions(+), 84 deletions(-) diff --git a/.gitignore b/.gitignore index fffa015..a5d1971 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,9 @@ numjuggler.sfood2 .coverage htmlcov/ -# Travis tests -travis_tests/*.diff -travis_tests/*.res +# pyreverse +*_numjuggler.puml +*_numjuggler.html # docs site diff --git a/justfile b/justfile index 7a546d8..4cf6805 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ set windows-shell := ["pwsh.exe", "-NoProfile", "-NonInteractive", "-ExecutionPo alias t := test alias c := check -set dotenv-load := true +set dotenv-load default_python := "3.13" # TITLE := `uv version` @@ -25,170 +25,180 @@ log := "warn" export JUST_LOG := log @_default: - just --list + just --list -[group: 'dev'] +[group('dev')] @version: - uv run --with setuptools_scm python -m setuptools_scm + uv run --with setuptools_scm python -m setuptools_scm # create venv, if not exists -[group: 'dev'] +[group('dev')] @venv: - [ -d .venv ] || uv venv --python {{default_python}} + [ -d .venv ] || uv venv --python {{ default_python }} # build package -[group: 'dev'] +[group('dev')] @build: venv - uv build + uv build # check distribution with twine -[group: 'dev'] +[group('dev')] @check-dist: build - uvx twine check dist/* + uvx twine check dist/* # clean reproducible files -[group: 'dev'] +[group('dev')] @clean: - #!/bin/bash - dirs_to_clean=( - ".benchmarks" - ".cache" - ".eggs" - ".mypy_cache" - ".pytest_cache" - ".ruff_cache" - ".venv" - "__pycache__" - "_build" - "build" - "dist" - "docs/_build" - "htmlcov" - ) - for d in "${dirs_to_clean[@]}"; do - find . -type d -wholename "$d" -exec rm -rf {} + - done - coverage erase - + #!/bin/bash + dirs_to_clean=( + ".benchmarks" + ".cache" + ".eggs" + ".mypy_cache" + ".pytest_cache" + ".ruff_cache" + ".venv" + "__pycache__" + "_build" + "build" + "dist" + "docs/_build" + "htmlcov" + ) + for d in "${dirs_to_clean[@]}"; do + find . -type d -wholename "$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'] +[group('dev')] @install: build - uv sync + uv sync # clean build -[group: 'dev'] +[group('dev')] @reinstall: clean install - # Check style and test -[group: 'dev'] +[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 +# git commit -m "bump: version $(uv version)" pyproject.toml uv.lock # update tools -[group: 'dev'] +[group('dev')] @up-tools: - pre-commit autoupdate - uv self update - pre-commit run -a + pre-commit autoupdate + uv self update + pre-commit run -a # update dependencies -[group: 'dev'] +[group('dev')] @up: - uv sync --upgrade --all-extras - pre-commit run -a - pytest + uv sync --upgrade --all-extras + pre-commit run -a + pytest # show dependencies -[group: 'dev'] +[group('dev')] @tree *args: - uv tree --outdated {{args}} + uv tree --outdated {{ args }} # run pyupgrade -[group: 'dev'] -@pyupgrade *args="--py314-plus": # this check python version on moving to the python-3.14 - uvx pyupgrade {{args}} # presumably, code is updated by ruff, just to check occasionally +[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'] +[group('test')] @test-ff *args: - uv run --no-dev --group test pytest -x {{args}} + uv run --no-dev --group test pytest -x {{ args }} # test with clean cache -[group: 'test'] +[group('test')] @test-cache-clear *args: - uv run --no-dev --group test pytest --cache-clear {{args}} + uv run --no-dev --group test pytest --cache-clear {{ args }} # test fast -[group: 'test'] +[group('test')] @test-fast *args: - uv run --no-dev --group test pytest -m "not slow" {{args}} + uv run --no-dev --group test pytest -m "not slow" {{ args }} # run all the tests -[group: 'test'] +[group('test')] @test *args: - uv run --no-dev --group test pytest {{args}} + uv run --no-dev --group test pytest {{ args }} -# # run documentation tests +# # run documentation tests # [group: 'test'] # @xdoctest *args: # uv run --no-dev --group test python -m xdoctest --silent -c all src tools {{args}} # create coverage data -[group: 'test'] +[group('test')] @coverage: - uv run --no-dev --group test pytest --cov --cov-report=term-missing:skip-covered + uv run --no-dev --group test pytest --cov --cov-report=term-missing:skip-covered # coverage to html -[group: 'test'] +[group('test')] @coverage-html: - uv run --no-dev --group test pytest --cov --cov-report html:htmlcov - open htmlcov/index.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'] +[group('style')] @ruff: - ruff check --fix src tests - ruff format src tests + ruff check --fix src tests + ruff format src tests # Run pre-commit on all files -[group: 'style'] +[group('style')] @pre-commit: - uv run --no-dev --group style pre-commit run --show-diff-on-failure --color=always --all-files + 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'] +[group('style')] @pylint: - uv run --no-dev --group style pylint --recursive=y --output-format colorized src tests + 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'] +[group('style')] @ty: - uv run --no-dev --group style ty check + 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'] @@ -196,11 +206,11 @@ export JUST_LOG := log # uv run --no-dev --group docs rstcheck --recursive *.rst docs # # build documentation -[group: 'docs'] +[group('docs')] @docs-build *args: - uv run --no-dev --group docs mkdocs build -d .docs-build --theme readthedocs {{args}} + uv run --no-dev --group docs mkdocs build -d .docs-build --theme readthedocs {{ args }} # browse and edit documentation with auto build -[group: 'docs'] +[group('docs')] @docs: - uv run --no-dev --group docs mkdocs serve --dirty --watch docs --theme readthedocs + uv run --no-dev --group docs mkdocs serve --dirty --watch docs --theme readthedocs diff --git a/pyproject.toml b/pyproject.toml index 9e3cd91..1ee411f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ test = ["pytest>=9.0.3", "pytest-cov>=7.1.0", "pytest-mock>=3.15.1"] [tool.coverage] run.branch = true -run.omit = ["**/__init__.py", "*_tab.py", "tools/*.py"] +run.omit = ["__main__.py", "**/__init__.py", "*_tab.py", "tools/*.py"] run.parallel = true run.source = ["src"] paths.source = ["src"] From 49c849d22a9a09978b54527f5c4ec7c889629bc2 Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 20:19:22 +0300 Subject: [PATCH 36/54] build: fix 'just clean' command --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 4cf6805..98fc50d 100644 --- a/justfile +++ b/justfile @@ -62,11 +62,11 @@ export JUST_LOG := log "_build" "build" "dist" - "docs/_build" "htmlcov" ) for d in "${dirs_to_clean[@]}"; do - find . -type d -wholename "$d" -exec rm -rf {} + + echo "Remove $d" + find . -type d -name "$d" -exec rm -rf {} + done coverage erase #pyreverse files From e55f4d2579df1b9a17e5372e04cd92cb1f0f2d4e Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 20:19:55 +0300 Subject: [PATCH 37/54] test: better diangostics on different texts --- tests/test_travis.py | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 5421547..1ba2116 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -1,10 +1,12 @@ 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 @@ -51,7 +53,7 @@ def test_mode_options_inp(cd_tmpdir, capsys, mode, options, inp): main(command) out, _ = capsys.readouterr() ref_path = subdir / (inp + ".ref") - _assert_equal(out, ref_path) + _assert_str_path_equal(out, ref_path) cdense_data = Path(data / "cdens") @@ -67,7 +69,7 @@ def test_cdens(cd_tmpdir, capsys, inp, map_): main(command) out, _ = capsys.readouterr() ref_path = cdense_data / f"{inp}.{map_}.ref" - _assert_equal(out, ref_path) + _assert_str_path_equal(out, ref_path) merge_data = Path(data / "merge") @@ -96,15 +98,49 @@ def test_merge(cd_tmpdir, capsys, inp, merged): main(command) out, _ = capsys.readouterr() ref_path = merge_data / f"{merged}.{inp}.ref" - _assert_equal(out, ref_path) + _assert_str_path_equal(out, ref_path) -def _assert_equal(out: str, ref_path: Path) -> None: - name = ref_path.name +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) - def report(o: str, r: str) -> str: - return "\n".join(difflib.Differ().compare([o], [r])) +@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: - for i, (o, r) in enumerate(zip(out.split("\n"), filter(lambda s: s, f.readlines()))): - assert o.strip() == r.strip(), f"{name}:{i + 1} {report(o, r)}" + assert_lines_equal(ref_path.name, StringIO(out).readlines(), f.readlines()) From 3c87ad452435c7e266e135343d9fa79714dd2bd0 Mon Sep 17 00:00:00 2001 From: dvp Date: Wed, 22 Apr 2026 23:12:21 +0300 Subject: [PATCH 38/54] test: add tests bottom up --- src/numjuggler/likefunc.py | 62 +++++++------ src/numjuggler/numbering.py | 94 ++++++++++---------- src/numjuggler/utils/PartialFormatter.py | 101 ---------------------- src/numjuggler/utils/__init__.py | 5 +- src/numjuggler/utils/{io.py => _io.py} | 31 ++++--- src/numjuggler/utils/partial_formatter.py | 32 +++++++ tests/test_likefunc.py | 5 +- tests/test_numbering.py | 33 +++++++ tests/utils/__init__.py | 0 tests/utils/test_io.py | 40 +++++++++ tests/utils/test_partial_formatter.py | 51 +++++++++++ 11 files changed, 268 insertions(+), 186 deletions(-) delete mode 100644 src/numjuggler/utils/PartialFormatter.py rename src/numjuggler/utils/{io.py => _io.py} (50%) create mode 100644 src/numjuggler/utils/partial_formatter.py create mode 100644 tests/test_numbering.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_io.py create mode 100644 tests/utils/test_partial_formatter.py diff --git a/src/numjuggler/likefunc.py b/src/numjuggler/likefunc.py index f9e43dc..6564c81 100644 --- a/src/numjuggler/likefunc.py +++ b/src/numjuggler/likefunc.py @@ -1,19 +1,20 @@ -# -*- conding: utf-8 -*- - """ New implementation of mapping, where one can specify different functions for different ranges and separate values. """ + from __future__ import annotations from collections import OrderedDict -from numjuggler.utils.io import resolve_fname_or_stream +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,16 +24,19 @@ def const_func(c): """ Return function f(x) = c. """ + def f(x): return c - f._mydoc = f'{c}' + + f._mydoc = f"{c}" return f def add_func(c): def f(x): return x + c - f._mydoc = f'x + {c}' + + f._mydoc = f"x + {c}" return f @@ -88,15 +92,15 @@ def __str__(self): res.extend(self._str()) - res.append(f'other -> {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(f'{t} {nnew}: {nold}', file=fout) + print(f"{t} {nnew}: {nold}", file=fout) class LikeFunction(LikeFunctionBase): @@ -106,13 +110,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().__init__(log) # OrderedDict of range -> function self.mappings = OrderedDict() - def get_value(self, x): for rng, f in reversed(self.mappings.items()): if x in rng: @@ -122,7 +126,7 @@ def get_value(self, x): def _str(self): res = [] for r, f in self.mappings.items(): - res.append(f'{r} -> {f._mydoc}') + res.append(f"{r} -> {f._mydoc}") return res @@ -132,6 +136,7 @@ class LikeIndexFunction(LikeFunctionBase): List to be indexed is in self.vals. """ + def __init__(self, log=False, i0=1, skip=[], vals=[]): super().__init__(log) @@ -182,7 +187,7 @@ def get_valueD(self, x): def _str(self): res = [] for x in self.vals: - res.append(f'{x} -> {self.get_value(x)}') + res.append(f"{x} -> {self.get_value(x)}") return res @@ -190,6 +195,7 @@ 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 @@ -202,12 +208,12 @@ def __init__(self, x1, x2=None): def __contains__(self, value): if self.__x2 is None: return value == self.__x1 - return (self.__x1 <= value <= self.__x2) + return self.__x1 <= value <= self.__x2 def __str__(self): if self.__x2 is None: return str(self.__x1) - return f'[{self.__x1} -- {self.__x2}]' + return f"[{self.__x1} -- {self.__x2}]" def __hash__(self): return hash((self.__x1, self.__x2)) @@ -218,8 +224,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)) @@ -231,7 +238,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: @@ -239,7 +246,7 @@ def read_map_file(fname, log=False): continue if t not in maps: m = LikeFunction(log=log) - m.doc = f'Mappping for `{t}` from `{fname}`' + m.doc = f"Mappping for `{t}` from `{fname}`" maps[t] = m m = maps[t] for r in ranges: @@ -255,20 +262,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)) @@ -283,14 +290,14 @@ 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)) @@ -311,13 +318,14 @@ def get_indices(scards, log=False): 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 = [] @@ -328,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/src/numjuggler/numbering.py b/src/numjuggler/numbering.py index 5770dfd..32966d6 100644 --- a/src/numjuggler/numbering.py +++ b/src/numjuggler/numbering.py @@ -1,9 +1,12 @@ """ Functions to renumber cells, surfaces, etc. in MCNP input file. """ + from __future__ import annotations import collections +from pathlib import Path +from typing import Callable import warnings @@ -11,6 +14,7 @@ class _Range: """ Represents a range or a point. """ + def __init__(self, n1, n2=None): if n2 is None: self.n1 = n1 @@ -23,12 +27,22 @@ def __init__(self, n1, n2=None): def __contains__(self, value): if self.n2 is None: return value == self.n1 - return (self.n1 <= value <= self.n2) + return self.n1 <= value <= self.n2 def __str__(self): if self.n2 is None: return str(self.n1) - return f'[{self.n1} -- {self.n2}]' + return f"[{self.n1} -- {self.n2}]" + + +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: """ @@ -47,7 +61,7 @@ class LikeFunction: > 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), ...] @@ -67,31 +81,31 @@ class LikeFunction: 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. + 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): + def __applyD(f: MappingItem, n: int) -> MappingItem: if isinstance(f, collections.Callable): return f(n) return f @staticmethod - def __applyL(f, n): + def __applyL(f: MappingItem, n: int) -> int: if isinstance(f, collections.Callable): return f(n) 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] 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 @@ -116,15 +130,15 @@ def __call__(self, n, t): k = (t, nnew) if k in ld: if ld[k] != n: - warnings.warn('Non-injective mapping. ' - '({}, {}) and ({}, {}) ' - f'are mapped to {t}') + 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(f'WARNING: material {n} replaced with {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): @@ -132,18 +146,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(f'{t} {nnew:>6d}: {n:>6d}', file=f) + print(f"{t} {nnew:>6d}: {n:>6d}", file=f) def get_numbers(scards): @@ -189,16 +203,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: @@ -208,7 +222,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. @@ -220,11 +234,7 @@ 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()): @@ -232,7 +242,7 @@ def read_map_file(fname): 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] @@ -271,23 +281,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 @@ -296,13 +306,13 @@ 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) @@ -312,7 +322,3 @@ def _get_map_ranges(s): if v1 is not None: yield v1, v1 v1 = int(t) - - -if __name__ == '__main__': - pass diff --git a/src/numjuggler/utils/PartialFormatter.py b/src/numjuggler/utils/PartialFormatter.py deleted file mode 100644 index 3a293b1..0000000 --- a/src/numjuggler/utils/PartialFormatter.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import string - -import six - - -def make_label(item): - if len(item) > 0: - return '{' + item + '}' - return item - - -class SafeDict(dict): - - def __getitem__(self, item): - return super().__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 - return result, auto_arg_index diff --git a/src/numjuggler/utils/__init__.py b/src/numjuggler/utils/__init__.py index 6b7dafb..d300327 100644 --- a/src/numjuggler/utils/__init__.py +++ b/src/numjuggler/utils/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations -from .PartialFormatter import PartialFormatter +from ._io import resolve_fname_or_stream +from .partial_formatter import PartialFormatter -__all__ = ["PartialFormatter"] +__all__ = ["PartialFormatter", "resolve_fname_or_stream"] diff --git a/src/numjuggler/utils/io.py b/src/numjuggler/utils/_io.py similarity index 50% rename from src/numjuggler/utils/io.py rename to src/numjuggler/utils/_io.py index e38e07b..3420ebd 100644 --- a/src/numjuggler/utils/io.py +++ b/src/numjuggler/utils/_io.py @@ -1,24 +1,35 @@ from __future__ import annotations -import os +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 cd_temporarily(cd_to): - cur_dir = str(Path.cwd()) - try: - os.chdir(str(cd_to)) - yield - finally: - os.chdir(cur_dir) +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 + """ -@contextmanager -def resolve_fname_or_stream(fname_or_stream, mode="r"): is_input = mode == "r" if fname_or_stream is None: if is_input: 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/test_likefunc.py b/tests/test_likefunc.py index a22c799..0767e5e 100644 --- a/tests/test_likefunc.py +++ b/tests/test_likefunc.py @@ -1,6 +1,7 @@ +from io import StringIO + import pytest -from numjuggler.utils.io import cd_temporarily -from six import StringIO + import numjuggler.likefunc as lf diff --git a/tests/test_numbering.py b/tests/test_numbering.py new file mode 100644 index 0000000..44c4ede --- /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, 5)]) +def test_like_function(pdict, n, expected): + lf = LikeFunction(pdict) + assert lf(n, "c") == expected diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 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 From 7edb20ba7b339b327edad52ff0251b0553afeb54 Mon Sep 17 00:00:00 2001 From: dvp Date: Thu, 23 Apr 2026 18:44:48 +0300 Subject: [PATCH 39/54] refact: move get_numbers to parser module this method only relates to Card class, which is not defined in parser module. And this is the only used method from numbering --- src/numjuggler/likefunc.py | 2 +- src/numjuggler/main.py | 1017 ++++++++++++++++++----------------- src/numjuggler/numbering.py | 13 - src/numjuggler/parser.py | 78 ++- 4 files changed, 548 insertions(+), 562 deletions(-) diff --git a/src/numjuggler/likefunc.py b/src/numjuggler/likefunc.py index 6564c81..1697ec7 100644 --- a/src/numjuggler/likefunc.py +++ b/src/numjuggler/likefunc.py @@ -7,6 +7,7 @@ from collections import OrderedDict +from numjuggler.parser import get_numbers from numjuggler.utils import resolve_fname_or_stream @@ -317,7 +318,6 @@ 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) diff --git a/src/numjuggler/main.py b/src/numjuggler/main.py index 8a38e7d..332e75f 100644 --- a/src/numjuggler/main.py +++ b/src/numjuggler/main.py @@ -27,36 +27,36 @@ 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: @@ -84,87 +84,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) -> None: # 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(f'{indent}{t[0]:>3s}', end='', file=outstr) + print(f"{indent}{t[0]:>3s}", end="", file=outstr) if r1 == r2: - rs = f' {r1}' + rs = f" {r1}" else: - rs = f' {r1} -- {r2}' + rs = f" {r1} -- {r2}" if rp is not None: - fr = f'{r1 - rp - 1}' + fr = f"{r1 - rp - 1}" else: - fr = '' - ur = f'{r2 - r1 + 1}' - print(f'{rs:<30s} {ur:>8s} {fr:>8s}', 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(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': + 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: @@ -173,13 +205,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 @@ -192,15 +224,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[:] @@ -208,7 +241,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() @@ -222,27 +255,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(f'sd{nt} 1 {nc - 1}r', file=outstr) - print(f'fc{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(len(csets[u]), end="", file=outstr) print(file=outstr) - elif args.mode == 'addgeom': + 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() @@ -255,13 +288,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: @@ -273,10 +306,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 @@ -297,7 +330,7 @@ def processing(args, cards, debuglog): mb = [] if mb: for c in mb: - print(c.card(), end='', file=outstr) + print(c.card(), end="", file=outstr) print(file=outstr) # Title: if args.t == "0": @@ -312,13 +345,13 @@ 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 ' + f'"{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) + print(c.card(), end="", file=outstr) if blk2.get(t): # First check if blk2 actually contains any cards: flg = False @@ -327,14 +360,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': + elif args.mode == "uexp": if args.u == "0": N = " u=0 " else: @@ -347,54 +380,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 + f'.{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 + f'.{k}z', '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) @@ -408,60 +444,54 @@ 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 = f'm{c.name} from {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: @@ -471,36 +501,34 @@ def cfunc(n): 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 @@ -512,12 +540,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)) @@ -544,32 +572,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) or (c.ctype == mp.CID.surface and c.name not in sset): + 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) + 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: @@ -577,23 +605,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) l = len(str(max(uref))) - f = f'{{0:0{l}d}}' + f = f"{{0:0{l}d}}" for u in sorted(uref): s = f.format(u) - 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': + 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())) @@ -607,28 +635,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 += f'({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 = f'*tr{trn} 0 0 0 {ag} {ag-90} 90 {90+ag} {ag} 90 90 90 0' + 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 +665,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 +680,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 +695,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)] 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,19 +746,19 @@ 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): for c in l.split(): @@ -741,10 +770,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 +785,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,56 +839,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) 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) 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] + - f'$ a^2={a2:12.6e} c={g + a2:12.6e}\n') + 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 @@ -875,28 +904,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 += f'{c.name} {trn + trn0} c/z {x0:15.8e} 0 {R:15.8e}\n' + 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 @@ -904,14 +932,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(f'c Log for GQ card {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 @@ -919,10 +947,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: @@ -941,32 +969,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()): @@ -975,16 +997,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', - f'>{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 @@ -996,47 +1014,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("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 = {} @@ -1053,56 +1073,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(f'm{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(f'Compact list of cells for material m{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(f'{m:20d}{v:20e}{w:20e}', 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 @@ -1121,13 +1141,13 @@ 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(f'u{u} ', end='', file=outstr) + print(f"u{u} ", end="", file=outstr) for e in rin.shorten(l): - print(e, end=' ', file=outstr) + print(e, end=" ", file=outstr) print(file=outstr) print(len(l), file=outstr) else: @@ -1136,20 +1156,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(): @@ -1159,8 +1179,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 @@ -1174,136 +1194,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, 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} $ {} {}' + 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(f'sdef sur {s} nrm -1 wgt {Pi * r**2:12.7e}', 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: @@ -1316,75 +1339,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(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) + 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("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') + 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') - 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: @@ -1403,38 +1424,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 = 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].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 = f'Function for {t} from command line' + 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) @@ -1442,65 +1463,56 @@ 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=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='') + 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, f'help/{harg.h}.rst') - print(f'Reading help from {hlp}') + dir1 = os.path.split(dir1)[0] # remove the most deep dir + hlp = os.path.join(dir1, f"help/{harg.h}.rst") + print(f"Reading help from {hlp}") print(open(hlp).read()) except Exception: print(f'Cannot read help file for "{harg.h}"') @@ -1519,38 +1531,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/src/numjuggler/numbering.py b/src/numjuggler/numbering.py index 32966d6..8217716 100644 --- a/src/numjuggler/numbering.py +++ b/src/numjuggler/numbering.py @@ -160,19 +160,6 @@ def write_log_as_map(self, fname): print(f"{t} {nnew:>6d}: {n:>6d}", 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 - def get_indices(scards): """ diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index 770b477..4809e7d 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -7,6 +7,7 @@ import os from pathlib import Path import re +from typing import Iterable, Literal import warnings from io import StringIO @@ -93,7 +94,7 @@ class __CIDClass: 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. """ @@ -112,7 +113,7 @@ class Card: Representation of a card. """ - def __init__(self, lines, ctype, pos, debug=None): + def __init__(self, lines: list[str], ctype: Literal[1, 2, 3, 4, 5], pos: int, debug=None): # Original lines, as read from the input file self.lines = lines @@ -151,11 +152,7 @@ def __init__(self, lines, ctype, pos, debug=None): self.hidden = {} # 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]] = [] # some properties defined on demand # cell properties @@ -487,7 +484,9 @@ def get_f(self, newv=None): def get_imp(self, vals=None): """ - Returns importances, if explicitly specified in the cell card. + Returns + ------- + importances, if explicitly specified in the cell card. """ if vals is None: vals = {} @@ -688,39 +687,20 @@ def apply_map(self, f): self.print_debug("after apply_map", "vi") -# 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 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): @@ -1135,7 +1115,7 @@ 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 + If dump exists and it is newer than the input file, read the dump file """ yield from get_cards_from_input(inp, debug=debug, preservetabs=preservetabs) @@ -1171,12 +1151,18 @@ def load_decode_buffer(filename): return StringIO(finp.read().decode(inpencoding, errors="backslashreplace")) -def get_cards_from_input(inp, debug=None, preservetabs=False): - """ +def get_cards_from_input(inp: str, debug=None, preservetabs=False) -> Iterable[Card]: + """Load cards from a file. + + Parameters + ---------- + inp + the filename. + + Returns + ------- Iterable, return instances of the Card() class representing cards in the input file. - - inp -- is the filename. """ def _yield(card, ct, ln): From ad91143d7526dfc38264789bf257e915dcc37a3b Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 00:57:23 +0300 Subject: [PATCH 40/54] test: increasing coverage of parser module --- src/numjuggler/parser.py | 40 +++++----- tests/test_parser.py | 153 +++++++++++++++++++++++++++++++++++---- 2 files changed, 159 insertions(+), 34 deletions(-) diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index 4809e7d..dd7d16c 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -4,28 +4,18 @@ from __future__ import annotations -import os -from pathlib import Path +from typing import Iterable, Literal, TextIO + import re -from typing import Iterable, Literal import warnings from io import StringIO - -import six +from pathlib import Path from chardet import UniversalDetector from numjuggler.utils import PartialFormatter -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+") @@ -101,8 +91,8 @@ def get_name(cls, cid: str) -> str: for k, v in list(cls.__dict__.items()): if "__" not in k and v == cid: return k - print("No attribute with name", cid) - raise ValueError + msg = f"No CID names with value {cid}" + raise ValueError(msg) CID = __CIDClass() @@ -113,7 +103,9 @@ class Card: Representation of a card. """ - def __init__(self, lines: list[str], ctype: Literal[1, 2, 3, 4, 5], pos: int, debug=None): + def __init__( + self, lines: list[str], ctype: Literal[1, 2, 3, 4, 5], pos: int, debug: TextIO | None = None + ): # Original lines, as read from the input file self.lines = lines @@ -139,12 +131,12 @@ def __init__(self, lines: list[str], ctype: Literal[1, 2, 3, 4, 5], pos: int, de # 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 = "" # 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] = [] # Dictionary of parts that are removed from input before processing it. # For example, repitition syntax (e.g. 5r or 7i) is replaced with '!' @@ -217,7 +209,7 @@ def print_debug(self, comment, key="tihv"): if "v" in key: print(" values: ", self.values, file=d) - def get_input(self, check_bad_chars=False): + def get_input(self, check_bad_chars: bool = False) -> None: """ Recompute template, input and hidden attributes from lines """ @@ -1120,13 +1112,21 @@ def get_cards(inp, debug=None, preservetabs=False): 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(f"[{chars}]") m = r.search(line) 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): diff --git a/tests/test_parser.py b/tests/test_parser.py index e5bf434..cd98a48 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,18 +1,143 @@ -from numjuggler.parser import Card +from io import StringIO +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 - cell2 = Card(definition, ctype=3, pos=1) - cell2.get_values() - 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 + + +def test_vol_param(card_600177: Card): + assert "Vol=1.335972e+01" in card_600177.card() + + +def test_get_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( + "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) From a65e8ceb6aecc240e62e0e60527b0f8805a9f7fd Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 13:32:01 +0300 Subject: [PATCH 41/54] fix: are_close_lists --- src/numjuggler/parser.py | 86 +++++++++++++++++++++++++++++++--------- tests/test_parser.py | 56 +++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 19 deletions(-) diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index dd7d16c..4581180 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -4,6 +4,7 @@ from __future__ import annotations +from numbers import Number from typing import Iterable, Literal, TextIO import re @@ -474,8 +475,16 @@ def get_f(self, newv=None): self.__f = None return self.__f - def get_imp(self, vals=None): - """ + 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. @@ -513,9 +522,8 @@ def get_imp(self, vals=None): self.__i = res return self.__i - def remove_fill(self): - """ - Removes the FILL= keyword of a cell card. + def remove_fill(self) -> None: + """Remove the FILL= keyword and its values from this cell card. This method must be called after get_values(). """ @@ -1314,9 +1322,23 @@ def get_blocks(cards): return d -def are_close_vals(x, y, re=1e-6, ra=0.0): - """ - Return True if x and y are closer then re or ra. +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 + + Returns + ------- + True if x and y are closer then re or ra. """ if abs(x - y) <= ra: r = True @@ -1328,11 +1350,29 @@ def are_close_vals(x, y, re=1e-6, ra=0.0): return r -def are_close_lists(x, y, re=1e-6, pci=None): - """ - Return True if x and y are close but not equal. +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 pci is None: + if pci is None: # TODO @dvp2015: move object creation below the preliminary check pci = [] if len(x) != len(y): res = False @@ -1359,7 +1399,7 @@ def are_close_lists(x, y, re=1e-6, pci=None): xp = [] yp = [] i = 0 - for i1, i2 in zip(pci[0::2], pci[1::2], strict=False): + 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] @@ -1367,12 +1407,22 @@ def are_close_lists(x, y, re=1e-6, pci=None): 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 = [] + # @dvp2015 changed: + # 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) + ypn = max(abs(e) for e in yp) + if xpn > 0 and ypn > 0: + yp = [e * xpn / ypn for e in yp] + + msg = [] # TODO @dvp2015: remove debugging code: `msg`, `res` res = [] for xl, yl in zip([xe, xp], [ye, yp], strict=False): # compare xl and yl without normalization diff --git a/tests/test_parser.py b/tests/test_parser.py index cd98a48..62b4bba 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,7 +2,7 @@ import pytest -from numjuggler.parser import Card, CID +from numjuggler.parser import Card, CID, are_close_lists @pytest.fixture @@ -122,6 +122,60 @@ def test_card_get_f(): 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), + None, + {"imp:n": 2}, + "expect `imp:n=2`", + ), + ], +) +def test_card_get_imp(card, vals, expected, msg): + card.get_values() + actual = card.get_imp(vals) + assert actual == expected, msg + + +@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, 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), + ], +) +def test_are_close_lists(x, y, re, pci, expected): + actual = are_close_lists(x, y, re, pci) + assert actual == expected + + @pytest.mark.parametrize( "value, expected_name", [ From 427da34157de14c2919eb086e0074e4fc685e602 Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 14:13:56 +0300 Subject: [PATCH 42/54] test: remove from test texts --- tests/test_travis.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 1ba2116..f277040 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -143,4 +143,9 @@ def test_assert_lines_equal_when_not_equal(a, b): def _assert_str_path_equal(out: str, ref_path: Path) -> None: with ref_path.open(encoding="utf8") as f: - assert_lines_equal(ref_path.name, StringIO(out).readlines(), f.readlines()) + actual = StringIO(out).readlines() + expected = f.readlines() + if sys.platform == "win32": + actual = [s.remove("\r") for s in actual] + expected = [s.remove("\r") for s in expected] + assert_lines_equal(ref_path.name, actual, expected) From d363ed9fcd2b5a0c1bf840209eb4701f2e4aa8cb Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 14:15:47 +0300 Subject: [PATCH 43/54] test: remove from test texts --- tests/test_travis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index f277040..eab147c 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -146,6 +146,6 @@ def _assert_str_path_equal(out: str, ref_path: Path) -> None: actual = StringIO(out).readlines() expected = f.readlines() if sys.platform == "win32": - actual = [s.remove("\r") for s in actual] - expected = [s.remove("\r") for s in expected] + actual = [s.replace("\r", "") for s in actual] + expected = [s.replace("\r", "") for s in expected] assert_lines_equal(ref_path.name, actual, expected) From d26ca0a363152a0954c1fd0af121218f67651251 Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 14:19:22 +0300 Subject: [PATCH 44/54] test: remove xfail for Windows --- tests/test_travis.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index eab147c..99e2225 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -35,13 +35,10 @@ "-u -5942", "i3", ), - pytest.param( + ( "remh", "", "nested_complement", - marks=pytest.mark.xfail( - sys.platform == "win32", reason="Need more efforts on parser testing" - ), ), ], ) From c23d049a52bec40b707ea3f74d90a69dad44d302 Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 14:26:01 +0300 Subject: [PATCH 45/54] test: return xfail for Windows --- tests/test_travis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index 99e2225..eab147c 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -35,10 +35,13 @@ "-u -5942", "i3", ), - ( + pytest.param( "remh", "", "nested_complement", + marks=pytest.mark.xfail( + sys.platform == "win32", reason="Need more efforts on parser testing" + ), ), ], ) From e86d2ded33e4e9508e4cec7fc3d47dd2832be4b3 Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 14:32:25 +0300 Subject: [PATCH 46/54] test: remove xfail for Windows - this time correct one --- tests/test_travis.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_travis.py b/tests/test_travis.py index eab147c..ca40650 100644 --- a/tests/test_travis.py +++ b/tests/test_travis.py @@ -75,17 +75,15 @@ def test_cdens(cd_tmpdir, capsys, inp, map_): merge_data = Path(data / "merge") +# TODO @dvp2015: numjuggler parser on Windows leaves before the closing quote `inp\r"` + + @pytest.mark.parametrize( "inp,merged", [ - pytest.param( + ( "inp", "merged", - marks=pytest.mark.xfail( - sys.platform == "win32", - reason='...inp1.inp\\r"', - # numjuggler parser leaves character at the end of title before double quote - ), ) ], ) From 7c7df599a8dd41bdfc846387d0decdfe199a41be Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 17:50:52 +0300 Subject: [PATCH 47/54] test: encoding detection fails on short texts --- src/numjuggler/numbering.py | 13 +- src/numjuggler/parser.py | 73 ++++------ .../data/simple_cubes_with_multiline_tr.mcnp | 3 +- tests/test_numbering.py | 4 +- tests/test_parser.py | 131 ++++++++++++++++-- 5 files changed, 155 insertions(+), 69 deletions(-) diff --git a/src/numjuggler/numbering.py b/src/numjuggler/numbering.py index 8217716..a5f057c 100644 --- a/src/numjuggler/numbering.py +++ b/src/numjuggler/numbering.py @@ -4,11 +4,13 @@ from __future__ import annotations -import collections -from pathlib import Path -from typing import Callable +from typing import Callable, TYPE_CHECKING + import warnings +if TYPE_CHECKING: + from pathlib import Path + class _Range: """ @@ -89,13 +91,13 @@ def __init__(self, pdict: MappingDict, log: bool = False): @staticmethod def __applyD(f: MappingItem, n: int) -> MappingItem: - if isinstance(f, collections.Callable): + if isinstance(f, Callable): return f(n) return f @staticmethod def __applyL(f: MappingItem, n: int) -> int: - if isinstance(f, collections.Callable): + if isinstance(f, Callable): return f(n) return n + int(f) @@ -160,7 +162,6 @@ def write_log_as_map(self, fname): print(f"{t} {nnew:>6d}: {n:>6d}", file=f) - def get_indices(scards): """ Return a dictionary that can be used as an argument for the LikeFunction diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index 4581180..500092c 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -572,9 +572,17 @@ def remove_fill(self) -> None: 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: @@ -982,7 +990,7 @@ def _get_int(s): return r -def _parse_tr(input_): +def _parse_tr(input_: list[str]) -> tuple[str, list[str], list[tuple[float, str]]]: """ input_ should be already passed through _split_data() """ @@ -998,7 +1006,7 @@ def _parse_tr(input_): return unit, (inp1 + " " + inp2).split("\n"), fvals -def _split_data(input_): +def _split_data(input_: list[str]) -> tuple[list[str], list[tuple[int, str], str | None]]: inpt = "\n".join(input_) t = inpt.split() @@ -1137,7 +1145,7 @@ def index_(line: str, chars: str = "$&") -> int: # return d -def load_decode_buffer(filename): +def load_decode_buffer(filename: str | Path) -> StringIO: """ Load and decode the text inside an file to a string buffer. @@ -1340,14 +1348,7 @@ def are_close_vals(x: Number, y: Number, re: float = 1e-6, ra: float = 0.0) -> b ------- 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 + 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: @@ -1372,15 +1373,15 @@ def are_close_lists(x: list[Number], y: list[Number], re=1e-6, pci=None) -> bool ------- True if x and y are close but not equal. """ - if pci is None: # TODO @dvp2015: move object creation below the preliminary check - pci = [] 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: @@ -1408,7 +1409,7 @@ def are_close_lists(x: list[Number], y: list[Number], re=1e-6, pci=None) -> bool # normalize yp - # @dvp2015 changed: + # @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) @@ -1417,35 +1418,17 @@ def are_close_lists(x: list[Number], y: list[Number], re=1e-6, pci=None) -> bool # yp = [e * xpn / ypn for e in yp] # to: if xp and yp: - xpn = max(abs(e) for e in xp) + 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: - yp = [e * xpn / ypn for e in yp] + r = xpn / ypn + yp = [e * r for e in yp] - msg = [] # TODO @dvp2015: remove debugging code: `msg`, `res` - res = [] 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, strict=False): - r = are_close_vals(xx, yy, re) - if not r: - m = f"diff at {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 + 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/tests/data/simple_cubes_with_multiline_tr.mcnp b/tests/data/simple_cubes_with_multiline_tr.mcnp index 9d19c2a..5057b10 100644 --- a/tests/data/simple_cubes_with_multiline_tr.mcnp +++ b/tests/data/simple_cubes_with_multiline_tr.mcnp @@ -47,4 +47,5 @@ c c Comment may contain entries with braces {31c} c tr1 - 100 0 0 \ No newline at end of file + 100 0 0 + \ No newline at end of file diff --git a/tests/test_numbering.py b/tests/test_numbering.py index 44c4ede..a047759 100644 --- a/tests/test_numbering.py +++ b/tests/test_numbering.py @@ -26,8 +26,8 @@ def test_range_class(): 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, 5)]) +# @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 62b4bba..5e81d31 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,8 +1,13 @@ +from encodings import cp1251 +from enum import unique from io import StringIO +import locale +from pathlib import Path +from textwrap import dedent import pytest -from numjuggler.parser import Card, CID, are_close_lists +from numjuggler.parser import Card, CID, _split_data, are_close_lists, load_decode_buffer @pytest.fixture @@ -18,6 +23,27 @@ def card_600177() -> Card: 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() @@ -165,10 +191,17 @@ def test_card_remove_fill(card): "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, 100], [2, 4, 6, 100], 0.1, (0, 3), 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): @@ -177,21 +210,89 @@ def test_are_close_lists(x, y, re, pci, expected): @pytest.mark.parametrize( - "value, expected_name", + "card,expected", [ - (-1, "comment"), - (-2, "blankline"), - (1, "message"), - (2, "title"), - (3, "cell"), - (4, "surface"), - (5, "data"), + (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_cid(value, expected_name): - assert CID.get_name(value) == expected_name +def test_remove_spaces(card, expected): + card.get_values() + card.remove_spaces() + assert card.card() == expected -def test_cid_bad_path(): - with pytest.raises(ValueError, match="No CID names with value 100"): - CID.get_name(100) +@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", + [ + (["1 0 1\n", " 2 3 4\n", "\n", "m1 00101 1\n", "tr1 1 0 0 1\n"], None), + (["1 0 1\n", " 2 3 4\n", "\n", "m1 00101 1\n", "tr1\n 0 0 1\n"], None), + ], +) +def test_split_data(inp, expected): + actual = _split_data(inp) + assert actual is not None From 6a5e61c029da089210e8834c00f12e340cdcc067 Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 21:06:49 +0300 Subject: [PATCH 48/54] test: setup xdoctest --- justfile | 8 ++++---- pyproject.toml | 8 +++++++- pytest.toml | 1 + uv.lock | 16 ++++++++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/justfile b/justfile index 98fc50d..83ba2f0 100644 --- a/justfile +++ b/justfile @@ -140,10 +140,10 @@ export JUST_LOG := log @test *args: uv run --no-dev --group test pytest {{ args }} -# # run documentation tests -# [group: 'test'] -# @xdoctest *args: -# uv run --no-dev --group test python -m xdoctest --silent -c all src tools {{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')] diff --git a/pyproject.toml b/pyproject.toml index 1ee411f..106d7f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,13 @@ style = [ "ty>=0.0.31", ] pylint = ["pylint", "pylint-per-file-ignores"] -test = ["pytest>=9.0.3", "pytest-cov>=7.1.0", "pytest-mock>=3.15.1"] +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 diff --git a/pytest.toml b/pytest.toml index 81906a8..9dd7b24 100644 --- a/pytest.toml +++ b/pytest.toml @@ -25,6 +25,7 @@ addopts = [ "--strict-markers", "--ignore=setup.py", "--failed-first", + "--xdoctest" ] doctest_optionflags = [ "ELLIPSIS", diff --git a/uv.lock b/uv.lock index 7c9d3a5..f4a3863 100644 --- a/uv.lock +++ b/uv.lock @@ -698,6 +698,7 @@ dev = [ { name = "pytest-mock" }, { name = "ruff" }, { name = "ty" }, + { name = "xdoctest" }, ] docs = [ { name = "griffe-public-wildcard-imports" }, @@ -719,9 +720,11 @@ style = [ { name = "ty" }, ] test = [ + { name = "pygments" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "xdoctest" }, ] [package.metadata] @@ -740,6 +743,7 @@ dev = [ { 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" }, @@ -747,6 +751,7 @@ dev = [ { 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" }, @@ -768,9 +773,11 @@ style = [ { 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]] @@ -1297,3 +1304,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e476 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" }, +] From 76312a1a61f3126c8ac1db81dba5c4e8ac7372d0 Mon Sep 17 00:00:00 2001 From: dvp Date: Fri, 24 Apr 2026 21:08:27 +0300 Subject: [PATCH 49/54] docs: _parse_tr, _get_int --- src/numjuggler/parser.py | 45 ++++++++++++++++++++++++++++++++++++++-- tests/test_parser.py | 2 +- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index 500092c..3727310 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -980,7 +980,27 @@ def _split_surface(input_): return inpt.split("\n"), vals, st, scoef -def _get_int(s): +def _get_int(s: str) -> str: + """Extract digits at the end of a word `s`. + + Example + ------- + >>> _get_int("TR1") + '1' + >>> _get_int("100") + '100' + >>> _get_int("m2") + '2' + + Parameters + ---------- + s + string to parse + + Returns + ------- + str: digits value part + """ r = "" for c in s: if r and c.isalpha(): @@ -991,8 +1011,29 @@ def _get_int(s): 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_) inp1, inp2 = inpt.split(None, 1) diff --git a/tests/test_parser.py b/tests/test_parser.py index 5e81d31..01ac696 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -48,7 +48,7 @@ def test_vol_param(card_600177: Card): assert "Vol=1.335972e+01" in card_600177.card() -def test_get_set_value(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) From f0df992ef39ea199f213f7f71a8e6a9caabdf48b Mon Sep 17 00:00:00 2001 From: dvp Date: Sat, 25 Apr 2026 19:57:13 +0300 Subject: [PATCH 50/54] docs: documenting Card class --- src/numjuggler/parser.py | 69 ++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index 3727310..e54af02 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -101,33 +101,66 @@ def get_name(cls, cid: str) -> str: 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 + Optional file-like object to write debug info, by default None + cstrg + True if self.lines has changed after initialization + used in remove_hash function (default False) + + 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. """ 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 + """ - # Original lines, as read from the input file 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 info""" # template string. Represents the general structure of the card. It is # a copy of lines, but meaningful parts are replaced by format @@ -981,7 +1014,7 @@ def _split_surface(input_): def _get_int(s: str) -> str: - """Extract digits at the end of a word `s`. + """Extract contigous digits from a word `s`. Example ------- @@ -991,6 +1024,8 @@ def _get_int(s: str) -> str: '100' >>> _get_int("m2") '2' + >>> _get_int("d1s") + '1' Parameters ---------- @@ -1208,21 +1243,27 @@ def load_decode_buffer(filename: str | Path) -> StringIO: return StringIO(finp.read().decode(inpencoding, errors="backslashreplace")) -def get_cards_from_input(inp: str, debug=None, preservetabs=False) -> Iterable[Card]: +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 + Iterable, return instances of the Card class representing cards in the input file. """ - 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): From fa82d3eed3a0ebadf1e1efd969b0ccb281215455 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 26 Apr 2026 00:00:38 +0300 Subject: [PATCH 51/54] fix: restore help output --- src/numjuggler/help/cdens.md.txt | 34 +++++++++++++++ src/numjuggler/help/from_doc.sh | 8 ++++ src/numjuggler/help/index.md.txt | 59 +++++++++++++++++++++++++ src/numjuggler/help/info.md.txt | 24 ++++++++++ src/numjuggler/help/map.md.txt | 74 +++++++++++++++++++++++++++++++ src/numjuggler/help/renum.md.txt | 75 ++++++++++++++++++++++++++++++++ src/numjuggler/main.py | 5 ++- tests/data/continue | 3 ++ tests/data/with_message.mcnp | 51 ++++++++++++++++++++++ 9 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 src/numjuggler/help/cdens.md.txt create mode 100755 src/numjuggler/help/from_doc.sh create mode 100644 src/numjuggler/help/index.md.txt create mode 100644 src/numjuggler/help/info.md.txt create mode 100644 src/numjuggler/help/map.md.txt create mode 100644 src/numjuggler/help/renum.md.txt create mode 100644 tests/data/continue create mode 100644 tests/data/with_message.mcnp diff --git a/src/numjuggler/help/cdens.md.txt b/src/numjuggler/help/cdens.md.txt new file mode 100644 index 0000000..6e04296 --- /dev/null +++ b/src/numjuggler/help/cdens.md.txt @@ -0,0 +1,34 @@ + + +DESCRIPTION + + +Change cell densities according to the map file. + +In the map file one can specify coefficients to multiply the original +input file densities. The coefficients can be specified for particular +cells and/or materials (or their ranges -- see descritpion of the map +file format). + + +Rationale + +One of the approaches to get the fist guess for the mesh weight-window +through the model geometry is to decrease all material densities by +several orders of magnitude and perform particle transport to generate +the weight window mesh. + + +Invocation example + +in the following example, the map file is densities.txt, the original +input file is input.orig and the new input file is written to input.new: + + >numjuggler --mode cdens --map densities.txt input.orig > input.new + +The content of densities.txt: + + c 1 -- 10: 0.01 # In cells 1 to 10 multiply density by 0.01 + c 12: 0.1 # Multiply cell 12 density by 0.1 + + m 5: 1e-3 # For all cells filled with material 5 multiply density by 1e-3 diff --git a/src/numjuggler/help/from_doc.sh b/src/numjuggler/help/from_doc.sh new file mode 100755 index 0000000..79fbba9 --- /dev/null +++ b/src/numjuggler/help/from_doc.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generate txt files from md sources + +for md in $(ls ../docsrc/docs/*.md); do + pandoc -i "$md" --to plain -o $(basename "$md").txt +done + diff --git a/src/numjuggler/help/index.md.txt b/src/numjuggler/help/index.md.txt new file mode 100644 index 0000000..337ffbd --- /dev/null +++ b/src/numjuggler/help/index.md.txt @@ -0,0 +1,59 @@ + + +NUMJUGGLER + + +About + +Numjuggler is a command line tool to perform specific tasks on the MCNP +input files. + +Originally, it was developed at INR-KIT to rename cells, surfaces and +materials in the MCNP input file. This task appeared often within the +framework of ITER nuclear analyses, where different parts of the MCNP +computational model were developed independently by different +organizations and than merged together to the model describing the whole +ITER facility. The merging of the model parts into a single MCNP input +file was often complicated by the use of the same cell, surface and +material numbers in the different model parts. + +Later, other features was added to numjuggler. Now it can be run in one +of the execution modes, each performing particular task. + +The original development was previously conducted in the inr-kit +repository. The author of numjuggler has its own fork at +travleev/numjuggler. + + +Installation + +TODO: the best way -- clone from travleev/numjuggler and install in the +"development mode" using pip: pip install -e .. + +TODO: try under windows/anaconda(?) + + +Invocation + +numjuggler is a command line tool written in Python. When properly +installed, it can be invoked in one of the following ways: + + >numjuggler --mode [arguments] input.txt > input.new + >python -m numjuggler --mode [arguments] input.txt > input.new + +These two invocation forms are equal. The command line arguments in +general contain the only necessary argument -- the original MCNP input +file (in the above example input.txt). The output is written to standard +output that can be redirected to a file. The execution mode is specified +after the --mode flag. The other optional arguments can contain +additional arguments relevant to the chosen mode. + +TODO: how to get interactive help. + + +List of execution modes + +- renum +- info +- cdens + diff --git a/src/numjuggler/help/info.md.txt b/src/numjuggler/help/info.md.txt new file mode 100644 index 0000000..db1a6e1 --- /dev/null +++ b/src/numjuggler/help/info.md.txt @@ -0,0 +1,24 @@ + + +DESCRIPTION + + +The input file is analysed and ranges of used numbers for cells, +surfaces, ets. are written to the standard output. Note that the output +of this mode can be used (after necessary modifications) as the input to +the --map option. + +The first two columns specify type (cells, surfaces, etc.) and the range +of used numbers. The third column shows the amount of numbers in current +range, and the last column shows how many numbers left unused between +the current and previous ranges. + + + +INVOCATION EXAMPLE + + +The input file input.orig is analysed and the result is redirected to +info.txt: + + >numjuggler --mode info input.orig > info.txt diff --git a/src/numjuggler/help/map.md.txt b/src/numjuggler/help/map.md.txt new file mode 100644 index 0000000..67d73d1 --- /dev/null +++ b/src/numjuggler/help/map.md.txt @@ -0,0 +1,74 @@ + + +GENERAL INFO + + +Mapping rules can be specified in a separate file that is read when the +--map option is given. + +Different from the -c, -s, -m and -u command line arguments, in the map +file one can specify mapping rules for separate ranges. Ultimately, all +new cell, surface, material or universe names can be given explicitly. + + + +MAP FILE FORMAT + + +A map file consists of lines of the following form: + + t [range]: [+-]D + +The first entry, t, is the one-character type specification: c for +cells, s for surfaces, m for materials and u for universes. + +It is optionally followed by the range specifier that can be a single +number, for example 10, or two numbers delimited by two dashes, for +example 10 -- 25. If the range is omitted, the line defines the default +mapping, i.e. it is applied to elements not belonging to all other +ranges. + +The semicolon, :, delimits the range specification from the +specification of the mapping rule. It is followed by an integer, +optionally signed. This integer defines an increment to which numbers in +the current range are increased. The mapping in this case is N -> N+D, +where N is the original number from the range [N1, N2]. + +When an unsigned integer is given together with the range specification, +it is considered as the first element of the mapped range. Mapping in +this case is N -> N+D-N1, where N in [N1, N2]. + + + +MAP FILE EXAMPLES + + +In the example below, cells from 1 to 10, inclusive, are renamed to the +range from 11 to 20, cell 200 is renamed to 250 and all other cells +numbers are incremented by 1000: + + c 1 -- 10: +10 # explicit sign means increment + c 200: 250 # no sign means new number + c: 1000 # all other cell numbers increment by 1000 + +Another example specifies that only universe number 0 should be +modified: + + u 0: 10 # universe 0 will become number 10 + u: 0 # not necessary, while the trivial mapping is by default. + +Provide all cell numbers explicitly, assuming that the input file has +cells from 1 to 5 only: + + c 1: 12 + c 2: 14 + c 3: 16 + c 4: 18 + c 5: 20 + +Note that the info execution mode returns a list of all used ranges and +can be used as a template for the mapping file. + +Only lines beginning with c, s, u or m and having the semicolon : are +taken into account; all other lines are ignored. After the semicolon, +only one entry is taken into account. diff --git a/src/numjuggler/help/renum.md.txt b/src/numjuggler/help/renum.md.txt new file mode 100644 index 0000000..fe499a7 --- /dev/null +++ b/src/numjuggler/help/renum.md.txt @@ -0,0 +1,75 @@ + + +DESCRIPTION + + +The --mode renum is the default mode. Cells, surfaces, materials +transformations and universes are renamed according to the -c, -s, -m, +-u, -t or --map command line options. The original MCNP input file is +not modified, the input file with renamed elements is written to +standard output. + + + +OPTIONAL ARGUMENTS + + +In this mode, the cell, surface, material, transformation and/or +universe numbers are modified by adding terms, specified in the command +line. + +The following command line options are used to specify terms to be +added: + +- -c for cells +- -s for surfaces +- -m for materials +- -t for transformations +- -u for universes +- --map for any objects (see below) + +Each of the above flags except --map must be followed by an expression + that defines the term to be addedto the original number. This +expression can be one of the following: + +- -- simple integer value. This value is added to all cells, + materials, etc. +- i -- the character 'i'. In this case, the corresponding objects in + the input files are numbered with increasing numbers starting from 1 + (indexed). + +The --map flag must be followed by a file name of the map file. When +both --map and one of the -c, -s etc. flags is given, the latter is +used. + + + +INVOCATION EXAMPLES + + +All cells number in the input file input.orig are increased by 10. The +resulting input file is written to standard output and redirected to +input.new: + + >numjuggler -c 10 input.orig > input.new + >numjuggler --mode renum -c 10 input.orig > input.new + +Both variants are equal, since the --mode renum is the default one. +Similar, one can specify terms to be added to surfaces, materials and +transformations, repsectively with the -s, -m and -t command line +arguments: + + >numjuggler -s 5 -m 10 -t 100 input.orig > input.new + +In this example, surface numbers are increased by 5, material numbers +are increased by 10 (zero material remains unchanged) and all +transformation numbers are increased by 100. + +More complex renaming rules can be deifned in the map file. Here one can +specify different renaming rules for different cells or cell ranges. For +example: + + >numjuggler --map map.txt input.orig > input.new + +where map.txt is a text file, which format is described in details here: +map file format. diff --git a/src/numjuggler/main.py b/src/numjuggler/main.py index 332e75f..dbf5ca6 100644 --- a/src/numjuggler/main.py +++ b/src/numjuggler/main.py @@ -8,6 +8,7 @@ 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 @@ -123,7 +124,7 @@ def tr2str(pl, fmt1="{:12.9f}", fmte="{:16.8e}"): ) -def processing(args, cards: list[mp.Card], debuglog) -> None: +def processing(args, cards: list[mp.Card], debuglog: TextIO | None) -> StringIO: # define output string stream outstr = StringIO() @@ -1511,7 +1512,7 @@ def main(args=sys.argv[1:]): 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, f"help/{harg.h}.rst") + hlp = os.path.join(dir1, f"numjuggler/help/{harg.h}.md.txt") print(f"Reading help from {hlp}") print(open(hlp).read()) except Exception: 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/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 From 765cd181cd704e63c99041ada7a5b596a3d55cc1 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 26 Apr 2026 00:01:24 +0300 Subject: [PATCH 52/54] style: exclued data dirs from 'ruff' checks --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index f957754..129cd67 100644 --- a/ruff.toml +++ b/ruff.toml @@ -29,6 +29,7 @@ exclude = [ "node_modules", "notebooks", "site-packages", + "tests/data", "venv", "wrk", ] From 5f05f550d981935525078a70a81a03f5d098f905 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 26 Apr 2026 00:02:39 +0300 Subject: [PATCH 53/54] fix: LIKE cell --- help/cdens.md.txt | 34 ------------------ help/from_doc.sh | 8 ----- help/index.md.txt | 59 ------------------------------- help/info.md.txt | 24 ------------- help/map.md.txt | 74 --------------------------------------- help/renum.md.txt | 75 ---------------------------------------- src/numjuggler/parser.py | 63 +++++++++++++++++++++------------ tests/test_parser.py | 72 +++++++++++++++++++++++++++++++++----- 8 files changed, 105 insertions(+), 304 deletions(-) delete mode 100644 help/cdens.md.txt delete mode 100755 help/from_doc.sh delete mode 100644 help/index.md.txt delete mode 100644 help/info.md.txt delete mode 100644 help/map.md.txt delete mode 100644 help/renum.md.txt diff --git a/help/cdens.md.txt b/help/cdens.md.txt deleted file mode 100644 index 6e04296..0000000 --- a/help/cdens.md.txt +++ /dev/null @@ -1,34 +0,0 @@ - - -DESCRIPTION - - -Change cell densities according to the map file. - -In the map file one can specify coefficients to multiply the original -input file densities. The coefficients can be specified for particular -cells and/or materials (or their ranges -- see descritpion of the map -file format). - - -Rationale - -One of the approaches to get the fist guess for the mesh weight-window -through the model geometry is to decrease all material densities by -several orders of magnitude and perform particle transport to generate -the weight window mesh. - - -Invocation example - -in the following example, the map file is densities.txt, the original -input file is input.orig and the new input file is written to input.new: - - >numjuggler --mode cdens --map densities.txt input.orig > input.new - -The content of densities.txt: - - c 1 -- 10: 0.01 # In cells 1 to 10 multiply density by 0.01 - c 12: 0.1 # Multiply cell 12 density by 0.1 - - m 5: 1e-3 # For all cells filled with material 5 multiply density by 1e-3 diff --git a/help/from_doc.sh b/help/from_doc.sh deleted file mode 100755 index 79fbba9..0000000 --- a/help/from_doc.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# Generate txt files from md sources - -for md in $(ls ../docsrc/docs/*.md); do - pandoc -i "$md" --to plain -o $(basename "$md").txt -done - diff --git a/help/index.md.txt b/help/index.md.txt deleted file mode 100644 index 337ffbd..0000000 --- a/help/index.md.txt +++ /dev/null @@ -1,59 +0,0 @@ - - -NUMJUGGLER - - -About - -Numjuggler is a command line tool to perform specific tasks on the MCNP -input files. - -Originally, it was developed at INR-KIT to rename cells, surfaces and -materials in the MCNP input file. This task appeared often within the -framework of ITER nuclear analyses, where different parts of the MCNP -computational model were developed independently by different -organizations and than merged together to the model describing the whole -ITER facility. The merging of the model parts into a single MCNP input -file was often complicated by the use of the same cell, surface and -material numbers in the different model parts. - -Later, other features was added to numjuggler. Now it can be run in one -of the execution modes, each performing particular task. - -The original development was previously conducted in the inr-kit -repository. The author of numjuggler has its own fork at -travleev/numjuggler. - - -Installation - -TODO: the best way -- clone from travleev/numjuggler and install in the -"development mode" using pip: pip install -e .. - -TODO: try under windows/anaconda(?) - - -Invocation - -numjuggler is a command line tool written in Python. When properly -installed, it can be invoked in one of the following ways: - - >numjuggler --mode [arguments] input.txt > input.new - >python -m numjuggler --mode [arguments] input.txt > input.new - -These two invocation forms are equal. The command line arguments in -general contain the only necessary argument -- the original MCNP input -file (in the above example input.txt). The output is written to standard -output that can be redirected to a file. The execution mode is specified -after the --mode flag. The other optional arguments can contain -additional arguments relevant to the chosen mode. - -TODO: how to get interactive help. - - -List of execution modes - -- renum -- info -- cdens - diff --git a/help/info.md.txt b/help/info.md.txt deleted file mode 100644 index db1a6e1..0000000 --- a/help/info.md.txt +++ /dev/null @@ -1,24 +0,0 @@ - - -DESCRIPTION - - -The input file is analysed and ranges of used numbers for cells, -surfaces, ets. are written to the standard output. Note that the output -of this mode can be used (after necessary modifications) as the input to -the --map option. - -The first two columns specify type (cells, surfaces, etc.) and the range -of used numbers. The third column shows the amount of numbers in current -range, and the last column shows how many numbers left unused between -the current and previous ranges. - - - -INVOCATION EXAMPLE - - -The input file input.orig is analysed and the result is redirected to -info.txt: - - >numjuggler --mode info input.orig > info.txt diff --git a/help/map.md.txt b/help/map.md.txt deleted file mode 100644 index 67d73d1..0000000 --- a/help/map.md.txt +++ /dev/null @@ -1,74 +0,0 @@ - - -GENERAL INFO - - -Mapping rules can be specified in a separate file that is read when the ---map option is given. - -Different from the -c, -s, -m and -u command line arguments, in the map -file one can specify mapping rules for separate ranges. Ultimately, all -new cell, surface, material or universe names can be given explicitly. - - - -MAP FILE FORMAT - - -A map file consists of lines of the following form: - - t [range]: [+-]D - -The first entry, t, is the one-character type specification: c for -cells, s for surfaces, m for materials and u for universes. - -It is optionally followed by the range specifier that can be a single -number, for example 10, or two numbers delimited by two dashes, for -example 10 -- 25. If the range is omitted, the line defines the default -mapping, i.e. it is applied to elements not belonging to all other -ranges. - -The semicolon, :, delimits the range specification from the -specification of the mapping rule. It is followed by an integer, -optionally signed. This integer defines an increment to which numbers in -the current range are increased. The mapping in this case is N -> N+D, -where N is the original number from the range [N1, N2]. - -When an unsigned integer is given together with the range specification, -it is considered as the first element of the mapped range. Mapping in -this case is N -> N+D-N1, where N in [N1, N2]. - - - -MAP FILE EXAMPLES - - -In the example below, cells from 1 to 10, inclusive, are renamed to the -range from 11 to 20, cell 200 is renamed to 250 and all other cells -numbers are incremented by 1000: - - c 1 -- 10: +10 # explicit sign means increment - c 200: 250 # no sign means new number - c: 1000 # all other cell numbers increment by 1000 - -Another example specifies that only universe number 0 should be -modified: - - u 0: 10 # universe 0 will become number 10 - u: 0 # not necessary, while the trivial mapping is by default. - -Provide all cell numbers explicitly, assuming that the input file has -cells from 1 to 5 only: - - c 1: 12 - c 2: 14 - c 3: 16 - c 4: 18 - c 5: 20 - -Note that the info execution mode returns a list of all used ranges and -can be used as a template for the mapping file. - -Only lines beginning with c, s, u or m and having the semicolon : are -taken into account; all other lines are ignored. After the semicolon, -only one entry is taken into account. diff --git a/help/renum.md.txt b/help/renum.md.txt deleted file mode 100644 index fe499a7..0000000 --- a/help/renum.md.txt +++ /dev/null @@ -1,75 +0,0 @@ - - -DESCRIPTION - - -The --mode renum is the default mode. Cells, surfaces, materials -transformations and universes are renamed according to the -c, -s, -m, --u, -t or --map command line options. The original MCNP input file is -not modified, the input file with renamed elements is written to -standard output. - - - -OPTIONAL ARGUMENTS - - -In this mode, the cell, surface, material, transformation and/or -universe numbers are modified by adding terms, specified in the command -line. - -The following command line options are used to specify terms to be -added: - -- -c for cells -- -s for surfaces -- -m for materials -- -t for transformations -- -u for universes -- --map for any objects (see below) - -Each of the above flags except --map must be followed by an expression - that defines the term to be addedto the original number. This -expression can be one of the following: - -- -- simple integer value. This value is added to all cells, - materials, etc. -- i -- the character 'i'. In this case, the corresponding objects in - the input files are numbered with increasing numbers starting from 1 - (indexed). - -The --map flag must be followed by a file name of the map file. When -both --map and one of the -c, -s etc. flags is given, the latter is -used. - - - -INVOCATION EXAMPLES - - -All cells number in the input file input.orig are increased by 10. The -resulting input file is written to standard output and redirected to -input.new: - - >numjuggler -c 10 input.orig > input.new - >numjuggler --mode renum -c 10 input.orig > input.new - -Both variants are equal, since the --mode renum is the default one. -Similar, one can specify terms to be added to surfaces, materials and -transformations, repsectively with the -s, -m and -t command line -arguments: - - >numjuggler -s 5 -m 10 -t 100 input.orig > input.new - -In this example, surface numbers are increased by 5, material numbers -are increased by 10 (zero material remains unchanged) and all -transformation numbers are increased by 100. - -More complex renaming rules can be deifned in the map file. Here one can -specify different renaming rules for different cells or cell ranges. For -example: - - >numjuggler --map map.txt input.orig > input.new - -where map.txt is a text file, which format is described in details here: -map file format. diff --git a/src/numjuggler/parser.py b/src/numjuggler/parser.py index e54af02..df2163a 100644 --- a/src/numjuggler/parser.py +++ b/src/numjuggler/parser.py @@ -4,8 +4,7 @@ from __future__ import annotations -from numbers import Number -from typing import Iterable, Literal, TextIO +from typing import Iterable, Literal, TextIO, TYPE_CHECKING import re import warnings @@ -17,6 +16,9 @@ from numjuggler.utils import PartialFormatter +if TYPE_CHECKING: + from numbers import Number + # integer with one prefix character re_int = re.compile(r"\D{0,1}\d+") @@ -113,15 +115,28 @@ class Card: pos Input file line number, where the card was found debug, optional - Optional file-like object to write debug info, by default None + 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) - - self.dtype = None + 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__( @@ -160,25 +175,25 @@ def __init__( """Input file line number, where the card was found.""" self.debug = debug - """File-like object to write debug info""" + """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: 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: 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: list[tuple[int, str]] = [] + """List of (v, t) tuples, where v -- value and t -- its type.""" # some properties defined on demand # cell properties @@ -320,7 +335,7 @@ def _protect_nums(self): d["~"] = [] # float values in cells # Replace material density - if "like" not in inpt: + if "like" not in inpt.lower(): tokens = inpt.replace("=", " ").split() cell, mat, rho = tokens[:3] if int(mat) != 0: @@ -539,11 +554,14 @@ def get_imp( # there is no key in the input line. continue n = s[0].count("~") - res[key] = float(self.hidden["~"][n]) + hidden_item = float(self.hidden["~"][n]) + res[key] = hidden_item # change value only if necessary - if p in vals and res[key] != vals[p]: - res[key] = vals[p] - self.hidden["~"][n] = str(vals[p]) + 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() @@ -1195,12 +1213,13 @@ def is_blankline(l): return l.strip() == "" -def get_cards(inp, debug=None, preservetabs=False): +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 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) diff --git a/tests/test_parser.py b/tests/test_parser.py index 01ac696..51b128c 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,13 +1,22 @@ -from encodings import cp1251 -from enum import unique from io import StringIO -import locale from pathlib import Path from textwrap import dedent +from typing import Iterable import pytest -from numjuggler.parser import Card, CID, _split_data, are_close_lists, load_decode_buffer +from numjuggler.parser import ( + Card, + CID, + _split_data, + are_close_lists, + get_cards_from_input, + load_decode_buffer, +) + +HERE = Path(__file__).parent +DATA = HERE / "data" +assert DATA.is_dir() @pytest.fixture @@ -157,18 +166,27 @@ def test_card_get_f(): {"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( @@ -196,6 +214,7 @@ def test_card_remove_fill(card): ([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), @@ -224,6 +243,18 @@ def test_remove_spaces(card, expected): 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", [ @@ -288,11 +319,36 @@ def test_load_decode_buffer(cd_tmpdir, encoding): @pytest.mark.parametrize( "inp, expected", - [ - (["1 0 1\n", " 2 3 4\n", "\n", "m1 00101 1\n", "tr1 1 0 0 1\n"], None), - (["1 0 1\n", " 2 3 4\n", "\n", "m1 00101 1\n", "tr1\n 0 0 1\n"], None), + [ # 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 is not None + 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) From dc0a4d3f28026a0c62c1f4c1ec061ae885d273e2 Mon Sep 17 00:00:00 2001 From: dvp Date: Sun, 26 Apr 2026 00:12:47 +0300 Subject: [PATCH 54/54] build: remove unused MANIFEST.in --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) delete mode 100644 MANIFEST.in 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