From 873ef4ab00e1d8322a06b91446bd23e90e43b449 Mon Sep 17 00:00:00 2001 From: Andreas Eisenbarth Date: Fri, 28 Jun 2024 13:14:54 +0200 Subject: [PATCH] Initial commit --- .gitignore | 58 + .pdm-python | 1 + .pre-commit-config.yaml | 62 + LICENSE | 29 + MANIFEST.in | 15 + README.md | 27 + docs/conf.py | 60 + docs/development.md | 26 + docs/examples.ipynb | 192 ++ docs/images/example1-jupyter.png | Bin 0 -> 17669 bytes docs/images/example1-myst-nb-json.png | Bin 0 -> 17950 bytes docs/images/example1-myst-nb.png | Bin 0 -> 15080 bytes docs/index.md | 57 + docs/static/custom.css | 12 + myst_nb_json/__init__.py | 201 ++ myst_nb_json/py.typed | 0 myst_nb_json/resources/myst-nb-json.css | 58 + myst_nb_json/resources/myst-nb-json.js | 28 + pdm.lock | 1739 +++++++++++++++++ pyproject.toml | 119 ++ tests/__init__.py | 0 tests/conftest.py | 168 ++ tests/notebooks/json_output.ipynb | 54 + tests/test_interactivity.py | 73 + tests/test_mime_plugin.py | 101 + tests/test_myst_nb.py | 17 + .../test_myst_nb/test_render_json_output.xml | 113 ++ 27 files changed, 3210 insertions(+) create mode 100644 .gitignore create mode 100644 .pdm-python create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 docs/conf.py create mode 100644 docs/development.md create mode 100644 docs/examples.ipynb create mode 100644 docs/images/example1-jupyter.png create mode 100644 docs/images/example1-myst-nb-json.png create mode 100644 docs/images/example1-myst-nb.png create mode 100644 docs/index.md create mode 100644 docs/static/custom.css create mode 100644 myst_nb_json/__init__.py create mode 100644 myst_nb_json/py.typed create mode 100644 myst_nb_json/resources/myst-nb-json.css create mode 100644 myst_nb_json/resources/myst-nb-json.js create mode 100644 pdm.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/notebooks/json_output.ipynb create mode 100644 tests/test_interactivity.py create mode 100644 tests/test_mime_plugin.py create mode 100644 tests/test_myst_nb.py create mode 100644 tests/test_myst_nb/test_render_json_output.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1c3669 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +dist/ +eggs/ +sdist/ +wheels/ +*.egg-info/ +MANIFEST + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +coverage.xml +*.cover +*.py,cover +.pytest_cache/ + +# Sphinx documentation +docs/_build/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +jupyter_execute diff --git a/.pdm-python b/.pdm-python new file mode 100644 index 0000000..28f4ba0 --- /dev/null +++ b/.pdm-python @@ -0,0 +1 @@ +/home/andreas/software/miniconda3/envs/myst-nb-json/bin/python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bf0b925 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,62 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-json + files: \.(json)$ + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/python/black + rev: 22.3.0 + hooks: + - id: black-jupyter + args: ["--config=pyproject.toml"] + pass_filenames: true + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--settings-path=pyproject.toml"] + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.14 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-beautysh + - mdformat-black + - mdformat-frontmatter + - mdformat-toc + - mdformat-gfm + args: ["--number", "--wrap", "keep"] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.0 + hooks: + - id: mypy + args: ["--install-types", "--non-interactive"] + additional_dependencies: ["bokeh>=3.3.4", "sphinx>=7.2.6"] + exclude: > + (?x)^( + tests/.*\.py + )$ + - repo: https://github.com/pdm-project/pdm + rev: 2.15.4 + hooks: + - id: pdm-lock-check + - repo: https://github.com/hadialqattan/pycln + rev: v1.3.2 + hooks: + - id: pycln + args: ["--config=pyproject.toml"] + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.4.10 + hooks: + # Run the linter. + - id: ruff + types_or: [python, pyi, jupyter] + # Run the formatter. + - id: ruff-format + types_or: [python, pyi, jupyter] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..59bb17e --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD License + +Copyright (c) 2024, Andreas Eisenbarth +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..eac4cdd --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,15 @@ +exclude docs +recursive-exclude docs * +exclude tests +recursive-exclude tests *.js *.css +include myst_nb_json/resources/*.js +include myst_nb_json/resources/*.css + +exclude .pre-commit-config.yaml +exclude .readthedocs.yaml + +include LICENSE +include README.md +include AUTHORS.md +include CONTRIBUTING.md +include src/myst_nb_json/py.typed diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a4b017 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# MyST-NB JSON + +A MIME-type plugin for rendering JSON output from Jupyter notebooks to HTML + +______________________________________________________________________ + +Outputs from Jupyter notebook code cells usually contain representations in one or multiple MIME +types (image, text, ...). IPython provides multiple built-in output types, but not all of them +have a representation for HTML, and fall back to a stringified version of the Python object. + +This is the case for the `application/json` type: + +It is nicely rendered in Jupyter because Jupyter includes a +[JSON renderer](https://github.com/jupyterlab/jupyterlab/tree/7909745d075aceb0cf1099ad53a3174e92b575ae/packages/json-extension), +but MyST-NB can only use IPython's built-ins which display as ``. + +## Jupyter Lab + +![Screenshot of json_dict in Jupyter](./docs/images/example1-jupyter.png) + +## MyST-NB without plugin + +![Screenshot of json_dict in HTML by MyST-NB](./docs/images/example1-myst-nb.png) + +## MyST-NB with myst-nb-json plugin + +![Screenshot of json_dict in HTML with myst-nb-json](./docs/images/example1-myst-nb-json.png) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..ba7339e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,60 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +from datetime import datetime +from importlib import metadata + +# Custom constants (no Sphinx configuration) + +REPOSITORY_URL = "https://github.com/aeisenbarth/myst-nb-json" + +# -- Project information --------------------------------------------------------------------------- + +project = "MyST-NB JSON" +author = "Andreas Eisenbarth" + +copyright = f"{datetime.now():%Y}, Andreas Eisenbarth" + +release = version = metadata.version("myst-nb-json") + +# -- General configuration ------------------------------------------------------------------------- + +extensions = ["myst_nb"] + +# List of glob-style patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +# Myst-nb creates a folder "jupyter_execute" and copies notebooks there, which causes recursive +# inclusion, so exclude it. See https://github.com/executablebooks/MyST-NB/issues/129 +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "jupyter_execute"] + +# -- Options for HTML output ----------------------------------------------------------------------- + +html_theme = "sphinx_book_theme" + +html_theme_options = dict( + use_repository_button=True, + use_download_button=False, + repository_provider="gitlab", + repository_url=REPOSITORY_URL, + repository_branch="main", + navigation_with_keys=False, # https://github.com/pydata/pydata-sphinx-theme/issues/1492 + collapse_navbar=False, + show_navbar_depth=1, +) + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["static"] + +# Define custom CSS rules +html_css_files = ["custom.css"] + +# If true, “Created using Sphinx” is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# -- myst_parser / myst_nb ------------------------------------------------------------------------- diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..95907e6 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,26 @@ +# Development + +1. Clone the repository: + + ```shell + git clone https://github.com/aeisenbarth/myst-nb-json.git + ``` + +2. Install dependencies: + + ```shell + pip install pdm + pdm install + ``` + +3. Run tests: + + ```shell + pdm test + ``` + +4. Build documentation: + + ```shell + pdm docs + ``` diff --git a/docs/examples.ipynb b/docs/examples.ipynb new file mode 100644 index 0000000..aba590b --- /dev/null +++ b/docs/examples.ipynb @@ -0,0 +1,192 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c095fe2d196b3295", + "metadata": {}, + "source": [ + "# Examples\n", + "\n", + "A JSON-like Python dictionary renders the full Python object as `text/plain` (notice the single quotes)." + ] + }, + { + "cell_type": "code", + "id": "5fa6e511ebe825d8", + "metadata": {}, + "source": [ + "json_dict = {\n", + " \"key1\": \"value1\",\n", + " \"key2\": {\"key21\": 42, \"key22\": True, \"key23\": None},\n", + " \"key3\": [\"a\", \"b\"],\n", + "}\n", + "json_dict" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "33f24ef62f7364da", + "metadata": {}, + "source": [ + "It can be wrapped in an output type, which adds the MIME type `application/json`.\n", + "\n", + "```python\n", + "from IPython.display import JSON\n", + "\n", + "JSON(json_dict)\n", + "```\n", + "\n", + "However, there are two issues with this:\n", + "\n", + "- Now `text/plain` is ``\n", + "\n", + "- `application/json`'s data is the original JSON, and it is left up to renderers how to represent it.\n", + "\n", + "Jupyter Lab renders the following:\n", + "\n", + "![Screenshot of json_dict in Jupyter](./images/example1-jupyter.png)\n", + "\n", + "Whereas MyST-NB renders:\n", + "\n", + "![Screenshot of json_dict in HTML by MyST-NB](./images/example1-myst-nb.png)\n", + "\n", + "With `myst-nb-json`, you should get an interactive rendering similar to Jupyter Lab:\n", + "\n", + "![Screenshot of json_dict in HTML with myst-nb-json](./images/example1-myst-nb-json.png)" + ] + }, + { + "cell_type": "code", + "id": "23be0ec2f8ceb03a", + "metadata": {}, + "source": [ + "from IPython.display import JSON\n", + "\n", + "JSON(json_dict)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "cde3f57f5dd64638", + "metadata": {}, + "source": [ + "With the `expanded` parameter, you can control how much to show by default:" + ] + }, + { + "cell_type": "code", + "id": "7659e360f770ceab", + "metadata": {}, + "source": [ + "JSON(json_dict, expanded=True)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "32536237d491e70f", + "metadata": {}, + "source": [ + "With the `root` parameter, you can control the display name:" + ] + }, + { + "cell_type": "code", + "id": "3f85070c626b8d8f", + "metadata": {}, + "source": [ + "JSON(json_dict, root=\"json_dict\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "ed652ffb9714f6ae", + "metadata": {}, + "source": "This is also nice for **Pydantic** models! By default, they are rendered as plain text:" + }, + { + "cell_type": "code", + "id": "cfb51d59e132581d", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "source": [ + "from typing import Optional\n", + "from pydantic import BaseModel\n", + "\n", + "\n", + "class Model2(BaseModel):\n", + " key21: int\n", + " key22: bool\n", + " key23: Optional[str]\n", + "\n", + "\n", + "class ExampleModel(BaseModel):\n", + " key1: str\n", + " key2: Model2\n", + " key3: list[str]" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "b75220850f01881", + "metadata": {}, + "source": [ + "example_model = ExampleModel(**json_dict)\n", + "example_model" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "ca7cd38c9589f5d5", + "metadata": {}, + "source": [ + "JSON(example_model.model_dump(mode=\"json\"))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "58c778876c5b43d6", + "metadata": {}, + "source": [ + "As a bonus, MyST-NB has been designed that despite the simplified rendering, when you **select and copy** the text, it will include valid JSON." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/images/example1-jupyter.png b/docs/images/example1-jupyter.png new file mode 100644 index 0000000000000000000000000000000000000000..d7987a14f3980a2ab87be5fd720f41019984a3d8 GIT binary patch literal 17669 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 Pr@gA8Ca~oBtM~s0tvW&l literal 0 HcmV?d00001 diff --git a/docs/images/example1-myst-nb-json.png b/docs/images/example1-myst-nb-json.png new file mode 100644 index 0000000000000000000000000000000000000000..318901564e037ddc01de06e73dfd5757e4de4c34 GIT binary patch literal 17950 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 Jm3i~={{Rs|3F-g< literal 0 HcmV?d00001 diff --git a/docs/images/example1-myst-nb.png b/docs/images/example1-myst-nb.png new file mode 100644 index 0000000000000000000000000000000000000000..e757cebd027c98f9b1a17870e80e979c23d4cee3 GIT binary patch literal 15080 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..49fa753 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,57 @@ +# MyST-NB JSON + +A MIME-type plugin for rendering JSON output from Jupyter notebooks to HTML + +______________________________________________________________________ + +```{toctree} +:hidden: + +examples +development +``` + +Outputs from Jupyter notebook code cells usually contain representations in one or multiple MIME +types (image, text…). IPython provides multiple built-in output types, but not all of them +have a representation for HTML, and fall back to a stringified version of the Python object. + +This is the case for the `application/json` type: + +It is nicely rendered in Jupyter because Jupyter includes a +[JSON renderer](https://github.com/jupyterlab/jupyterlab/tree/7909745d075aceb0cf1099ad53a3174e92b575ae/packages/json-extension), +but MyST-NB can only use IPython's built-ins which display as ``. + +## Installation + +Install the Python package: + +```shell +pip install myst-nb-json +``` + +It registers itself in MyST-NB via entry-points, so it is ready to use. + +## Usage + +1. Wrap output from code cells in the IPython `JSON` class. + + ```python + from IPython.display import JSON + + JSON({"key": "value"}) + ``` + +2. When building documentation with Sphinx, the JSON output will be rendered as interactive tree, + instead of displaying plain text. + + ```shell + sphinx-build -b html docs/ docs/_build + ``` + + Or just the notebook with: + + ```shell + mystnb-docutils-html example.ipynb > example.html + ``` + +For more details, see [examples](./examples.ipynb). diff --git a/docs/static/custom.css b/docs/static/custom.css new file mode 100644 index 0000000..09578d8 --- /dev/null +++ b/docs/static/custom.css @@ -0,0 +1,12 @@ +/* + myst-nb renders cells as div block elements with border. However, output cells are separated by a + gap from their input cells, which makes the notebook more visually cluttered. Remove the gap. +*/ +.cell .cell_input + .cell_output { + margin-top: 0; +} +.cell .cell_input + .cell_output .output { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/myst_nb_json/__init__.py b/myst_nb_json/__init__.py new file mode 100644 index 0000000..a6e2330 --- /dev/null +++ b/myst_nb_json/__init__.py @@ -0,0 +1,201 @@ +""" +MimeRenderPlugin for MyST-NB for rendering IPython JSON display type to HTML +""" + +__all__ = ["JsonMimeRenderPlugin"] +__version__ = "0.1.0" + +import json +from collections.abc import Generator, Mapping, Sequence +from functools import cached_property +from importlib.resources import files +from typing import Union, cast + +from docutils import nodes +from myst_nb.core.render import MimeData, MimeRenderPlugin, NbElementRenderer + +JAVASCRIPT_FILE_NAME = "myst-nb-json.js" +CSS_FILE_NAME = "myst-nb-json.css" +CLS_COLLAPSIBLE = "myst-nb-json-collapsible" +CLS_COLLAPSED = "myst-nb-json-collapsed" +CLS_HIDDEN = "myst-nb-json-hidden" +CLS_KEY = "myst-nb-json-key" +CLS_UNSELECTABLE = "myst-nb-json-unselectable" +CLS_VALUE = "myst-nb-json-value" + +ScalarJsonType = Union[str, int, float, bool, None] + +JsonType = Union[Mapping[str, "JsonType"], Sequence["JsonType"], ScalarJsonType] + + +class JsonMimeRenderPlugin(MimeRenderPlugin): + mime_priority_overrides = [("*", "application/json", 1)] + + @staticmethod + def handle_mime( + renderer: NbElementRenderer, data: MimeData, inline: bool + ) -> Union[None, list[nodes.Element]]: + """ + A function that renders a mime type to docutils nodes, or returns None to reject + + Args: + renderer: A class for rendering notebook elements + data: The Notebook output data + inline: Whether the MIME type is displayed inline, e.g. in an interactive Jupyter + notebook. In the context of MyST-NB, content is rendered to a file, so the renderer + should not accept inline content. + + Returns: + A list of docutils nodes, or None if this renderer cannot handle the data + """ + try: + if not inline and data.mime_type == "application/json": + root = data.output_metadata.get(data.mime_type, {}).get("root", "root") + expanded = data.output_metadata.get(data.mime_type, {}).get("expanded", True) + plugin = JsonMimeRenderPlugin() + html = plugin.html(data.content, root=root, expanded=expanded) + return [nodes.raw(text=html, format="html", classes=["output", "text_html"])] + except Exception as e: + import traceback + + print(e) + traceback.print_exc() + return None + + def html(self, jsonable: JsonType, root: str = "root", expanded: bool = False) -> str: + """ + Generate an HTML string for a JSON object + + Args: + jsonable: A JSON-like Python object + root: Optional name to display as key for the JSON value + expanded: Whether to initialize the JSON tree collapsed or expanded + + Returns: + The component's HTML as string + """ + return "".join(self.component(jsonable, root=root, expanded=expanded)) + + def component( + self, jsonable: JsonType, root: str = "root", expanded: bool = False + ) -> Generator[str, None, None]: + """ + Yield HTML fragment strings for a JSON object + + Args: + jsonable: A JSON-like Python object + root: Optional name to display as key for the JSON value + expanded: Whether to initialize the JSON tree collapsed or expanded + + Yields: + HTML strings + """ + yield "
" + yield f"""
  • """ + yield from self.key(root, collapsible=is_nested(jsonable), selectable=False) + yield from self.value(jsonable, expanded=expanded) + yield "
" + yield self.style + yield self.script + yield "
" + + @cached_property + def script(self) -> str: + script_str = (files(__package__) / "resources" / JAVASCRIPT_FILE_NAME).read_text() + return f"" + + @cached_property + def style(self) -> str: + css_str = (files(__package__) / "resources" / CSS_FILE_NAME).read_text() + return f"""""" + + def value( + self, value: JsonType, expanded: bool = False, with_comma: bool = False + ) -> Generator[str, None, None]: + if isinstance(value, Sequence) and not isinstance(value, str) and len(value) > 0: + yield from self.list_value(value, expanded=expanded, with_comma=with_comma) + elif isinstance(value, Mapping) and len(value) > 0: + yield from self.dict_value(value, expanded=expanded, with_comma=with_comma) + else: + yield from self.scalar_value(cast(ScalarJsonType, value), with_comma=with_comma) + + def key( + self, key: Union[str, int], collapsible: bool = False, selectable: bool = True + ) -> Generator[str, None, None]: + classes = CLS_KEY + if collapsible: + classes += " " + CLS_COLLAPSIBLE + if not selectable: + classes += " " + CLS_UNSELECTABLE + yield f"""{self.quote}{key}{self.quote}{self.colon}""" + + def list_value( + self, value: Sequence, expanded: bool = False, with_comma: bool = False + ) -> Generator[str, None, None]: + yield f"""
{self.bracket_open}
    """ + last_index = len(value) - 1 + for index, val in enumerate(value): + is_last = index == last_index + yield f"""
  • """ + # yield from self.key(index, collapsible=is_nested(val), selectable=False) + yield from self.value(val, with_comma=not is_last) + # It would be nicer to add the comma here, but if value yields a block element, + # the comma would be an orphan in the next line below the value. To have it in the same + # line, it needs to be inside the value's block. + yield "
  • " + yield f"
{self.bracket_close}{self.comma if with_comma else ''}
" + + def dict_value( + self, value: Mapping, expanded: bool = False, with_comma: bool = False + ) -> Generator[str, None, None]: + yield f"""
{self.curly_open}
    """ + last_index = len(value.items()) - 1 + for index, (key, val) in enumerate(value.items()): + is_last = index == last_index + yield f"""
  • """ + yield from self.key(key, collapsible=is_nested(val)) + yield from self.value(val, with_comma=not is_last) + # It would be nicer to add the comma here. + yield "
  • " + yield f"
{self.curly_close}{self.comma if with_comma else ''}
" + + def scalar_value( + self, value: ScalarJsonType, with_comma: bool = False + ) -> Generator[str, None, None]: + value_str = json.dumps(value) + yield f"""{value_str}{self.comma if with_comma else ''}""" + + @cached_property + def quote(self): + return f""""""" + + @cached_property + def colon(self): + return f""": """ + + @cached_property + def comma(self): + return f""", """ + + @cached_property + def curly_open(self): + return f"""{""" + + @cached_property + def curly_close(self): + return f"""}""" + + @cached_property + def bracket_open(self): + return f"""[""" + + @cached_property + def bracket_close(self): + return f"""]""" + + +def is_nested(jsonable: JsonType) -> bool: + return ( + (isinstance(jsonable, Sequence) and not isinstance(jsonable, str)) + or isinstance(jsonable, Mapping) + ) and len(jsonable) > 0 diff --git a/myst_nb_json/py.typed b/myst_nb_json/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/myst_nb_json/resources/myst-nb-json.css b/myst_nb_json/resources/myst-nb-json.css new file mode 100644 index 0000000..fd362f4 --- /dev/null +++ b/myst_nb_json/resources/myst-nb-json.css @@ -0,0 +1,58 @@ +div.myst-nb-json-value > ul { + display: inline-block; + list-style: none; + margin: 0; + padding: 0; +} + +li { + padding-left: 1.5em; +} + +li.myst-nb-json-collapsed > div.myst-nb-json-value { + display: none; +} + +span.myst-nb-json-key { + color: #008000; + display: inline-block; + font-weight: bold; + margin: 0px 0.5em 0px 0px; + padding: 0; + user-select: all; +} + +span.myst-nb-json-key.myst-nb-json-collapsible::before { + color: black; + content: "▼"; + display: inline-block; + font-size: 0.75em; + margin-left: -1.25em; + position: relative; + top: -0.15em; + transform: rotateZ(0deg); + transform-origin: 40% 50% 0; + transition: 150ms; + user-select: none; + width: 1.25em; +} + +li.myst-nb-json-collapsed > span.myst-nb-json-key.myst-nb-json-collapsible::before { + transform: rotateZ(-90deg); +} + +span.myst-nb-json-value { + color: #ba2121; + text-indent: -0.5em; + user-select: all; +} + +span.myst-nb-json-hidden { + display: inline-block; + opacity: 0; + width: 0; +} + +.myst-nb-json-unselectable { + user-select: none !important; +} diff --git a/myst_nb_json/resources/myst-nb-json.js b/myst_nb_json/resources/myst-nb-json.js new file mode 100644 index 0000000..409ddf9 --- /dev/null +++ b/myst_nb_json/resources/myst-nb-json.js @@ -0,0 +1,28 @@ +// Script to be embedded inside the component's root HTMLElement +(function (component) { + document.addEventListener('DOMContentLoaded', () => { + const CLASS_KEY = "myst-nb-json-key"; + const CLASS_COLLAPSIBLE = "myst-nb-json-collapsible"; + const CLASS_COLLAPSED = "myst-nb-json-collapsed"; + + let toggleable = Array.from(component.getElementsByTagName("li")); + toggleable.forEach((li) => { + // Find list item elements that contain a nested, collapsible value. + if (li.getElementsByClassName(CLASS_COLLAPSIBLE).length == 0) return; + let key = li.getElementsByClassName(CLASS_KEY)[0]; + let toggleFn = function() { + let isCollapsed = li.classList.contains(CLASS_COLLAPSED); + if (isCollapsed) { + li.classList.remove(CLASS_COLLAPSED); + } else { + li.classList.add(CLASS_COLLAPSED); + } + } + // Make the key element interactive to toggle the list item's collapsed state. + key.addEventListener("click", toggleFn); + key.addEventListener("keydown", (event) => { + if (event.code === "ArrowLeft" || event.code === "ArrowRight" || event.code === "Space") toggleFn() + }); + }); + }); +})(document.currentScript.parentElement); diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..c83e9be --- /dev/null +++ b/pdm.lock @@ -0,0 +1,1739 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev", "test", "docs"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.2" +content_hash = "sha256:a32c00ffd185cb5f73fc2b00c520dcf4ddc329c9db3bf732e7c358b37b321412" + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +requires_python = ">=3.9" +summary = "A collection of accessible pygments styles" +groups = ["docs"] +dependencies = [ + "pygments>=1.5", +] +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +requires_python = ">=3.9" +summary = "A light, configurable Sphinx theme" +groups = ["default", "docs", "test"] +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] + +[[package]] +name = "appnope" +version = "0.1.4" +requires_python = ">=3.6" +summary = "Disable App Nap on macOS >= 10.9" +groups = ["default", "docs"] +marker = "platform_system == \"Darwin\"" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "asttokens" +version = "2.4.1" +summary = "Annotate AST trees with source code positions" +groups = ["default", "docs", "test"] +dependencies = [ + "six>=1.12.0", +] +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +groups = ["default", "docs", "test"] +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[[package]] +name = "babel" +version = "2.15.0" +requires_python = ">=3.8" +summary = "Internationalization utilities" +groups = ["default", "docs", "test"] +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +requires_python = ">=3.6.0" +summary = "Screen-scraping library" +groups = ["docs"] +dependencies = [ + "soupsieve>1.2", +] +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[[package]] +name = "certifi" +version = "2024.6.2" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default", "docs", "test"] +files = [ + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default", "docs", "test"] +marker = "os_name == \"nt\" or implementation_name == \"pypy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Validate configuration and produce human readable error messages." +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default", "docs", "test"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default", "docs"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "docs", "test"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "comm" +version = "0.2.2" +requires_python = ">=3.8" +summary = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +groups = ["default", "docs"] +dependencies = [ + "traitlets>=4", +] +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[[package]] +name = "coverage" +version = "7.5.4" +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["test"] +files = [ + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, +] + +[[package]] +name = "coverage" +version = "7.5.4" +extras = ["toml"] +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.5.4", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, +] + +[[package]] +name = "cython" +version = "3.0.10" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +summary = "The Cython compiler for writing C extensions in the Python language." +groups = ["test"] +files = [ + {file = "Cython-3.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:077b61ee789e48700e25d4a16daa4258b8e65167136e457174df400cf9b4feab"}, + {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f1f8bba9d8f37c0cffc934792b4ac7c42d0891077127c11deebe9fa0a0f7e4"}, + {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd"}, + {file = "Cython-3.0.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d10fc9aa82e5e53a0b7fd118f9771199cddac8feb4a6d8350b7d4109085aa775"}, + {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f610964ab252a83e573a427e28b103e2f1dd3c23bee54f32319f9e73c3c5499"}, + {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c9c4c4f3ab8f8c02817b0e16e8fa7b8cc880f76e9b63fe9c010e60c1a6c2b13"}, + {file = "Cython-3.0.10-cp39-cp39-win32.whl", hash = "sha256:0bac3ccdd4e03924028220c62ae3529e17efa8ca7e9df9330de95de02f582b26"}, + {file = "Cython-3.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:81f356c1c8c0885b8435bfc468025f545c5d764aa9c75ab662616dd1193c331e"}, + {file = "Cython-3.0.10-py2.py3-none-any.whl", hash = "sha256:fcbb679c0b43514d591577fd0d20021c55c240ca9ccafbdb82d3fb95e5edfee2"}, + {file = "Cython-3.0.10.tar.gz", hash = "sha256:dcc96739331fb854dcf503f94607576cfe8488066c61ca50dfd55836f132de99"}, +] + +[[package]] +name = "debugpy" +version = "1.8.2" +requires_python = ">=3.8" +summary = "An implementation of the Debug Adapter Protocol for Python" +groups = ["default", "docs"] +files = [ + {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, + {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, + {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, + {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, + {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, + {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +requires_python = ">=3.5" +summary = "Decorators for Humans" +groups = ["default", "docs", "test"] +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "XML bomb protection for Python stdlib modules" +groups = ["test"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +summary = "Distribution utilities" +groups = ["dev"] +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "docutils" +version = "0.21.2" +requires_python = ">=3.9" +summary = "Docutils -- Python Documentation Utilities" +groups = ["default", "docs", "test"] +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "docs", "test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[[package]] +name = "executing" +version = "2.0.1" +requires_python = ">=3.5" +summary = "Get the currently executing AST node of a frame, and other information" +groups = ["default", "docs", "test"] +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[[package]] +name = "fastjsonschema" +version = "2.20.0" +summary = "Fastest Python implementation of JSON schema" +groups = ["default", "docs"] +files = [ + {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, + {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, +] + +[[package]] +name = "filelock" +version = "3.15.4" +requires_python = ">=3.8" +summary = "A platform independent file lock." +groups = ["dev"] +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[[package]] +name = "greenlet" +version = "3.0.3" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +groups = ["default", "docs"] +marker = "(platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"" +files = [ + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["test"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "identify" +version = "2.5.36" +requires_python = ">=3.8" +summary = "File identification library for Python" +groups = ["dev"] +files = [ + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, +] + +[[package]] +name = "idna" +version = "3.7" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "docs", "test"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Getting image size from png/jpeg/jpeg2000/gif file" +groups = ["default", "docs", "test"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.0.0" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +groups = ["default", "docs", "test"] +dependencies = [ + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipykernel" +version = "6.29.4" +requires_python = ">=3.8" +summary = "IPython Kernel for Jupyter" +groups = ["default", "docs"] +dependencies = [ + "appnope; platform_system == \"Darwin\"", + "comm>=0.1.1", + "debugpy>=1.6.5", + "ipython>=7.23.1", + "jupyter-client>=6.1.12", + "jupyter-core!=5.0.*,>=4.12", + "matplotlib-inline>=0.1", + "nest-asyncio", + "packaging", + "psutil", + "pyzmq>=24", + "tornado>=6.1", + "traitlets>=5.4.0", +] +files = [ + {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, + {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, +] + +[[package]] +name = "ipython" +version = "8.18.1" +requires_python = ">=3.9" +summary = "IPython: Productive Interactive Computing" +groups = ["default", "docs", "test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "decorator", + "exceptiongroup; python_version < \"3.11\"", + "jedi>=0.16", + "matplotlib-inline", + "pexpect>4.3; sys_platform != \"win32\"", + "prompt-toolkit<3.1.0,>=3.0.41", + "pygments>=2.4.0", + "stack-data", + "traitlets>=5", + "typing-extensions; python_version < \"3.10\"", +] +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[[package]] +name = "jedi" +version = "0.19.1" +requires_python = ">=3.6" +summary = "An autocompletion tool for Python that can be used for text editors." +groups = ["default", "docs", "test"] +dependencies = [ + "parso<0.9.0,>=0.8.3", +] +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["default", "docs", "test"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "jsonschema" +version = "4.22.0" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +groups = ["default", "docs"] +dependencies = [ + "attrs>=22.2.0", + "jsonschema-specifications>=2023.03.6", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, + {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +requires_python = ">=3.8" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +groups = ["default", "docs"] +dependencies = [ + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[[package]] +name = "jupyter-cache" +version = "1.0.0" +requires_python = ">=3.9" +summary = "A defined interface for working with a cache of jupyter notebooks." +groups = ["default", "docs"] +dependencies = [ + "attrs", + "click", + "importlib-metadata", + "nbclient>=0.2", + "nbformat", + "pyyaml", + "sqlalchemy<3,>=1.3.12", + "tabulate", +] +files = [ + {file = "jupyter_cache-1.0.0-py3-none-any.whl", hash = "sha256:594b1c4e29b488b36547e12477645f489dbdc62cc939b2408df5679f79245078"}, + {file = "jupyter_cache-1.0.0.tar.gz", hash = "sha256:d0fa7d7533cd5798198d8889318269a8c1382ed3b22f622c09a9356521f48687"}, +] + +[[package]] +name = "jupyter-client" +version = "8.6.2" +requires_python = ">=3.8" +summary = "Jupyter protocol implementation and client libraries" +groups = ["default", "docs"] +dependencies = [ + "importlib-metadata>=4.8.3; python_version < \"3.10\"", + "jupyter-core!=5.0.*,>=4.12", + "python-dateutil>=2.8.2", + "pyzmq>=23.0", + "tornado>=6.2", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, + {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +requires_python = ">=3.8" +summary = "Jupyter core package. A base package on which Jupyter projects rely." +groups = ["default", "docs"] +dependencies = [ + "platformdirs>=2.5", + "pywin32>=300; sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default", "docs"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["default", "docs", "test"] +files = [ + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +requires_python = ">=3.8" +summary = "Inline Matplotlib backend for Jupyter" +groups = ["default", "docs", "test"] +dependencies = [ + "traitlets", +] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.1" +requires_python = ">=3.8" +summary = "Collection of plugins for markdown-it-py" +groups = ["default", "docs"] +dependencies = [ + "markdown-it-py<4.0.0,>=1.0.0", +] +files = [ + {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, + {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default", "docs"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy" +version = "1.10.1" +requires_python = ">=3.8" +summary = "Optional static typing for Python" +groups = ["dev"] +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.1.0", +] +files = [ + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "myst-nb" +version = "1.1.1" +requires_python = ">=3.9" +summary = "A Jupyter Notebook Sphinx reader built on top of the MyST markdown parser." +groups = ["default", "docs"] +dependencies = [ + "importlib-metadata", + "ipykernel", + "ipython", + "jupyter-cache>=0.5", + "myst-parser>=1.0.0", + "nbclient", + "nbformat>=5.0", + "pyyaml", + "sphinx>=5", + "typing-extensions", +] +files = [ + {file = "myst_nb-1.1.1-py3-none-any.whl", hash = "sha256:8b8f9085287d948eef46cb3764aafc21915e0e981882b8c742719f5b1a84c36f"}, + {file = "myst_nb-1.1.1.tar.gz", hash = "sha256:74227c11f76d03494f43b7788659b161b94f4dedef230a2912412bc8c3c9e553"}, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +requires_python = ">=3.8" +summary = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," +groups = ["default", "docs"] +dependencies = [ + "docutils<0.22,>=0.18", + "jinja2", + "markdown-it-py~=3.0", + "mdit-py-plugins~=0.4", + "pyyaml", + "sphinx<8,>=6", +] +files = [ + {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, + {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, +] + +[[package]] +name = "nbclient" +version = "0.10.0" +requires_python = ">=3.8.0" +summary = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +groups = ["default", "docs"] +dependencies = [ + "jupyter-client>=6.1.12", + "jupyter-core!=5.0.*,>=4.12", + "nbformat>=5.1", + "traitlets>=5.4", +] +files = [ + {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, + {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +requires_python = ">=3.8" +summary = "The Jupyter Notebook format" +groups = ["default", "docs"] +dependencies = [ + "fastjsonschema>=2.15", + "jsonschema>=2.6", + "jupyter-core!=5.0.*,>=4.12", + "traitlets>=5.1", +] +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +requires_python = ">=3.5" +summary = "Patch asyncio to allow nested event loops" +groups = ["default", "docs"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Node.js virtual environment builder" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +requires_python = ">=3.7" +summary = "Capture the outcome of Python function calls." +groups = ["test"] +dependencies = [ + "attrs>=19.2.0", +] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[[package]] +name = "packaging" +version = "24.1" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default", "docs", "test"] +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "parso" +version = "0.8.4" +requires_python = ">=3.6" +summary = "A Python Parser" +groups = ["default", "docs", "test"] +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +summary = "Pexpect allows easy control of interactive console applications." +groups = ["default", "docs", "test"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "ptyprocess>=0.5", +] +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default", "dev", "docs"] +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "pre-commit" +version = "3.7.1" +requires_python = ">=3.9" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +groups = ["dev"] +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.47" +requires_python = ">=3.7.0" +summary = "Library for building powerful interactive command lines in Python" +groups = ["default", "docs", "test"] +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, +] + +[[package]] +name = "psutil" +version = "6.0.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "Cross-platform lib for process and system monitoring in Python." +groups = ["default", "docs"] +files = [ + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +summary = "Run a subprocess in a pseudo terminal" +groups = ["default", "docs", "test"] +marker = "sys_platform != \"win32\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +summary = "Safely evaluate AST nodes without side effects" +groups = ["default", "docs", "test"] +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default", "docs", "test"] +marker = "os_name == \"nt\" or implementation_name == \"pypy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.15.4" +requires_python = ">=3.9" +summary = "Bootstrap-based Sphinx theme from the PyData community" +groups = ["docs"] +dependencies = [ + "Babel", + "accessible-pygments", + "beautifulsoup4", + "docutils!=0.17.0", + "packaging", + "pygments>=2.7", + "sphinx>=5", + "typing-extensions", +] +files = [ + {file = "pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6"}, + {file = "pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default", "docs", "test"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +groups = ["test"] +files = [ + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pytest" +version = "7.4.4" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +groups = ["test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +requires_python = ">=3.8" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[[package]] +name = "pytest-datadir" +version = "1.5.0" +requires_python = ">=3.8" +summary = "pytest plugin for test data directories and files" +groups = ["test"] +dependencies = [ + "pytest>=5.0", +] +files = [ + {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, + {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, +] + +[[package]] +name = "pytest-regressions" +version = "2.5.0" +requires_python = ">=3.8" +summary = "Easy to use fixtures to write regression tests." +groups = ["test"] +dependencies = [ + "pytest-datadir>=1.2.0", + "pytest>=6.2.0", + "pyyaml", +] +files = [ + {file = "pytest-regressions-2.5.0.tar.gz", hash = "sha256:818c7884c1cff3babf89eebb02cbc27b307856b1985427c24d52cb812a106fd9"}, + {file = "pytest_regressions-2.5.0-py3-none-any.whl", hash = "sha256:8c4e5c4037325fdb0825bc1fdcb75e17e03adf3407049f0cb704bb996d496255"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["default", "docs"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "pywin32" +version = "306" +summary = "Python for Window Extensions" +groups = ["default", "docs"] +marker = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +groups = ["default", "dev", "docs", "test"] +files = [ + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyzmq" +version = "26.0.3" +requires_python = ">=3.7" +summary = "Python bindings for 0MQ" +groups = ["default", "docs"] +dependencies = [ + "cffi; implementation_name == \"pypy\"", +] +files = [ + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, + {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, + {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +requires_python = ">=3.8" +summary = "JSON Referencing + Python" +groups = ["default", "docs"] +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", +] +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["default", "docs", "test"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[[package]] +name = "rpds-py" +version = "0.18.1" +requires_python = ">=3.8" +summary = "Python bindings to Rust's persistent data structures (rpds)" +groups = ["default", "docs"] +files = [ + {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, + {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, + {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, + {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, +] + +[[package]] +name = "ruff" +version = "0.5.0" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, + {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, + {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, + {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, + {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, + {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, + {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, +] + +[[package]] +name = "selenium" +version = "4.22.0" +requires_python = ">=3.8" +summary = "Official Python bindings for Selenium WebDriver" +groups = ["test"] +dependencies = [ + "certifi>=2021.10.8", + "trio-websocket~=0.9", + "trio~=0.17", + "typing-extensions>=4.9.0", + "urllib3[socks]<3,>=1.26", + "websocket-client>=1.8.0", +] +files = [ + {file = "selenium-4.22.0-py3-none-any.whl", hash = "sha256:e424991196e9857e19bf04fe5c1c0a4aac076794ff5e74615b1124e729d93104"}, + {file = "selenium-4.22.0.tar.gz", hash = "sha256:903c8c9d61b3eea6fcc9809dc7d9377e04e2ac87709876542cc8f863e482c4ce"}, +] + +[[package]] +name = "setuptools" +version = "70.1.1" +requires_python = ">=3.8" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["test"] +files = [ + {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"}, + {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default", "docs", "test"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["test"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +groups = ["default", "docs", "test"] +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["test"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +requires_python = ">=3.8" +summary = "A modern CSS selector implementation for Beautiful Soup." +groups = ["docs"] +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "sphinx" +version = "7.3.7" +requires_python = ">=3.9" +summary = "Python documentation generator" +groups = ["default", "docs", "test"] +dependencies = [ + "Jinja2>=3.0", + "Pygments>=2.14", + "alabaster~=0.7.14", + "babel>=2.9", + "colorama>=0.4.5; sys_platform == \"win32\"", + "docutils<0.22,>=0.18.1", + "imagesize>=1.3", + "importlib-metadata>=4.8; python_version < \"3.10\"", + "packaging>=21.0", + "requests>=2.25.0", + "snowballstemmer>=2.0", + "sphinxcontrib-applehelp", + "sphinxcontrib-devhelp", + "sphinxcontrib-htmlhelp>=2.0.0", + "sphinxcontrib-jsmath", + "sphinxcontrib-qthelp", + "sphinxcontrib-serializinghtml>=1.1.9", + "tomli>=2; python_version < \"3.11\"", +] +files = [ + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, +] + +[[package]] +name = "sphinx-book-theme" +version = "1.1.3" +requires_python = ">=3.9" +summary = "A clean book theme for scientific explanations and documentation with Sphinx" +groups = ["docs"] +dependencies = [ + "pydata-sphinx-theme>=0.15.2", + "sphinx>=5", +] +files = [ + {file = "sphinx_book_theme-1.1.3-py3-none-any.whl", hash = "sha256:a554a9a7ac3881979a87a2b10f633aa2a5706e72218a10f71be38b3c9e831ae9"}, + {file = "sphinx_book_theme-1.1.3.tar.gz", hash = "sha256:1f25483b1846cb3d353a6bc61b3b45b031f4acf845665d7da90e01ae0aef5b4d"}, +] + +[[package]] +name = "sphinx" +version = "7.3.7" +extras = ["test"] +requires_python = ">=3.9" +summary = "Python documentation generator" +groups = ["test"] +dependencies = [ + "Sphinx==7.3.7", + "cython>=3.0", + "defusedxml>=0.7.1", + "pytest>=6.0", + "setuptools>=67.0", +] +files = [ + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.8" +requires_python = ">=3.9" +summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +groups = ["default", "docs", "test"] +files = [ + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.6" +requires_python = ">=3.9" +summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +groups = ["default", "docs", "test"] +files = [ + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.5" +requires_python = ">=3.9" +summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +groups = ["default", "docs", "test"] +files = [ + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +requires_python = ">=3.5" +summary = "A sphinx extension which renders display math in HTML via JavaScript" +groups = ["default", "docs", "test"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.7" +requires_python = ">=3.9" +summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +groups = ["default", "docs", "test"] +files = [ + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.10" +requires_python = ">=3.9" +summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +groups = ["default", "docs", "test"] +files = [ + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.31" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["default", "docs"] +dependencies = [ + "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"}, + {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, + {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +summary = "Extract data from python stack frames and tracebacks for informative displays" +groups = ["default", "docs", "test"] +dependencies = [ + "asttokens>=2.1.0", + "executing>=1.2.0", + "pure-eval", +] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +requires_python = ">=3.7" +summary = "Pretty-print tabular data" +groups = ["default", "docs"] +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["default", "dev", "docs", "test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tornado" +version = "6.4.1" +requires_python = ">=3.8" +summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +groups = ["default", "docs"] +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +requires_python = ">=3.8" +summary = "Traitlets Python configuration system" +groups = ["default", "docs", "test"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[[package]] +name = "trio" +version = "0.25.1" +requires_python = ">=3.8" +summary = "A friendly Python library for async concurrency and I/O" +groups = ["test"] +dependencies = [ + "attrs>=23.2.0", + "cffi>=1.14; os_name == \"nt\" and implementation_name != \"pypy\"", + "exceptiongroup; python_version < \"3.11\"", + "idna", + "outcome", + "sniffio>=1.3.0", + "sortedcontainers", +] +files = [ + {file = "trio-0.25.1-py3-none-any.whl", hash = "sha256:e42617ba091e7b2e50c899052e83a3c403101841de925187f61e7b7eaebdf3fb"}, + {file = "trio-0.25.1.tar.gz", hash = "sha256:9f5314f014ea3af489e77b001861c535005c3858d38ec46b6b071ebfa339d7fb"}, +] + +[[package]] +name = "trio-websocket" +version = "0.11.1" +requires_python = ">=3.7" +summary = "WebSocket library for Trio" +groups = ["test"] +dependencies = [ + "exceptiongroup; python_version < \"3.11\"", + "trio>=0.11", + "wsproto>=0.14", +] +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "dev", "docs", "test"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default", "docs", "test"] +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +extras = ["socks"] +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["test"] +dependencies = [ + "pysocks!=1.5.7,<2.0,>=1.5.6", + "urllib3==2.2.2", +] +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.3" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +groups = ["dev"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +summary = "Measures the displayed width of unicode strings in a terminal" +groups = ["default", "docs", "test"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +requires_python = ">=3.8" +summary = "WebSocket client for Python with low level API options" +groups = ["test"] +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +requires_python = ">=3.7.0" +summary = "WebSockets state-machine based protocol implementation" +groups = ["test"] +dependencies = [ + "h11<1,>=0.9.0", +] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[[package]] +name = "zipp" +version = "3.19.2" +requires_python = ">=3.8" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["default", "docs", "test"] +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..042b27b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,119 @@ +[project] +name = "myst-nb-json" +authors = [ + {name = "Alexandrov Team, EMBL", email = "andreas.eisenbarth@embl.de"}, + {name = "Andreas Eisenbarth", email = "andreas.eisenbarth@embl.de"}, +] +description = "Default template for PDM package" +keywords = ["docutils", "sphinx", "json"] +requires-python = "==3.9.*" +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: Sphinx :: Extension", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Markup", +] +dependencies = [ + "docutils", + "myst-nb>=1.0.0", +] +license = { text = "MIT" } +dynamic = ["version", "readme"] + +[project.optional-dependencies] +dev = [ + "mypy>=1.8.0", + "pre-commit>=3.6.0", + "ruff>=0.1.14", +] +docs = [ + "docutils>=0.21.2", + "ipython>=8.12.0", + "myst-nb>=1.0.0", + "Sphinx>=7.2.6", + "sphinx-book-theme>=1.1.3", +] +test = [ + "ipython>=8.12.0", + "pytest~=7.1", + "pytest-cov>=5.0.0", + "pytest-regressions>=2.5.0", + "selenium~=4.22", + "Sphinx[test]>=7.2.6", # For test dependencies like defusedxml +] +type-hints = ["types-docutils"] + +[project.entry-points."myst_nb.mime_renderers"] +application_json = "myst_nb_json:JsonMimeRenderPlugin" + +[tool.black] +line-length = 100 +target-version = ['py310'] +skip-magic-trailing-comma = true +include = '\.(pyi?|ipynb)$' +exclude = ''' +^/( + ( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.mypy_cache + | \.pytest_cache + | build + | dist + ) +)/ +''' + +[tool.isort] +combine_as_imports = true +filter_files = true +line_length = 100 +profile = "black" + +[[tool.mypy.overrides]] +module = [ + "myst_nb.core.render", +] +ignore_missing_imports = true + +[tool.pdm] +distribution = true + +[tool.pdm.version] +source = "file" +path = "myst_nb_json/__init__.py" + +[tool.pdm.scripts] +ruff_check = { cmd = "ruff check myst_nb_json/ docs/ tests/" } +ruff_format_check = { cmd = "ruff format --check myst_nb_json/ docs/ tests/" } +mypy_check = { cmd = "mypy myst_nb_json/" } +lint = { composite = ["ruff_check", "ruff_format_check", "mypy_check"] } +ruff_fix = { cmd = "ruff check --fix-only myst_nb_json/ docs/ tests/" } +ruff_format = { cmd = "ruff format myst_nb_json/ docs/ tests/" } +format = { composite = ["ruff_fix", "ruff_format"] } +test = { cmd = "pytest --cov=myst_nb_json --cov-report=xml --cov-report=term-missing" } +docs = { cmd = "sphinx-build -b html docs/ docs/_build" } + +[tool.pycln] +all = true + +[tool.pytest.ini_options] +minversion = "6.0" +# CLI options: Show extra test summary for all except passed; quiet +addopts = "-ra --quiet --color=yes --code-highlight=yes" +# Directories to be searched for tests to speed up test collection +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 + +[tool.setuptools.dynamic] +version = {attr = "myst_nb_json.__version__"} +readme = {file = "README.md", content-type = "text/markdown"} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..aad3a14 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,168 @@ +# Source: https://github.com/executablebooks/MyST-NB/blob/master/tests/conftest.py +import json +import os +import re +import uuid +from pathlib import Path + +import pytest +import sphinx +from docutils.nodes import image as image_node +from sphinx import version_info as sphinx_version_info +from sphinx.util.console import nocolor + +pytest_plugins = "sphinx.testing.fixtures" + +TEST_FILE_DIR = Path(__file__).parent.joinpath("notebooks") + + +@pytest.fixture() +def get_test_path(): + def _get_test_path(name): + return TEST_FILE_DIR.joinpath(name) + + return _get_test_path + + +class SphinxFixture: + """A class returned by the ``sphinx_run`` fixture, to run sphinx, + and retrieve aspects of the build. + """ + + def __init__(self, app, filenames): + self.app = app + self.env = app.env + self.files = [os.path.splitext(ff) for ff in filenames] + self.software_versions = ( + f".sphinx{sphinx.version_info[0]}" # software version tracking for fixtures + ) + + def build(self): + """Run the sphinx build.""" + self.app.build() + + def warnings(self): + """Return the stderr stream of the sphinx build.""" + return self.app._warning.getvalue().strip() + + def get_resolved_doctree(self, docname=None): + """Load and return the built docutils.document, after post-transforms.""" + docname = docname or self.files[0][0] + doctree = self.env.get_and_resolve_doctree(docname, self.app.builder) + doctree["source"] = docname + return doctree + + +@pytest.fixture() +def sphinx_params(request): + """Parameters that are specified by 'pytest.mark.sphinx_params' + are passed to the ``sphinx_run`` fixture:: + + @pytest.mark.sphinx_params("name.ipynb", conf={"option": "value"}) + def test_something(sphinx_run): + ... + + The first file specified here will be set as the master_doc + """ + markers = request.node.iter_markers("sphinx_params") + kwargs = {} + if markers is not None: + for info in reversed(list(markers)): + kwargs.update(info.kwargs) + kwargs["files"] = info.args + return kwargs + + +@pytest.fixture() +def sphinx_run(sphinx_params, make_app, tmp_path): + """A fixture to setup and run a sphinx build, in a sandboxed folder. + + The `myst_nb` extension is added by default, + and the first file will be set as the masterdoc + + """ + assert len(sphinx_params["files"]) > 0, sphinx_params["files"] + conf = sphinx_params.get("conf", {}) + buildername = sphinx_params.get("buildername", "html") + + confoverrides = { + "extensions": ["myst_nb"], + "master_doc": os.path.splitext(sphinx_params["files"][0])[0], + "exclude_patterns": ["_build"], + "nb_execution_show_tb": True, + } + confoverrides.update(conf) + + current_dir = os.getcwd() + if "working_dir" in sphinx_params: + base_dir = Path(sphinx_params["working_dir"]) / str(uuid.uuid4()) + else: + base_dir = tmp_path + srcdir = base_dir / "source" + srcdir.mkdir(exist_ok=True) + os.chdir(base_dir) + (srcdir / "conf.py").write_text( + "# conf overrides (passed directly to sphinx):\n" + + "\n".join(["# " + ll for ll in json.dumps(confoverrides, indent=2).splitlines()]) + + "\n" + ) + + for nb_file in sphinx_params["files"]: + nb_path = TEST_FILE_DIR.joinpath(nb_file) + assert nb_path.exists(), nb_path + (srcdir / nb_file).parent.mkdir(exist_ok=True) + (srcdir / nb_file).write_text(nb_path.read_text(encoding="utf-8"), encoding="utf-8") + + nocolor() + + # For compatibility with multiple versions of sphinx, convert pathlib.Path to + # sphinx.testing.path.path here. + if sphinx_version_info >= (7, 2): + app_srcdir = srcdir + else: + from sphinx.testing.path import path + + app_srcdir = path(os.fspath(srcdir)) + app = make_app(buildername=buildername, srcdir=app_srcdir, confoverrides=confoverrides) + + yield SphinxFixture(app, sphinx_params["files"]) + + # reset working directory + os.chdir(current_dir) + + +@pytest.fixture() +def clean_doctree(): + def _func(doctree): + if os.name == "nt": # on Windows file paths are absolute + findall = getattr(doctree, "findall", doctree.traverse) + for node in findall(image_node): # type: image_node + if "candidates" in node: + node["candidates"]["*"] = "_build/jupyter_execute/" + os.path.basename( + node["candidates"]["*"] + ) + if "uri" in node: + node["uri"] = "_build/jupyter_execute/" + os.path.basename(node["uri"]) + return doctree + + return _func + + +@pytest.fixture() +def file_regression(file_regression): + return FileRegression(file_regression) + + +class FileRegression: + ignores = (r"",) + + def __init__(self, file_regression): + self.file_regression = file_regression + + def check(self, data, **kwargs): + return self.file_regression.check(self._strip_ignores(data), **kwargs) + + def _strip_ignores(self, data): + for ig in self.ignores: + data = re.sub(ig, "", data) + return data diff --git a/tests/notebooks/json_output.ipynb b/tests/notebooks/json_output.ipynb new file mode 100644 index 0000000..9b99ec4 --- /dev/null +++ b/tests/notebooks/json_output.ipynb @@ -0,0 +1,54 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from IPython.display import JSON\n", + "\n", + "JSON(\n", + " {\n", + " \"key1\": \"value1\",\n", + " \"key2\": {\n", + " \"key21\": \"str\",\n", + " \"key22\": 42,\n", + " \"key23\": 3.14,\n", + " \"key24\": True,\n", + " \"key25\": False,\n", + " \"key26\": None,\n", + " \"key27\": {},\n", + " \"key28\": [],\n", + " },\n", + " \"key3\": [\"str\", 42, 3.14, True, False, None, {}, []],\n", + " },\n", + " root=\"test\",\n", + " expanded=True,\n", + ")" + ], + "id": "initial_id", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_interactivity.py b/tests/test_interactivity.py new file mode 100644 index 0000000..f124d7d --- /dev/null +++ b/tests/test_interactivity.py @@ -0,0 +1,73 @@ +import re +from collections.abc import Generator + +import pytest +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver + +from myst_nb_json import CLS_COLLAPSED, JsonMimeRenderPlugin + + +@pytest.fixture +def plugin() -> JsonMimeRenderPlugin: + return JsonMimeRenderPlugin() + + +JSON_DATA = { + "key1": "value1", + "key2": { + "key21": "str", + "key22": 42, + "key23": 3.14, + "key24": True, + "key25": False, + "key26": None, + "key27": {}, + "key28": [], + }, + "key3": ["str", 42, 3.14, True, False, None, {}, []], +} + + +@pytest.fixture +def browser() -> Generator[WebDriver]: + # Set up headless Chrome options + chrome_options = Options() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--disable-gpu") + + # Initialize the WebDriver + driver: WebDriver = webdriver.Chrome(options=chrome_options) + yield driver + driver.quit() + + +def test_interactive_collapse_expand(plugin: JsonMimeRenderPlugin, browser: WebDriver): + # Load the HTML string + html_content = plugin.html(JSON_DATA, root="test", expanded=True) + # Hash (in CSS color) causes problems for Selenium Chrome webdriver + html_content = re.sub("#", "#", html_content) + browser.get("data:text/html;charset=utf-8," + html_content) + + # Wait for the JavaScript to execute + browser.implicitly_wait(2) + + text_to_click = "key2" + element_to_click = browser.find_element(By.XPATH, f"//*[contains(text(), '{text_to_click}')]") + value_element = element_to_click.find_element(By.XPATH, "following-sibling::*[1]") + li_element = element_to_click.find_element(By.XPATH, "ancestor::li[1]") + + assert CLS_COLLAPSED not in li_element.get_attribute("class") + assert value_element.value_of_css_property("display") != "none" + + # Collapse + element_to_click.click() + assert CLS_COLLAPSED in li_element.get_attribute("class") + assert value_element.value_of_css_property("display") == "none" + + # Expand + element_to_click.click() + assert CLS_COLLAPSED not in li_element.get_attribute("class") + assert value_element.value_of_css_property("display") != "none" diff --git a/tests/test_mime_plugin.py b/tests/test_mime_plugin.py new file mode 100644 index 0000000..d3dd581 --- /dev/null +++ b/tests/test_mime_plugin.py @@ -0,0 +1,101 @@ +import re + +import pytest + +from myst_nb_json import JsonMimeRenderPlugin + + +@pytest.fixture +def plugin() -> JsonMimeRenderPlugin: + return JsonMimeRenderPlugin() + + +def _strip_xml_attributes(string: str) -> str: + return re.sub(pattern=r"<(?P\w+)(?:\s+[^>]*)>", repl=r"<\g>", string=string) + + +def _strip_xml_whitespace(string: str) -> str: + return re.sub(pattern=r">\s+<", repl="><", string=string) + + +def _strip_xml_tags(string: str) -> str: + return re.sub(pattern=r"<\/?\w+[^>]*>", repl="", string=string) + + +CHAR_to_HTML_ENTITY = { + ">": ">", + "<": "<", + "&": "&", + # These also cause problems, although they should not strictly need escaping + "[": "[", + "]": "]", + "{": "{", + "}": "}", + '"': """, + # Hash (in CSS color) causes problems for Selenium Chrome webdriver + "#": "#", +} +HTML_ENTITY_TO_CHAR = {v: k for k, v in CHAR_to_HTML_ENTITY.items()} + + +def _html_unescape(string: str) -> str: + return re.sub( + pattern=r"&\w+;", + repl=lambda m: HTML_ENTITY_TO_CHAR.get(m.group(), m.group()), + string=string, + ) + + +@pytest.mark.parametrize( + ("value", "expected_xml"), + [ + ("abc", '"abc"'), + (42, "42"), + (3.14, "3.14"), + (True, "true"), + (False, "false"), + (None, "null"), + ], +) +def test_scalar_value(plugin: JsonMimeRenderPlugin, value, expected_xml: str): + actual = "".join(plugin.scalar_value(value)) + # It should produce the expected HTML structure + assert _strip_xml_whitespace(_strip_xml_attributes(actual)) == expected_xml + + +@pytest.mark.parametrize( + ("value", "expected_xml", "expected_text"), + [ + ([], "
[
    ]
    ", "[]"), + ( + ["a", "b"], + '
    [
    • "a",
    • "b"
    ]
    ', + '["a", "b"]', + ), + ], +) +def test_list_value(plugin: JsonMimeRenderPlugin, value, expected_xml: str, expected_text: str): + actual = "".join(plugin.list_value(value)) + # It should produce the expected HTML structure + assert _strip_xml_whitespace(_strip_xml_attributes(actual)) == expected_xml + # It should preserve JSON syntax as user-electable text (even if brackets are not displayed) + assert _html_unescape(_strip_xml_tags(actual)) == expected_text + + +@pytest.mark.parametrize( + ("value", "expected_xml", "expected_text"), + [ + ({}, "
    {
      }
      ", "{}"), + ( + {"k1": "v1", "k2": "v2"}, + '
      {
      • "k1": "v1",
      • "k2": "v2"
      }
      ', + '{"k1": "v1", "k2": "v2"}', + ), + ], +) +def test_dict_value(plugin: JsonMimeRenderPlugin, value, expected_xml: str, expected_text: str): + actual = "".join(plugin.dict_value(value)) + # It should produce the expected HTML structure + assert _strip_xml_whitespace(_strip_xml_attributes(actual)) == expected_xml + # It should preserve JSON syntax as user-electable text (even if braces and quotes are not displayed) + assert _html_unescape(_strip_xml_tags(actual)) == expected_text diff --git a/tests/test_myst_nb.py b/tests/test_myst_nb.py new file mode 100644 index 0000000..198bd33 --- /dev/null +++ b/tests/test_myst_nb.py @@ -0,0 +1,17 @@ +import pytest + +from tests.conftest import clean_doctree, file_regression, get_test_path, sphinx_run # noqa: F401 + + +@pytest.mark.sphinx_params("json_output.ipynb", conf={"nb_execution_mode": "force"}) +def test_render_json_output( + sphinx_run, # noqa: F811 + clean_doctree, # noqa: F811 + file_regression, # noqa: F811 + get_test_path, # noqa: F811 +): + """Test that a cell with JSON output is rendered with the MIME type plugin""" + sphinx_run.build() + assert sphinx_run.warnings() == "" + doctree = clean_doctree(sphinx_run.get_resolved_doctree("json_output")) + file_regression.check(doctree.pformat(), extension=".xml", encoding="utf-8") diff --git a/tests/test_myst_nb/test_render_json_output.xml b/tests/test_myst_nb/test_render_json_output.xml new file mode 100644 index 0000000..c902bec --- /dev/null +++ b/tests/test_myst_nb/test_render_json_output.xml @@ -0,0 +1,113 @@ + + + + + from IPython.display import JSON + + JSON( + { + "key1": "value1", + "key2": { + "key21": "str", + "key22": 42, + "key23": 3.14, + "key24": True, + "key25": False, + "key26": None, + "key27": {}, + "key28": [], + }, + "key3": ["str", 42, 3.14, True, False, None, {}, []], + }, + root="test", + expanded=True, + ) + + +
      • "test":
        {
        • "key1": "value1",
        • "key2":
          {
          • "key21": "str",
          • "key22": 42,
          • "key23": 3.14,
          • "key24": true,
          • "key25": false,
          • "key26": null,
          • "key27": {},
          • "key28": []
          },
        • "key3":
          [
          • "str",
          • 42,
          • 3.14,
          • true,
          • false,
          • null,
          • {},
          • []
          ]
        }