diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 98fbaf0..600dd97 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -65,4 +65,33 @@ path: dist - name: Upload package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file + uses: pypa/gh-action-pypi-publish@release/v1 + + publish_sphinx_docs: + runs-on: ubuntu-latest + needs: release-pypi + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Install the project + run: uv sync --dev + + - name: Sphinx build + run: | + uv run sphinx-apidoc -o docs/source src/danom/ --separate + uv run sphinx-build docs/source docs/docs-build/html + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + publish_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/docs-build/html + force_orphan: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8cf6848..f8bbc58 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ scrap/ .coverage .DS_Store .benchmarks/ -.codspeed/ \ No newline at end of file +.codspeed/ +.hypothesis/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5a1ad2..2e55e6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,12 +39,7 @@ repos: - --ignore-dirs - .venv,target,.git,mock_data,.pytest_cache # - --ignore-hidden - - id: update-readme - name: update_readme - entry: uv run -m dev_tools.update_readme - language: python - pass_filenames: false - always_run: true + - id: update-cov name: update_cov entry: uv run -m dev_tools.update_cov diff --git a/README.md b/README.md index e0e34df..d46e6ea 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ Readability counts, abstracting common operations helps reduce cognitive complex Take this imperative pipeline of operations, it iterates once over the data, skipping the value if it fails one of the filter checks: ```python ->>> from danom import Stream >>> res = [] ... >>> for x in range(1_000_000): @@ -45,7 +44,6 @@ keyword breakdown: `{'for': 1, 'in': 1, 'if': 3, 'not': 3, 'continue': 3}` After a bit of experience with python you might use list comprehensions, however this is arguably _less_ clear and iterates multiple times over the same data ```python ->>> from danom import Stream >>> mul_three = [triple(x) for x in range(1_000_000)] >>> gt_ten = [x for x in mul_three if is_gt_ten(x)] >>> sub_two = [min_two(x) for x in gt_ten] @@ -86,300 +84,10 @@ keyword breakdown: `{}` The business logic is arguably much clearer like this. -### `Stream.async_collect` -```python -Stream.async_collect(self) -> 'tuple' -``` -Async version of collect. Note that all functions in the stream should be `Awaitable`. - -```python ->>> from danom import Stream ->>> Stream.from_iterable(file_paths).map(async_read_files).async_collect() -``` - -If there are no operations in the `Stream` then this will act as a normal collect. - -```python ->>> from danom import Stream ->>> Stream.from_iterable(file_paths).async_collect() -``` - - -### `Stream.collect` -```python -Stream.collect(self) -> 'tuple' -``` -Materialise the sequence from the `Stream`. - -```python ->>> from danom import Stream ->>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) ->>> stream.collect() == (1, 2, 3, 4) -``` - - -### `Stream.filter` -```python -Stream.filter(self, *fns: 'Callable[[T], bool]') -> 'Self' -``` -Filter the stream based on a predicate. Will return a new `Stream` with the modified sequence. - -```python ->>> from danom import Stream ->>> Stream.from_iterable([0, 1, 2, 3]).filter(lambda x: x % 2 == 0).collect() == (0, 2) -``` - -Simple functions can be passed in sequence to compose more complex filters -```python ->>> from danom import Stream ->>> Stream.from_iterable(range(20)).filter(divisible_by_3, divisible_by_5).collect() == (0, 15) -``` - - -### `Stream.fold` -```python -Stream.fold(self, initial: 'T', fn: 'Callable[[T], U]', *, workers: 'int' = 1, use_threads: 'bool' = False) -> 'U' -``` -Fold the results into a single value. `fold` triggers an action so will incur a `collect`. - -```python ->>> from danom import Stream ->>> Stream.from_iterable([1, 2, 3, 4]).fold(0, lambda a, b: a + b) == 10 ->>> Stream.from_iterable([[1], [2], [3], [4]]).fold([0], lambda a, b: a + b) == [0, 1, 2, 3, 4] ->>> Stream.from_iterable([1, 2, 3, 4]).fold(1, lambda a, b: a * b) == 24 -``` - -As `fold` triggers an action, the parameters will be forwarded to the `par_collect` call if the `workers` are greater than 1. -This will only effect the `collect` that is used to create the iterable to reduce, not the `fold` operation itself. -```python ->>> from danom import Stream ->>> Stream.from_iterable([1, 2, 3, 4]).map(some_expensive_fn).fold(0, add, workers=4, use_threads=False) -``` - - -### `Stream.from_iterable` -```python -Stream.from_iterable(it: 'Iterable') -> 'Self' -``` -This is the recommended way of creating a `Stream` object. - -```python ->>> from danom import Stream ->>> Stream.from_iterable([0, 1, 2, 3]).collect() == (0, 1, 2, 3) -``` - - -### `Stream.map` -```python -Stream.map(self, *fns: 'Callable[[T], U]') -> 'Self' -``` -Map a function to the elements in the `Stream`. Will return a new `Stream` with the modified sequence. - -```python ->>> from danom import Stream ->>> Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (1, 2, 3, 4) -``` - -This can also be mixed with `safe` functions: -```python ->>> from danom import Stream ->>> Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (Ok(inner=1), Ok(inner=2), Ok(inner=3), Ok(inner=4)) - ->>> @safe -... def two_div_value(x: float) -> float: -... return 2 / x - ->>> Stream.from_iterable([0, 1, 2, 4]).map(two_div_value).collect() == (Err(error=ZeroDivisionError('division by zero')), Ok(inner=2.0), Ok(inner=1.0), Ok(inner=0.5)) -``` - -Simple functions can be passed in sequence to compose more complex transformations -```python ->>> from danom import Stream ->>> Stream.from_iterable(range(5)).map(mul_two, add_one).collect() == (1, 3, 5, 7, 9) -``` - - -### `Stream.par_collect` -```python -Stream.par_collect(self, workers: 'int' = 4, *, use_threads: 'bool' = False) -> 'tuple' -``` -Materialise the sequence from the `Stream` in parallel. - -```python ->>> from danom import Stream ->>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) ->>> stream.par_collect() == (1, 2, 3, 4) -``` - -Use the `workers` arg to select the number of workers to use. Use `-1` to use all available processors (except 1). -Defaults to `4`. -```python ->>> from danom import Stream ->>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) ->>> stream.par_collect(workers=-1) == (1, 2, 3, 4) -``` - -For smaller I/O bound tasks use the `use_threads` flag as True. -If False the processing will use `ProcessPoolExecutor` else it will use `ThreadPoolExecutor`. -```python ->>> from danom import Stream ->>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) ->>> stream.par_collect(use_threads=True) == (1, 2, 3, 4) -``` - -Note that all operations should be pickle-able, for that reason `Stream` does not support lambdas or closures. - - -### `Stream.partition` -```python -Stream.partition(self, fn: 'Callable[[T], bool]', *, workers: 'int' = 1, use_threads: 'bool' = False) -> 'tuple[Self, Self]' -``` -Similar to `filter` except splits the True and False values. Will return a two new `Stream` with the partitioned sequences. - -Each partition is independently replayable. -```python ->>> from danom import Stream ->>> part1, part2 = Stream.from_iterable([0, 1, 2, 3]).partition(lambda x: x % 2 == 0) ->>> part1.collect() == (0, 2) ->>> part2.collect() == (1, 3) -``` - -As `partition` triggers an action, the parameters will be forwarded to the `par_collect` call if the `workers` are greater than 1. -```python ->>> from danom import Stream ->>> Stream.from_iterable(range(10)).map(add_one, add_one).partition(divisible_by_3, workers=4) ->>> part1.map(add_one).par_collect() == (4, 7, 10) ->>> part2.collect() == (2, 4, 5, 7, 8, 10, 11) -``` - - -### `Stream.tap` -```python -Stream.tap(self, *fns: 'Callable[[T], None]') -> 'Self' -``` -Tap the values to another process that returns None. Will return a new `Stream` with the modified sequence. - -The value passed to the tap function will be deep-copied to avoid any modification to the `Stream` item for downstream consumers. - -```python ->>> from danom import Stream ->>> Stream.from_iterable([0, 1, 2, 3]).tap(log_value).collect() == (0, 1, 2, 3) -``` - -Simple functions can be passed in sequence for multiple `tap` operations -```python ->>> from danom import Stream ->>> Stream.from_iterable([0, 1, 2, 3]).tap(log_value, print_value).collect() == (0, 1, 2, 3) -``` - -`tap` is useful for logging and similar actions without effecting the individual items, in this example eligible and dormant users are logged using `tap`: - -```python ->>> from danom import Stream ->>> active_users, inactive_users = ( -... Stream.from_iterable(users).map(parse_user_objects).partition(inactive_users) -... ) -... ->>> active_users.filter(eligible_for_promotion).tap(log_eligible_users).map( -... construct_promo_email, send_with_confirmation -... ).collect() -... ->>> inactive_users.tap(log_inactive_users).map( -... create_dormant_user_entry, add_to_dormant_table -... ).collect() -``` - - ## Result `Result` monad. Consists of `Ok` and `Err` for successful and failed operations respectively. -Each monad is a frozen instance to prevent further mutation. - - -### `Result.and_then` -```python -Result.and_then(self, func: 'Callable[[T], Result[U]]', **kwargs: 'dict') -> 'Result[U]' -``` -Pipe another function that returns a monad. For `Err` will return original error. - -```python ->>> from danom import Err, Ok ->>> Ok(1).and_then(add_one) == Ok(2) ->>> Ok(1).and_then(raise_err) == Err(error=TypeError()) ->>> Err(error=TypeError()).and_then(add_one) == Err(error=TypeError()) ->>> Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError()) -``` - - -### `Result.is_ok` -```python -Result.is_ok(self) -> 'bool' -``` -Returns `True` if the result type is `Ok`. -Returns `False` if the result type is `Err`. - -```python ->>> from danom import Err, Ok ->>> Ok().is_ok() == True ->>> Err().is_ok() == False -``` - - -### `Result.map` -```python -Result.map(self, func: 'Callable[[T], U]', **kwargs: 'dict') -> 'Result[U]' -``` -Pipe a pure function and wrap the return value with `Ok`. -Given an `Err` will return self. - -```python ->>> from danom import Err, Ok ->>> Ok(1).map(add_one) == Ok(2) ->>> Err(error=TypeError()).map(add_one) == Err(error=TypeError()) -``` - - -### `Result.match` -```python -Result.match(self, if_ok_func: 'Callable[[T], Result]', if_err_func: 'Callable[[T], Result]') -> 'Result' -``` -Map `ok_func` to `Ok` and `err_func` to `Err` - -```python ->>> from danom import Err, Ok ->>> Ok(1).match(add_one, mock_get_error_type) == Ok(inner=2) ->>> Ok("ok").match(double, mock_get_error_type) == Ok(inner='okok') ->>> Err(error=TypeError()).match(double, mock_get_error_type) == Ok(inner='TypeError') -``` - - -### `Result.unit` -```python -Result.unit(inner: 'T') -> 'Ok[T]' -``` -Unit method. Given an item of type `T` return `Ok(T)` - -```python ->>> from danom import Err, Ok, Result ->>> Result.unit(0) == Ok(inner=0) ->>> Ok.unit(0) == Ok(inner=0) ->>> Err.unit(0) == Ok(inner=0) -``` - - -### `Result.unwrap` -```python -Result.unwrap(self) -> 'T' -``` -Unwrap the `Ok` monad and get the inner value. -Unwrap the `Err` monad will raise the inner error. -```python ->>> from danom import Err, Ok ->>> Ok().unwrap() == None ->>> Ok(1).unwrap() == 1 ->>> Ok("ok").unwrap() == 'ok' ->>> Err(error=TypeError()).unwrap() raise TypeError(...) -``` +Each monad is a frozen instance to prevent further mutation. `Err` provides the `details` attribute which returns the full traceback as a list of dictionaries. ## safe @@ -549,6 +257,9 @@ Alternatively the map method can be used to return a new type instance with the │ ├── __init__.py │ ├── update_cov.py │ └── update_readme.py +├── docs +│ └── source +│ └── conf.py ├── src │ └── danom │ ├── __init__.py @@ -561,6 +272,7 @@ Alternatively the map method can be used to return a new type instance with the │ ├── __init__.py │ ├── conftest.py │ ├── test_api.py +│ ├── test_monad_laws.py │ ├── test_new_type.py │ ├── test_result.py │ ├── test_safe.py diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..2db8992 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,30 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = docs-build + +# Put it first so that "make" without argument is like "make help". +help: + @uv run $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +clean: + @uv run $(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O); + +deploy: + @if [ -d "docs-build/html" ]; then \ + echo "Removing existing worktree..."; \ + git worktree remove -f docs-build/html; \ + fi + @cd docs-build && git worktree add -f html gh-pages || true + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @uv run $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..42364b0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,41 @@ +# ruff: noqa +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import sys +from pathlib import Path + +project = "danom" +copyright = "2025, Ed Cuss" +author = "Ed Cuss" +release = "2025" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +PROJECT_ROOT_DIR = Path(__file__).parents[2].resolve() +sys.path.insert(0, str(PROJECT_ROOT_DIR / "src")) + + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.napoleon", + "sphinx.ext.githubpages", +] + +templates_path = ["_templates"] +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + + +html_theme = "classic" +html_static_path = ["_static"] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..7a3710f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,19 @@ +danom documentation +============================= + +.. automodule:: danom + :members: + :undoc-members: + :imported-members: + :show-inheritance: + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2270419..c517fad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "danom" -version = "0.8.0" +version = "0.8.1" description = "Functional streams and monads" readme = "README.md" license = "MIT" @@ -31,6 +31,7 @@ dev = [ "pytest-cov>=7.0.0", "repo-mapper-rs>=0.3.0", "ruff>=0.14.6", + "sphinx>=9.0.4", ] [tool.coverage.run] diff --git a/src/danom/_new_type.py b/src/danom/_new_type.py index 736e0ef..5d22aee 100644 --- a/src/danom/_new_type.py +++ b/src/danom/_new_type.py @@ -18,32 +18,36 @@ def new_type( # noqa: ANN202 ): """Create a NewType based on another type. - ```python - >>> from danom import new_type - >>> def is_positive(value): - ... return value >= 0 + .. code-block:: python - >>> ValidBalance = new_type("ValidBalance", float, validators=[is_positive]) - >>> ValidBalance("20") == ValidBalance(inner=20.0) - ``` + from danom import new_type + + def is_positive(value): + return value >= 0 + + ValidBalance = new_type("ValidBalance", float, validators=[is_positive]) + ValidBalance("20") == ValidBalance(inner=20.0) Unlike an inherited class, the type will not return `True` for an isinstance check. - ```python - >>> isinstance(ValidBalance(20.0), ValidBalance) == True - >>> isinstance(ValidBalance(20.0), float) == False - ``` + + .. code-block:: python + + isinstance(ValidBalance(20.0), ValidBalance) == True + isinstance(ValidBalance(20.0), float) == False The methods of the given `base_type` will be forwarded to the specialised type. Alternatively the map method can be used to return a new type instance with the transformation. - ```python - >>> from danom import new_type - >>> def has_len(email: str) -> bool: - ... return len(email) > 0 - - >>> Email = new_type("Email", str, validators=[has_len]) - >>> Email("some_email@domain.com").upper() == "SOME_EMAIL@DOMAIN.COM" - >>> Email("some_email@domain.com").map(str.upper) == Email(inner='SOME_EMAIL@DOMAIN.COM') - ``` + + .. code-block:: python + + from danom import new_type + + def has_len(email: str) -> bool: + return len(email) > 0 + + Email = new_type("Email", str, validators=[has_len]) + Email("some_email@domain.com").upper() == "SOME_EMAIL@DOMAIN.COM" + Email("some_email@domain.com").map(str.upper) == Email(inner='SOME_EMAIL@DOMAIN.COM') """ kwargs = _callables_to_kwargs(base_type, validators, converters) diff --git a/src/danom/_result.py b/src/danom/_result.py index 805ce24..5953e7b 100644 --- a/src/danom/_result.py +++ b/src/danom/_result.py @@ -22,12 +22,13 @@ class Result[T, U](ABC): def unit(cls, inner: T) -> Ok[T]: """Unit method. Given an item of type `T` return `Ok(T)` - ```python - >>> from danom import Err, Ok, Result - >>> Result.unit(0) == Ok(inner=0) - >>> Ok.unit(0) == Ok(inner=0) - >>> Err.unit(0) == Ok(inner=0) - ``` + .. code-block:: python + + from danom import Err, Ok, Result + + Result.unit(0) == Ok(inner=0) + Ok.unit(0) == Ok(inner=0) + Err.unit(0) == Ok(inner=0) """ return Ok(inner) @@ -36,11 +37,12 @@ def is_ok(self) -> bool: """Returns `True` if the result type is `Ok`. Returns `False` if the result type is `Err`. - ```python - >>> from danom import Err, Ok - >>> Ok().is_ok() == True - >>> Err().is_ok() == False - ``` + .. code-block:: python + + from danom import Err, Ok + + Ok().is_ok() == True + Err().is_ok() == False """ ... @@ -49,11 +51,12 @@ def map(self, func: Callable[[T], U], **kwargs: dict) -> Result[U]: """Pipe a pure function and wrap the return value with `Ok`. Given an `Err` will return self. - ```python - >>> from danom import Err, Ok - >>> Ok(1).map(add_one) == Ok(2) - >>> Err(error=TypeError()).map(add_one) == Err(error=TypeError()) - ``` + .. code-block:: python + + from danom import Err, Ok + + Ok(1).map(add_one) == Ok(2) + Err(error=TypeError()).map(add_one) == Err(error=TypeError()) """ ... @@ -61,13 +64,14 @@ def map(self, func: Callable[[T], U], **kwargs: dict) -> Result[U]: def and_then(self, func: Callable[[T], Result[U]], **kwargs: dict) -> Result[U]: """Pipe another function that returns a monad. For `Err` will return original error. - ```python - >>> from danom import Err, Ok - >>> Ok(1).and_then(add_one) == Ok(2) - >>> Ok(1).and_then(raise_err) == Err(error=TypeError()) - >>> Err(error=TypeError()).and_then(add_one) == Err(error=TypeError()) - >>> Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError()) - ``` + .. code-block:: python + + from danom import Err, Ok + + Ok(1).and_then(add_one) == Ok(2) + Ok(1).and_then(raise_err) == Err(error=TypeError()) + Err(error=TypeError()).and_then(add_one) == Err(error=TypeError()) + Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError()) """ ... @@ -75,13 +79,15 @@ def and_then(self, func: Callable[[T], Result[U]], **kwargs: dict) -> Result[U]: def unwrap(self) -> T: """Unwrap the `Ok` monad and get the inner value. Unwrap the `Err` monad will raise the inner error. - ```python - >>> from danom import Err, Ok - >>> Ok().unwrap() == None - >>> Ok(1).unwrap() == 1 - >>> Ok("ok").unwrap() == 'ok' - >>> Err(error=TypeError()).unwrap() raise TypeError(...) - ``` + + .. code-block:: python + + from danom import Err, Ok + + Ok().unwrap() == None + Ok(1).unwrap() == 1 + Ok("ok").unwrap() == 'ok' + Err(error=TypeError()).unwrap() raise TypeError(...) """ ... @@ -91,12 +97,13 @@ def match( ) -> Result: """Map `ok_func` to `Ok` and `err_func` to `Err` - ```python - >>> from danom import Err, Ok - >>> Ok(1).match(add_one, mock_get_error_type) == Ok(inner=2) - >>> Ok("ok").match(double, mock_get_error_type) == Ok(inner='okok') - >>> Err(error=TypeError()).match(double, mock_get_error_type) == Ok(inner='TypeError') - ``` + .. code-block:: python + + from danom import Err, Ok + + Ok(1).match(add_one, mock_get_error_type) == Ok(inner=2) + Ok("ok").match(double, mock_get_error_type) == Ok(inner='okok') + Err(error=TypeError()).match(double, mock_get_error_type) == Ok(inner='TypeError') """ ... @@ -104,7 +111,7 @@ def __class_getitem__(cls, _params: tuple) -> Self: return cls -@attrs.define(frozen=True) +@attrs.define(frozen=True, hash=True) class Ok[T, U](Result): inner: Any = attrs.field(default=None) @@ -126,7 +133,7 @@ def match( return if_ok_func(self.inner) -@attrs.define(frozen=True) +@attrs.define(frozen=True, hash=True) class Err[T, U, E](Result): error: E | Exception | None = attrs.field(default=None) input_args: tuple[T] = attrs.field(default=None, repr=False) @@ -170,3 +177,13 @@ def match( self, _if_ok_func: Callable[[T], Result], if_err_func: Callable[[T], Result] ) -> Result: return if_err_func(self.error) + + def __eq__(self, other: Err) -> bool: + return all( + ( + isinstance(other, Err), + type(self.error) is type(other.error), + str(self.error) == str(other.error), + self.input_args == other.input_args, + ) + ) diff --git a/src/danom/_safe.py b/src/danom/_safe.py index e5ced41..2c76462 100644 --- a/src/danom/_safe.py +++ b/src/danom/_safe.py @@ -13,14 +13,15 @@ def safe[T, U](func: Callable[[T], U]) -> Callable[[T], Result]: """Decorator for functions that wraps the function in a try except returns `Ok` on success else `Err`. - ```python - >>> from danom import safe - >>> @safe - ... def add_one(a: int) -> int: - ... return a + 1 - - >>> add_one(1) == Ok(inner=2) - ``` + .. code-block:: python + + from danom import safe + + @safe + def add_one(a: int) -> int: + return a + 1 + + add_one(1) == Ok(inner=2) """ @functools.wraps(func) @@ -36,18 +37,19 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result: def safe_method[T, U, E](func: Callable[[T], U]) -> Callable[[T], Result[U, E]]: """The same as `safe` except it forwards on the `self` of the class instance to the wrapped function. - ```python - >>> from danom import safe_method - >>> class Adder: - ... def __init__(self, result: int = 0) -> None: - ... self.result = result - ... - ... @safe_method - ... def add_one(self, a: int) -> int: - ... return self.result + 1 - - >>> Adder.add_one(1) == Ok(inner=1) - ``` + .. code-block:: python + + from danom import safe_method + + class Adder: + def __init__(self, result: int = 0) -> None: + self.result = result + + @safe_method + def add_one(self, a: int) -> int: + return self.result + 1 + + Adder.add_one(1) == Ok(inner=1) """ @functools.wraps(func) diff --git a/src/danom/_stream.py b/src/danom/_stream.py index 7b3fcf5..c6457b5 100644 --- a/src/danom/_stream.py +++ b/src/danom/_stream.py @@ -53,35 +53,39 @@ async def async_collect(self) -> tuple: ... class Stream(_BaseStream): """An immutable lazy iterator with functional operations. - #### Why bother? + Why bother? + ----------- + Readability counts, abstracting common operations helps reduce cognitive complexity when reading code. - #### Comparison + Comparison + ---------- + Take this imperative pipeline of operations, it iterates once over the data, skipping the value if it fails one of the filter checks: - ```python - >>> from danom import Stream - >>> res = [] - ... - >>> for x in range(1_000_000): - ... item = triple(x) - ... - ... if not is_gt_ten(item): - ... continue - ... - ... item = min_two(item) - ... - ... if not is_even_num(item): - ... continue - ... - ... item = square(item) - ... - ... if not is_lt_400(item): - ... continue - ... - ... res.append(item) - >>> [100, 256] - ``` + .. code-block:: python + + res = [] + + for x in range(1_000_000): + item = triple(x) + + if not is_gt_ten(item): + continue + + item = min_two(item) + + if not is_even_num(item): + continue + + item = square(item) + + if not is_lt_400(item): + continue + + res.append(item) + [100, 256] + number of tokens: `90` number of keywords: `11` @@ -89,16 +93,17 @@ class Stream(_BaseStream): keyword breakdown: `{'for': 1, 'in': 1, 'if': 3, 'not': 3, 'continue': 3}` After a bit of experience with python you might use list comprehensions, however this is arguably _less_ clear and iterates multiple times over the same data - ```python - >>> from danom import Stream - >>> mul_three = [triple(x) for x in range(1_000_000)] - >>> gt_ten = [x for x in mul_three if is_gt_ten(x)] - >>> sub_two = [min_two(x) for x in gt_ten] - >>> is_even = [x for x in sub_two if is_even_num(x)] - >>> squared = [square(x) for x in is_even] - >>> lt_400 = [x for x in squared if is_lt_400(x)] - >>> [100, 256] - ``` + + .. code-block:: python + + mul_three = [triple(x) for x in range(1_000_000)] + gt_ten = [x for x in mul_three if is_gt_ten(x)] + sub_two = [min_two(x) for x in gt_ten] + is_even = [x for x in sub_two if is_even_num(x)] + squared = [square(x) for x in is_even] + lt_400 = [x for x in squared if is_lt_400(x)] + [100, 256] + number of tokens: `92` number of keywords: `15` @@ -108,20 +113,23 @@ class Stream(_BaseStream): This still has a lot of tokens that the developer has to read to understand the code. The extra keywords add noise that cloud the actual transformations. Using a `Stream` results in this: - ```python - >>> from danom import Stream - >>> ( - ... Stream.from_iterable(range(1_000_000)) - ... .map(triple) - ... .filter(is_gt_ten) - ... .map(min_two) - ... .filter(is_even_num) - ... .map(square) - ... .filter(is_lt_400) - ... .collect() - ... ) - >>> (100, 256) - ``` + + .. code-block:: python + + from danom import Stream + + ( + Stream.from_iterable(range(1_000_000)) + .map(triple) + .filter(is_gt_ten) + .map(min_two) + .filter(is_even_num) + .map(square) + .filter(is_lt_400) + .collect() + ) + (100, 256) + number of tokens: `60` number of keywords: `0` @@ -135,10 +143,12 @@ class Stream(_BaseStream): def from_iterable(cls, it: Iterable) -> Self: """This is the recommended way of creating a `Stream` object. - ```python - >>> from danom import Stream - >>> Stream.from_iterable([0, 1, 2, 3]).collect() == (0, 1, 2, 3) - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([0, 1, 2, 3]).collect() == (0, 1, 2, 3) + """ if not isinstance(it, Iterable): it = [it] @@ -147,28 +157,35 @@ def from_iterable(cls, it: Iterable) -> Self: def map[T, U](self, *fns: Callable[[T], U]) -> Self: """Map a function to the elements in the `Stream`. Will return a new `Stream` with the modified sequence. - ```python - >>> from danom import Stream - >>> Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (1, 2, 3, 4) - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (1, 2, 3, 4) This can also be mixed with `safe` functions: - ```python - >>> from danom import Stream - >>> Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (Ok(inner=1), Ok(inner=2), Ok(inner=3), Ok(inner=4)) - >>> @safe - ... def two_div_value(x: float) -> float: - ... return 2 / x + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (Ok(inner=1), Ok(inner=2), Ok(inner=3), Ok(inner=4)) + + @safe + def two_div_value(x: float) -> float: + return 2 / x + + Stream.from_iterable([0, 1, 2, 4]).map(two_div_value).collect() == (Err(error=ZeroDivisionError('division by zero')), Ok(inner=2.0), Ok(inner=1.0), Ok(inner=0.5)) - >>> Stream.from_iterable([0, 1, 2, 4]).map(two_div_value).collect() == (Err(error=ZeroDivisionError('division by zero')), Ok(inner=2.0), Ok(inner=1.0), Ok(inner=0.5)) - ``` Simple functions can be passed in sequence to compose more complex transformations - ```python - >>> from danom import Stream - >>> Stream.from_iterable(range(5)).map(mul_two, add_one).collect() == (1, 3, 5, 7, 9) - ``` + + .. code-block:: python + + from danom import Stream + + Stream.from_iterable(range(5)).map(mul_two, add_one).collect() == (1, 3, 5, 7, 9) + """ plan = (*self.ops, *tuple((_OpType.MAP, fn) for fn in fns)) return Stream(seq=self.seq, ops=plan) @@ -176,16 +193,21 @@ def map[T, U](self, *fns: Callable[[T], U]) -> Self: def filter[T](self, *fns: Callable[[T], bool]) -> Self: """Filter the stream based on a predicate. Will return a new `Stream` with the modified sequence. - ```python - >>> from danom import Stream - >>> Stream.from_iterable([0, 1, 2, 3]).filter(lambda x: x % 2 == 0).collect() == (0, 2) - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([0, 1, 2, 3]).filter(lambda x: x % 2 == 0).collect() == (0, 2) + Simple functions can be passed in sequence to compose more complex filters - ```python - >>> from danom import Stream - >>> Stream.from_iterable(range(20)).filter(divisible_by_3, divisible_by_5).collect() == (0, 15) - ``` + + .. code-block:: python + + from danom import Stream + + Stream.from_iterable(range(20)).filter(divisible_by_3, divisible_by_5).collect() == (0, 15) + """ plan = (*self.ops, *tuple((_OpType.FILTER, fn) for fn in fns)) return Stream(seq=self.seq, ops=plan) @@ -195,33 +217,40 @@ def tap[T](self, *fns: Callable[[T], None]) -> Self: The value passed to the tap function will be deep-copied to avoid any modification to the `Stream` item for downstream consumers. - ```python - >>> from danom import Stream - >>> Stream.from_iterable([0, 1, 2, 3]).tap(log_value).collect() == (0, 1, 2, 3) - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([0, 1, 2, 3]).tap(log_value).collect() == (0, 1, 2, 3) + Simple functions can be passed in sequence for multiple `tap` operations - ```python - >>> from danom import Stream - >>> Stream.from_iterable([0, 1, 2, 3]).tap(log_value, print_value).collect() == (0, 1, 2, 3) - ``` + + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([0, 1, 2, 3]).tap(log_value, print_value).collect() == (0, 1, 2, 3) + `tap` is useful for logging and similar actions without effecting the individual items, in this example eligible and dormant users are logged using `tap`: - ```python - >>> from danom import Stream - >>> active_users, inactive_users = ( - ... Stream.from_iterable(users).map(parse_user_objects).partition(inactive_users) - ... ) - ... - >>> active_users.filter(eligible_for_promotion).tap(log_eligible_users).map( - ... construct_promo_email, send_with_confirmation - ... ).collect() - ... - >>> inactive_users.tap(log_inactive_users).map( - ... create_dormant_user_entry, add_to_dormant_table - ... ).collect() - ``` + .. code-block:: python + + from danom import Stream + + active_users, inactive_users = ( + Stream.from_iterable(users).map(parse_user_objects).partition(inactive_users) + ) + + active_users.filter(eligible_for_promotion).tap(log_eligible_users).map( + construct_promo_email, send_with_confirmation + ).collect() + + inactive_users.tap(log_inactive_users).map( + create_dormant_user_entry, add_to_dormant_table + ).collect() + """ plan = (*self.ops, *tuple((_OpType.TAP, fn) for fn in fns)) return Stream(seq=self.seq, ops=plan) @@ -232,20 +261,26 @@ def partition[T]( """Similar to `filter` except splits the True and False values. Will return a two new `Stream` with the partitioned sequences. Each partition is independently replayable. - ```python - >>> from danom import Stream - >>> part1, part2 = Stream.from_iterable([0, 1, 2, 3]).partition(lambda x: x % 2 == 0) - >>> part1.collect() == (0, 2) - >>> part2.collect() == (1, 3) - ``` + + .. code-block:: python + + from danom import Stream + + part1, part2 = Stream.from_iterable([0, 1, 2, 3]).partition(lambda x: x % 2 == 0) + part1.collect() == (0, 2) + part2.collect() == (1, 3) + As `partition` triggers an action, the parameters will be forwarded to the `par_collect` call if the `workers` are greater than 1. - ```python - >>> from danom import Stream - >>> Stream.from_iterable(range(10)).map(add_one, add_one).partition(divisible_by_3, workers=4) - >>> part1.map(add_one).par_collect() == (4, 7, 10) - >>> part2.collect() == (2, 4, 5, 7, 8, 10, 11) - ``` + + .. code-block:: python + + from danom import Stream + + Stream.from_iterable(range(10)).map(add_one, add_one).partition(divisible_by_3, workers=4) + part1.map(add_one).par_collect() == (4, 7, 10) + part2.collect() == (2, 4, 5, 7, 8, 10, 11) + """ # have to materialise to be able to replay each side independently if workers > 1: @@ -262,19 +297,24 @@ def fold[T, U]( ) -> U: """Fold the results into a single value. `fold` triggers an action so will incur a `collect`. - ```python - >>> from danom import Stream - >>> Stream.from_iterable([1, 2, 3, 4]).fold(0, lambda a, b: a + b) == 10 - >>> Stream.from_iterable([[1], [2], [3], [4]]).fold([0], lambda a, b: a + b) == [0, 1, 2, 3, 4] - >>> Stream.from_iterable([1, 2, 3, 4]).fold(1, lambda a, b: a * b) == 24 - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([1, 2, 3, 4]).fold(0, lambda a, b: a + b) == 10 + Stream.from_iterable([[1], [2], [3], [4]]).fold([0], lambda a, b: a + b) == [0, 1, 2, 3, 4] + Stream.from_iterable([1, 2, 3, 4]).fold(1, lambda a, b: a * b) == 24 + As `fold` triggers an action, the parameters will be forwarded to the `par_collect` call if the `workers` are greater than 1. This will only effect the `collect` that is used to create the iterable to reduce, not the `fold` operation itself. - ```python - >>> from danom import Stream - >>> Stream.from_iterable([1, 2, 3, 4]).map(some_expensive_fn).fold(0, add, workers=4, use_threads=False) - ``` + + .. code-block:: python + + from danom import Stream + + Stream.from_iterable([1, 2, 3, 4]).map(some_expensive_fn).fold(0, add, workers=4, use_threads=False) + """ if workers > 1: return reduce(fn, self.par_collect(workers=workers, use_threads=use_threads), initial) @@ -283,11 +323,13 @@ def fold[T, U]( def collect(self) -> tuple: """Materialise the sequence from the `Stream`. - ```python - >>> from danom import Stream - >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) - >>> stream.collect() == (1, 2, 3, 4) - ``` + .. code-block:: python + + from danom import Stream + + stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) + stream.collect() == (1, 2, 3, 4) + """ return tuple( elem for x in self.seq if (elem := _apply_fns(x, self.ops)) != _Nothing.NOTHING @@ -296,27 +338,32 @@ def collect(self) -> tuple: def par_collect(self, workers: int = 4, *, use_threads: bool = False) -> tuple: """Materialise the sequence from the `Stream` in parallel. - ```python - >>> from danom import Stream - >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) - >>> stream.par_collect() == (1, 2, 3, 4) - ``` + .. code-block:: python + + from danom import Stream + + stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) + stream.par_collect() == (1, 2, 3, 4) Use the `workers` arg to select the number of workers to use. Use `-1` to use all available processors (except 1). Defaults to `4`. - ```python - >>> from danom import Stream - >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) - >>> stream.par_collect(workers=-1) == (1, 2, 3, 4) - ``` + + .. code-block:: python + + from danom import Stream + + stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) + stream.par_collect(workers=-1) == (1, 2, 3, 4) For smaller I/O bound tasks use the `use_threads` flag as True. If False the processing will use `ProcessPoolExecutor` else it will use `ThreadPoolExecutor`. - ```python - >>> from danom import Stream - >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) - >>> stream.par_collect(use_threads=True) == (1, 2, 3, 4) - ``` + + .. code-block:: python + + from danom import Stream + + stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one) + stream.par_collect(use_threads=True) == (1, 2, 3, 4) Note that all operations should be pickle-able, for that reason `Stream` does not support lambdas or closures. """ @@ -333,17 +380,20 @@ def par_collect(self, workers: int = 4, *, use_threads: bool = False) -> tuple: async def async_collect(self) -> tuple: """Async version of collect. Note that all functions in the stream should be `Awaitable`. - ```python - >>> from danom import Stream - >>> Stream.from_iterable(file_paths).map(async_read_files).async_collect() - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable(file_paths).map(async_read_files).async_collect() If there are no operations in the `Stream` then this will act as a normal collect. - ```python - >>> from danom import Stream - >>> Stream.from_iterable(file_paths).async_collect() - ``` + .. code-block:: python + + from danom import Stream + + Stream.from_iterable(file_paths).async_collect() + """ if not self.ops: return self.collect() diff --git a/src/danom/_utils.py b/src/danom/_utils.py index 83258f7..8a81ba1 100644 --- a/src/danom/_utils.py +++ b/src/danom/_utils.py @@ -20,13 +20,14 @@ def compose[T, U](*fns: Callable[[T], U]) -> Callable[[T], U]: The functions will be called in sequence with the result of one being used as the input for the next. - ```python - >>> from danom import compose - >>> add_two = compose(add_one, add_one) - >>> add_two(0) == 2 - >>> add_two_is_even = compose(add_one, add_one, is_even) - >>> add_two_is_even(0) == True - ``` + .. code-block:: python + + from danom import compose + + add_two = compose(add_one, add_one) + add_two(0) == 2 + add_two_is_even = compose(add_one, add_one, is_even) + add_two_is_even(0) == True """ return _Compose(fns) @@ -42,11 +43,12 @@ def __call__(self, initial: T) -> bool: def all_of[T](*fns: Callable[[T], bool]) -> Callable[[T], bool]: """True if all of the given functions return True. - ```python - >>> from danom import all_of - >>> is_valid_user = all_of(is_subscribed, is_active, has_2fa) - >>> is_valid_user(user) == True - ``` + .. code-block:: python + + from danom import all_of + + is_valid_user = all_of(is_subscribed, is_active, has_2fa) + is_valid_user(user) == True """ return _AllOf(fns) @@ -62,11 +64,12 @@ def __call__(self, initial: T) -> bool: def any_of[T](*fns: Callable[[T], bool]) -> Callable[[T], bool]: """True if any of the given functions return True. - ```python - >>> from danom import any_of - >>> is_eligible = any_of(has_coupon, is_vip, is_staff) - >>> is_eligible(user) == True - ``` + .. code-block:: python + + from danom import any_of + + is_eligible = any_of(has_coupon, is_vip, is_staff) + is_eligible(user) == True """ return _AnyOf(fns) @@ -74,11 +77,12 @@ def any_of[T](*fns: Callable[[T], bool]) -> Callable[[T], bool]: def none_of[T](*fns: Callable[[T], bool]) -> Callable[[T], bool]: """True if none of the given functions return True. - ```python - >>> from danom import none_of - >>> is_valid = none_of(is_empty, exceeds_size_limit, contains_unsupported_format) - >>> is_valid(submission) == True - ``` + .. code-block:: python + + from danom import none_of + + is_valid = none_of(is_empty, exceeds_size_limit, contains_unsupported_format) + is_valid(submission) == True """ return compose(_AnyOf(fns), not_) @@ -86,12 +90,13 @@ def none_of[T](*fns: Callable[[T], bool]) -> Callable[[T], bool]: def identity[T](x: T) -> T: """Basic identity function. - ```python - >>> from danom import identity - >>> identity("abc") == "abc" - >>> identity(1) == 1 - >>> identity(ComplexDataType(a=1, b=2, c=3)) == ComplexDataType(a=1, b=2, c=3) - ``` + .. code-block:: python + + from danom import identity + + identity("abc") == "abc" + identity(1) == 1 + identity(ComplexDataType(a=1, b=2, c=3)) == ComplexDataType(a=1, b=2, c=3) """ return x @@ -99,10 +104,11 @@ def identity[T](x: T) -> T: def invert[T](func: Callable[[T], bool]) -> Callable[[T], bool]: """Invert a boolean function so it returns False where it would've returned True. - ```python - >>> from danom import invert - >>> invert(has_len)("abc") == False - >>> invert(has_len)("") == True - ``` + .. code-block:: python + + from danom import invert + + invert(has_len)("abc") == False + invert(has_len)("") == True """ return compose(func, not_) diff --git a/tests/conftest.py b/tests/conftest.py index 3b972d7..f7a799d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import asyncio from pathlib import Path from typing import Any, Self from src.danom import safe, safe_method -from src.danom._result import Result +from src.danom._result import Err, Ok, Result REPO_ROOT = Path(__file__).parents[1] @@ -24,7 +26,7 @@ def double[T](x: T) -> T: return x * 2 -def divisible_by_3[T](x: float) -> bool: +def divisible_by_3(x: float) -> bool: return x % 3 == 0 @@ -50,9 +52,17 @@ def safe_add(a: int, b: int) -> Result[int, Exception]: return a + b +def safe_add_one(x: float | str) -> Result[float | str, TypeError]: + if isinstance(x, (int, float)): + return Ok(x + 1) + if isinstance(x, str): + return Ok(x + "1") + return Err(TypeError(f"unsupported type: {type(x)}")) + + @safe -def safe_add_one[T](x: T) -> T: - return x + 1 +def safe_double[T](x: T) -> T: + return x * 2 @safe diff --git a/tests/test_api.py b/tests/test_api.py index 77873e4..b8cc674 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,6 @@ import pytest from src.danom import Err, Ok, Result -from tests.conftest import safe_add, safe_add_one @pytest.mark.parametrize( @@ -13,23 +12,3 @@ ) def test_subclass(sub_cls, base_cls): assert isinstance(sub_cls, base_cls) - - -def test_monadic_left_identity(): - assert Result.unit(0).and_then(safe_add, b=1) == safe_add(0, 1) - - -@pytest.mark.parametrize(("monad"), [pytest.param(Ok(1)), pytest.param(Err())]) -def test_monadic_right_identity(monad): - assert monad.and_then(Result.unit) == monad - - -@pytest.mark.parametrize( - ("monad", "f", "g"), - [ - pytest.param(Ok(0), safe_add_one, safe_add_one), - pytest.param(Err(), safe_add_one, safe_add_one), - ], -) -def test_monadic_associativity(monad, f, g): - assert monad.and_then(f).and_then(g) == monad.and_then(lambda x: f(x).and_then(g)) diff --git a/tests/test_monad_laws.py b/tests/test_monad_laws.py new file mode 100644 index 0000000..3d3ecaf --- /dev/null +++ b/tests/test_monad_laws.py @@ -0,0 +1,30 @@ +from hypothesis import given +from hypothesis import strategies as st + +from src.danom import Err, Result +from tests.conftest import safe_add, safe_add_one, safe_double + + +def test_monadic_left_identity(): + assert Result.unit(0).and_then(safe_add, b=1) == safe_add(0, 1) + + +results = st.one_of( + st.integers().map(Result.unit), + st.text().map(Result.unit), + st.floats(allow_nan=False, allow_infinity=False).map(Result.unit), + st.just(Err()), +) + + +@given(results) +def test_monadic_right_identity(monad): + assert monad.and_then(Result.unit) == monad + + +safe_fns = st.sampled_from([safe_double, safe_add_one]) + + +@given(monad=results, f=safe_fns, g=safe_fns) +def test_monadic_associativity(monad, f, g): + assert monad.and_then(f).and_then(g) == monad.and_then(lambda x: f(x).and_then(g)) diff --git a/uv.lock b/uv.lock index a5de28e..17e5659 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "appnope" version = "0.1.4" @@ -29,6 +38,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + [[package]] name = "cffi" version = "2.0.0" @@ -95,6 +122,63 @@ 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 = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -189,7 +273,7 @@ wheels = [ [[package]] name = "danom" -version = "0.8.0" +version = "0.8.1" source = { editable = "." } dependencies = [ { name = "attrs" }, @@ -206,6 +290,7 @@ dev = [ { name = "pytest-cov" }, { name = "repo-mapper-rs" }, { name = "ruff" }, + { name = "sphinx" }, ] [package.metadata] @@ -222,6 +307,7 @@ dev = [ { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "repo-mapper-rs", specifier = ">=0.3.0" }, { name = "ruff", specifier = ">=0.14.6" }, + { name = "sphinx", specifier = ">=9.0.4" }, ] [[package]] @@ -263,6 +349,15 @@ 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 = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -302,6 +397,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] +[[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 = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -380,6 +493,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] +[[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 = "jupyter-client" version = "8.6.3" @@ -421,6 +546,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[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/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 = "matplotlib-inline" version = "0.2.1" @@ -778,6 +966,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/14/dedd1b243fdc2a16b291d9b3a664ee593bff997f1dcb5eaffedf5b776ade/repo_mapper_rs-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:c0815eff543c46a051b93247e98bad42213333d47415367b4d0716b4a01e88ef", size = 787923, upload-time = "2025-08-13T20:28:37.508Z" }, ] +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -791,6 +994,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "ruff" version = "0.14.6" @@ -826,6 +1038,15 @@ 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 = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -835,6 +1056,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -886,6 +1189,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + [[package]] name = "virtualenv" version = "20.35.4"